DefaultMetaschemaClassFactory.java
/*
* Portions of this software was developed by employees of the National Institute
* of Standards and Technology (NIST), an agency of the Federal Government and is
* being made available as a public service. Pursuant to title 17 United States
* Code Section 105, works of NIST employees are not subject to copyright
* protection in the United States. This software may be subject to foreign
* copyright. Permission in the United States and in foreign countries, to the
* extent that NIST may hold copyright, to use, copy, modify, create derivative
* works, and distribute this software and its documentation without fee is hereby
* granted on a non-exclusive basis, provided that this notice and disclaimer
* of warranty appears in all copies.
*
* THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
* EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
* THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
* INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
* SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE. IN NO EVENT
* SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
* INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
* OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
* CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
* PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
* OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
*/
package gov.nist.secauto.metaschema.databind.codegen;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;
import gov.nist.secauto.metaschema.core.datatype.IDataTypeAdapter;
import gov.nist.secauto.metaschema.core.datatype.adapter.MetaschemaDataTypeProvider;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupLine;
import gov.nist.secauto.metaschema.core.datatype.markup.MarkupMultiline;
import gov.nist.secauto.metaschema.core.model.IAssemblyDefinition;
import gov.nist.secauto.metaschema.core.model.IAssemblyInstance;
import gov.nist.secauto.metaschema.core.model.IDefinition;
import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
import gov.nist.secauto.metaschema.core.model.IFieldInstance;
import gov.nist.secauto.metaschema.core.model.IFlagContainer;
import gov.nist.secauto.metaschema.core.model.IFlagDefinition;
import gov.nist.secauto.metaschema.core.model.IFlagInstance;
import gov.nist.secauto.metaschema.core.model.IModule;
import gov.nist.secauto.metaschema.core.model.INamedModelInstance;
import gov.nist.secauto.metaschema.core.model.JsonGroupAsBehavior;
import gov.nist.secauto.metaschema.core.model.MetaschemaModelConstants;
import gov.nist.secauto.metaschema.core.model.XmlGroupAsBehavior;
import gov.nist.secauto.metaschema.core.util.CollectionUtil;
import gov.nist.secauto.metaschema.core.util.ObjectUtils;
import gov.nist.secauto.metaschema.databind.IBindingContext;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.IAssemblyDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.IFieldDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.IFieldValueTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.IFlagInstanceTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.IInstanceTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.IModelDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.IModelInstanceTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.ITypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.ITypeResolver;
import gov.nist.secauto.metaschema.databind.model.AbstractBoundModule;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundAssembly;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundField;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundFieldValue;
import gov.nist.secauto.metaschema.databind.model.annotations.BoundFlag;
import gov.nist.secauto.metaschema.databind.model.annotations.GroupAs;
import gov.nist.secauto.metaschema.databind.model.annotations.JsonFieldValueKeyFlag;
import gov.nist.secauto.metaschema.databind.model.annotations.JsonKey;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaAssembly;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaField;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaFieldValue;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaPackage;
import gov.nist.secauto.metaschema.databind.model.annotations.Module;
import gov.nist.secauto.metaschema.databind.model.annotations.XmlNs;
import gov.nist.secauto.metaschema.databind.model.annotations.XmlNsForm;
import gov.nist.secauto.metaschema.databind.model.annotations.XmlSchema;
import org.apache.commons.lang3.builder.MultilineRecursiveToStringStyle;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.lang.model.element.Modifier;
import edu.umd.cs.findbugs.annotations.NonNull;
@SuppressWarnings({
"PMD.CouplingBetweenObjects", // ok
"PMD.GodClass", // ok
"PMD.CyclomaticComplexity" // ok
})
class DefaultMetaschemaClassFactory implements IMetaschemaClassFactory {
@NonNull
private final ITypeResolver typeResolver;
/**
* Get a new instance of the this class generation factory that uses the
* provided {@code typeResolver}.
*
* @param typeResolver
* the resolver used to generate type information for Metasschema
* constructs
* @return the new class factory
*/
@NonNull
public static DefaultMetaschemaClassFactory newInstance(@NonNull ITypeResolver typeResolver) {
return new DefaultMetaschemaClassFactory(typeResolver);
}
/**
* Construct a new instance of the this class ganeration factory that uses the
* provided {@code typeResolver}.
*
* @param typeResolver
* the resolver used to generate type information for Metasschema
* constructs
*/
protected DefaultMetaschemaClassFactory(@NonNull ITypeResolver typeResolver) {
this.typeResolver = typeResolver;
}
@Override
@NonNull
public ITypeResolver getTypeResolver() {
return typeResolver;
}
@Override
public IGeneratedModuleClass generateClass(
IModule module,
Path targetDirectory) throws IOException {
// Generate the Module module class
ClassName className = getTypeResolver().getClassName(module);
TypeSpec.Builder classSpec = newClassBuilder(module, className);
JavaFile javaFile = JavaFile.builder(className.packageName(), classSpec.build()).build();
Path classFile = ObjectUtils.notNull(javaFile.writeToPath(targetDirectory));
// now generate all related definition classes
Stream<? extends IFlagContainer> globalDefinitions = Stream.concat(
module.getAssemblyDefinitions().stream(),
module.getFieldDefinitions().stream());
Set<String> classNames = new HashSet<>();
@SuppressWarnings("PMD.UseConcurrentHashMap") // map is unmodifiable
Map<IFlagContainer, IGeneratedDefinitionClass> definitionProductions
= ObjectUtils.notNull(globalDefinitions
// Get type information for assembly and field definitions.
// Avoid field definitions without flags that don't require a generated class
.flatMap(definition -> {
IModelDefinitionTypeInfo typeInfo = null;
if (definition instanceof IAssemblyDefinition) {
typeInfo = IAssemblyDefinitionTypeInfo.newTypeInfo((IAssemblyDefinition) definition, typeResolver);
} else if (definition instanceof IFieldDefinition
&& !((IFieldDefinition) definition).getFlagInstances().isEmpty()) {
typeInfo = IFieldDefinitionTypeInfo.newTypeInfo((IFieldDefinition) definition, typeResolver);
} // otherwise field is just a simple data value, then no class is needed
return typeInfo == null ? null : Stream.of(typeInfo);
})
// generate the class for each type information
.map(typeInfo -> {
IFlagContainer definition = typeInfo.getDefinition();
IGeneratedDefinitionClass generatedClass;
try {
generatedClass = generateClass(typeInfo, targetDirectory);
} catch (RuntimeException ex) { // NOPMD - intended
throw new IllegalStateException(
String.format("Unable to generate class for definition '%s' in Module '%s'",
definition.getName(),
module.getLocation()),
ex);
} catch (IOException ex) {
throw new IllegalStateException(ex);
}
String defClassName = generatedClass.getClassName().canonicalName();
if (classNames.contains(defClassName)) {
throw new IllegalStateException(String.format(
"Found duplicate class '%s' in metaschema '%s'."
+ " All class names must be unique within the same namespace.",
defClassName, module.getLocation()));
}
classNames.add(defClassName);
return generatedClass;
})
// collect the generated class information
.collect(Collectors.toUnmodifiableMap(
IGeneratedDefinitionClass::getDefinition,
Function.identity())));
String packageName = typeResolver.getPackageName(module);
return new DefaultGeneratedModuleClass(module, className, classFile, definitionProductions, packageName);
}
@Override
public IGeneratedDefinitionClass generateClass(
IModelDefinitionTypeInfo typeInfo,
Path targetDirectory)
throws IOException {
ClassName className = typeInfo.getClassName();
TypeSpec.Builder classSpec = newClassBuilder(typeInfo, false);
JavaFile javaFile = JavaFile.builder(className.packageName(), classSpec.build()).build();
Path classFile = ObjectUtils.notNull(javaFile.writeToPath(targetDirectory));
return new DefaultGeneratedDefinitionClass(classFile, className, typeInfo.getDefinition());
}
@Override
public IGeneratedClass generatePackageInfoClass(
String javaPackage,
URI xmlNamespace,
Collection<IGeneratedModuleClass> moduleProductions,
Path targetDirectory) throws IOException {
String packagePath = javaPackage.replace(".", "/");
Path packageInfo = ObjectUtils.notNull(targetDirectory.resolve(packagePath + "/package-info.java"));
try (PrintWriter writer = new PrintWriter(
Files.newBufferedWriter(packageInfo, StandardOpenOption.CREATE, StandardOpenOption.WRITE,
StandardOpenOption.TRUNCATE_EXISTING))) {
writer.format("@%1$s(moduleClass = {%n", MetaschemaPackage.class.getName());
boolean first = true;
for (IGeneratedModuleClass moduleProduction : moduleProductions) {
if (first) {
first = false;
} else {
writer.format(",%n");
}
writer.format(" %1$s.class", moduleProduction.getClassName().canonicalName());
}
writer.format("})%n");
writer.format(
"@%1$s(namespace = \"%2$s\", xmlns = {@%3$s(prefix = \"\", namespace = \"%2$s\")},"
+ " xmlElementFormDefault = %4$s.QUALIFIED)%n",
XmlSchema.class.getName(), xmlNamespace.toString(), XmlNs.class.getName(), XmlNsForm.class.getName());
writer.format("package %s;%n", javaPackage);
}
return new DefaultGeneratedClass(packageInfo, ObjectUtils.notNull(ClassName.get(javaPackage, "package-info")));
}
/**
* Creates and configures a builder, for a Module module, that can be used to
* generate a Java class.
*
* @param module
* a parsed Module module
* @param className
* the name of the class to create for the Module module
* @return the class builder
*/
@NonNull
protected TypeSpec.Builder newClassBuilder(
@NonNull IModule module,
@NonNull ClassName className) { // NOPMD - long, but readable
// create the class
TypeSpec.Builder builder = TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL);
builder.superclass(AbstractBoundModule.class);
AnnotationSpec.Builder moduleAnnotation = AnnotationSpec.builder(Module.class);
ITypeResolver typeResolver = getTypeResolver();
for (IFieldDefinition definition : module.getFieldDefinitions()) {
if (!definition.isSimple()) {
moduleAnnotation.addMember("fields", "$T.class", typeResolver.getClassName(definition));
}
}
for (IAssemblyDefinition definition : module.getAssemblyDefinitions()) {
moduleAnnotation.addMember(
"assemblies",
"$T.class",
typeResolver.getClassName(ObjectUtils.notNull(definition)));
}
for (IModule moduleImport : module.getImportedModules()) {
moduleAnnotation.addMember(
"imports",
"$T.class",
typeResolver.getClassName(ObjectUtils.notNull(moduleImport)));
}
{
MarkupMultiline remarks = module.getRemarks();
if (remarks != null) {
moduleAnnotation.addMember("remarks", "$S", remarks.toMarkdown());
}
}
builder.addAnnotation(moduleAnnotation.build());
builder.addField(
FieldSpec.builder(MarkupLine.class, "NAME", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.fromMarkdown($S)", MarkupLine.class, module.getName().toMarkdown())
.build());
builder.addField(
FieldSpec.builder(String.class, "SHORT_NAME", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$S", module.getShortName())
.build());
builder.addField(
FieldSpec.builder(String.class, "VERSION", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$S", module.getVersion())
.build());
builder.addField(
FieldSpec.builder(URI.class, "XML_NAMESPACE", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.create($S)", URI.class, module.getXmlNamespace())
.build());
builder.addField(
FieldSpec.builder(URI.class, "JSON_BASE_URI", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.create($S)", URI.class, module.getJsonBaseUri())
.build());
MarkupMultiline remarks = module.getRemarks();
if (remarks != null) {
builder.addField(
FieldSpec.builder(MarkupMultiline.class, "REMARKS", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("$T.fromMarkdown($S)", MarkupMultiline.class, remarks.toMarkdown())
.build());
}
builder.addMethod(
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(
ParameterizedTypeName.get(ClassName.get(List.class),
WildcardTypeName.subtypeOf(IModule.class).box()),
"importedModules")
.addParameter(IBindingContext.class, "bindingContext")
.addStatement("super($N, $N)", "importedModules", "bindingContext")
.build());
builder.addMethod(
MethodSpec.methodBuilder("getName")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(MarkupLine.class)
.addStatement("return NAME")
.build());
builder.addMethod(
MethodSpec.methodBuilder("getShortName")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addStatement("return SHORT_NAME")
.build());
builder.addMethod(
MethodSpec.methodBuilder("getVersion")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(String.class)
.addStatement("return VERSION")
.build());
builder.addMethod(
MethodSpec.methodBuilder("getXmlNamespace")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(URI.class)
.addStatement("return XML_NAMESPACE")
.build());
builder.addMethod(
MethodSpec.methodBuilder("getJsonBaseUri")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(URI.class)
.addStatement("return JSON_BASE_URI")
.build());
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("getRemarks")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(MarkupMultiline.class);
if (remarks == null) {
methodBuilder.addStatement("return null");
} else {
methodBuilder.addStatement("return REMARKS");
}
builder.addMethod(methodBuilder.build());
return builder;
}
/**
* Creates and configures a builder, for a Module model definition, that can be
* used to generate a Java class.
*
* @param typeInfo
* the type information for the class to generate
* @param isChild
* {@code true} if the class to be generated is a child class, or
* {@code false} otherwise
* @return the class builder
* @throws IOException
* if an error occurred while building the Java class
*/
@NonNull
protected TypeSpec.Builder newClassBuilder(
@NonNull IModelDefinitionTypeInfo typeInfo,
boolean isChild) throws IOException {
// create the class
TypeSpec.Builder builder = TypeSpec.classBuilder(typeInfo.getClassName()).addModifiers(Modifier.PUBLIC);
assert builder != null;
if (isChild) {
builder.addModifiers(Modifier.STATIC);
}
ClassName baseClassName = typeInfo.getBaseClassName();
if (baseClassName != null) {
builder.superclass(baseClassName);
}
Set<IFlagContainer> additionalChildClasses;
if (typeInfo instanceof IAssemblyDefinitionTypeInfo) {
additionalChildClasses = buildClass((IAssemblyDefinitionTypeInfo) typeInfo, builder);
} else if (typeInfo instanceof IFieldDefinitionTypeInfo) {
additionalChildClasses = buildClass((IFieldDefinitionTypeInfo) typeInfo, builder);
} else {
throw new UnsupportedOperationException(
String.format("Unsupported type: %s", typeInfo.getClass().getName()));
}
ITypeResolver typeResolver = getTypeResolver();
for (IFlagContainer definition : additionalChildClasses) {
assert definition != null;
IModelDefinitionTypeInfo childTypeInfo = typeResolver.getTypeInfo(definition);
TypeSpec childClass = newClassBuilder(childTypeInfo, true).build();
builder.addType(childClass);
}
return ObjectUtils.notNull(builder);
}
/**
* Generate the contents of the class represented by the provided
* {@code builder}.
*
* @param typeInfo
* the type information for the class to build
* @param builder
* the builder to use for generating the class content
* @return the set of additional definitions for which child classes need to be
* generated
*/
protected Set<IFlagContainer> buildClass(
@NonNull IAssemblyDefinitionTypeInfo typeInfo,
@NonNull TypeSpec.Builder builder) {
Set<IFlagContainer> retval = new HashSet<>();
retval.addAll(buildClass((IModelDefinitionTypeInfo) typeInfo, builder));
AnnotationSpec.Builder metaschemaAssembly = ObjectUtils.notNull(AnnotationSpec.builder(MetaschemaAssembly.class));
buildCommonProperties(typeInfo, metaschemaAssembly);
IAssemblyDefinition definition = typeInfo.getDefinition();
if (definition.isRoot()) {
metaschemaAssembly.addMember("rootName", "$S", definition.getRootName());
}
MarkupMultiline remarks = definition.getRemarks();
if (remarks != null) {
metaschemaAssembly.addMember("remarks", "$S", remarks.toMarkdown());
}
builder.addAnnotation(metaschemaAssembly.build());
AnnotationGenerator.buildValueConstraints(builder, definition);
AnnotationGenerator.buildAssemblyConstraints(builder, definition);
return retval;
}
/**
* Generate the contents of the class represented by the provided
* {@code builder}.
*
* @param typeInfo
* the type information for the class to build
* @param builder
* the builder to use for generating the class content
* @return the set of additional definitions for which child classes need to be
* generated
*/
protected Set<IFlagContainer> buildClass(
@NonNull IFieldDefinitionTypeInfo typeInfo,
@NonNull TypeSpec.Builder builder) {
Set<IFlagContainer> retval = new HashSet<>();
retval.addAll(buildClass((IModelDefinitionTypeInfo) typeInfo, builder));
AnnotationSpec.Builder metaschemaField = ObjectUtils.notNull(AnnotationSpec.builder(MetaschemaField.class));
buildCommonProperties(typeInfo, metaschemaField);
builder.addAnnotation(metaschemaField.build());
IFieldDefinition definition = typeInfo.getDefinition();
AnnotationGenerator.buildValueConstraints(builder, definition);
return retval;
}
/**
* Generate the contents of the class represented by the provided
* {@code builder}.
*
* @param typeInfo
* the type information for the class to build
* @param builder
* the builder to use for generating the class content
* @return the set of additional definitions for which child classes need to be
* generated
*/
@NonNull
protected Set<IFlagContainer> buildClass(
@NonNull IModelDefinitionTypeInfo typeInfo,
@NonNull TypeSpec.Builder builder) {
MarkupLine description = typeInfo.getDefinition().getDescription();
if (description != null) {
builder.addJavadoc(description.toHtml());
}
Set<IFlagContainer> additionalChildClasses = new HashSet<>();
// generate a no-arg constructor
builder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).build());
// // generate a copy constructor
// MethodSpec.Builder copyBuilder =
// MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);
// copyBuilder.addParameter(className, "that", Modifier.FINAL);
// for (IPropertyGenerator property : getPropertyGenerators()) {
// additionalChildClasses.addAll(property.buildCopyStatements(copyBuilder,
// getTypeResolver()));
// }
// builder.addMethod(copyBuilder.build());
// generate all the properties and access methods
for (ITypeInfo property : typeInfo.getPropertyTypeInfos()) {
assert property != null;
additionalChildClasses.addAll(buildClass(property, builder));
}
// generate a toString method that will help with debugging
MethodSpec.Builder toString = MethodSpec.methodBuilder("toString").addModifiers(Modifier.PUBLIC)
.returns(String.class).addAnnotation(Override.class);
toString.addStatement("return new $T(this, $T.MULTI_LINE_STYLE).toString()", ReflectionToStringBuilder.class,
MultilineRecursiveToStringStyle.class);
builder.addMethod(toString.build());
return CollectionUtil.unmodifiableSet(additionalChildClasses);
}
/**
* Build the Java class data for the property.
*
* @param typeInfo
* the type information for the Java property to build
* @param builder
* the class builder
* @return the set of additional child definitions that need to be built
*/
@NonNull
protected Set<IFlagContainer> buildClass(
@NonNull ITypeInfo typeInfo,
@NonNull TypeSpec.Builder builder) {
TypeName javaFieldType = typeInfo.getJavaFieldType();
FieldSpec.Builder field = FieldSpec.builder(javaFieldType, typeInfo.getJavaFieldName())
.addModifiers(Modifier.PRIVATE);
assert field != null;
final Set<IFlagContainer> retval = buildField(typeInfo, field);
FieldSpec valueField = ObjectUtils.notNull(field.build());
builder.addField(valueField);
String propertyName = typeInfo.getPropertyName();
{
MethodSpec.Builder method = MethodSpec.methodBuilder("get" + propertyName)
.returns(javaFieldType)
.addModifiers(Modifier.PUBLIC);
assert method != null;
method.addStatement("return $N", valueField);
builder.addMethod(method.build());
}
{
ParameterSpec valueParam = ParameterSpec.builder(javaFieldType, "value").build();
MethodSpec.Builder method = MethodSpec.methodBuilder("set" + propertyName)
.addModifiers(Modifier.PUBLIC)
.addParameter(valueParam);
assert method != null;
method.addStatement("$N = $N", valueField, valueParam);
builder.addMethod(method.build());
}
if (typeInfo instanceof IModelInstanceTypeInfo) {
buildExtraMethods((IModelInstanceTypeInfo) typeInfo, builder, valueField);
}
return retval;
}
/**
* Build the core property annotations that are common to all Module classes.
*
* @param typeInfo
* the type information for the Java property to build
* @param builder
* the class builder
*/
protected void buildCommonProperties(
@NonNull IModelDefinitionTypeInfo typeInfo,
@NonNull AnnotationSpec.Builder builder) {
IDefinition definition = typeInfo.getDefinition();
String formalName = definition.getEffectiveFormalName();
if (formalName != null) {
builder.addMember("formalName", "$S", formalName);
}
MarkupLine description = definition.getEffectiveDescription();
if (description != null) {
builder.addMember("description", "$S", description.toMarkdown());
}
builder.addMember("name", "$S", definition.getName());
IModule module = definition.getContainingModule();
builder.addMember("moduleClass", "$T.class", getTypeResolver().getClassName(module));
}
/**
* Generate the Java field associated with this property.
*
* @param typeInfo
* the type information for the Java property to build
* @param builder
* the field builder
* @return the set of definitions used by this field
*/
@NonNull
protected Set<IFlagContainer> buildField(
@NonNull ITypeInfo typeInfo,
@NonNull FieldSpec.Builder builder) {
Set<IFlagContainer> retval = null;
if (typeInfo instanceof IFlagInstanceTypeInfo) {
buildFieldForFlag((IFlagInstanceTypeInfo) typeInfo, builder);
} else if (typeInfo instanceof IModelInstanceTypeInfo) {
retval = buildFieldForModelInstance((IModelInstanceTypeInfo) typeInfo, builder);
} else if (typeInfo instanceof IFieldValueTypeInfo) {
buildFieldForFieldValue((IFieldValueTypeInfo) typeInfo, builder);
}
return retval == null ? CollectionUtil.emptySet() : retval;
}
protected void buildFieldForInstance(
@NonNull IInstanceTypeInfo typeInfo,
@NonNull FieldSpec.Builder builder) {
MarkupLine description = typeInfo.getInstance().getDescription();
if (description != null) {
builder.addJavadoc("$S", description.toHtml());
}
}
protected void buildFieldForFieldValue(
@NonNull IFieldValueTypeInfo typeInfo,
@NonNull FieldSpec.Builder builder) {
IFieldDefinition definition = typeInfo.getParentDefinitionTypeInfo().getDefinition();
AnnotationSpec.Builder fieldValue = AnnotationSpec.builder(MetaschemaFieldValue.class);
IDataTypeAdapter<?> valueDataType = definition.getJavaTypeAdapter();
// a field object always has a single value
if (!definition.hasJsonValueKeyFlagInstance()) {
fieldValue.addMember("valueKeyName", "$S", definition.getJsonValueKeyName());
} // else do nothing, the annotation will be on the flag
if (!MetaschemaDataTypeProvider.DEFAULT_DATA_TYPE.equals(valueDataType)) {
fieldValue.addMember("typeAdapter", "$T.class", valueDataType.getClass());
}
Object defaultValue = definition.getDefaultValue();
if (defaultValue != null) {
fieldValue.addMember("defaultValue", "$S", valueDataType.asString(defaultValue));
}
builder.addAnnotation(fieldValue.build());
}
@SuppressWarnings("PMD.CyclomaticComplexity") // acceptable
protected void buildFieldForFlag(
@NonNull IFlagInstanceTypeInfo typeInfo,
@NonNull FieldSpec.Builder builder) {
IFlagInstance instance = typeInfo.getInstance();
AnnotationSpec.Builder annotation
= AnnotationSpec.builder(BoundFlag.class);
String formalName = instance.getEffectiveFormalName();
if (formalName != null) {
annotation.addMember("formalName", "$S", formalName);
}
MarkupLine description = instance.getEffectiveDescription();
if (description != null) {
annotation.addMember("description", "$S", description.toMarkdown());
}
annotation.addMember("useName", "$S", instance.getEffectiveName());
if (instance.isRequired()) {
annotation.addMember("required", "$L", true);
}
IFlagDefinition definition = instance.getDefinition();
IDataTypeAdapter<?> valueDataType = definition.getJavaTypeAdapter();
annotation.addMember("typeAdapter", "$T.class", valueDataType.getClass());
MarkupMultiline remarks = instance.getRemarks();
if (remarks != null) {
annotation.addMember("remarks", "$S", remarks.toMarkdown());
}
builder.addAnnotation(annotation.build());
AnnotationGenerator.buildValueConstraints(builder, definition);
IFlagContainer parent = instance.getContainingDefinition();
if (parent.hasJsonKey() && instance.equals(parent.getJsonKeyFlagInstance())) {
builder.addAnnotation(JsonKey.class);
}
if (parent instanceof IFieldDefinition) {
IFieldDefinition parentField = (IFieldDefinition) parent;
if (parentField.hasJsonValueKeyFlagInstance() && instance.equals(parentField.getJsonValueKeyFlagInstance())) {
builder.addAnnotation(JsonFieldValueKeyFlag.class);
}
}
}
@SuppressWarnings("PMD.NPathComplexity")
@NonNull
protected AnnotationSpec.Builder generateBindingAnnotation(
@NonNull IModelInstanceTypeInfo typeInfo) {
// determine which annotation to apply
AnnotationSpec.Builder retval;
INamedModelInstance modelInstance = typeInfo.getInstance();
if (modelInstance instanceof IFieldInstance) {
retval = AnnotationSpec.builder(BoundField.class);
} else if (modelInstance instanceof IAssemblyInstance) {
retval = AnnotationSpec.builder(BoundAssembly.class);
} else {
throw new UnsupportedOperationException(
String.format("ModelContainer instance '%s' of type '%s' is not supported.",
modelInstance.getName(), modelInstance.getClass().getName()));
}
String formalName = modelInstance.getEffectiveFormalName();
if (formalName != null) {
retval.addMember("formalName", "$S", formalName);
}
MarkupLine description = modelInstance.getEffectiveDescription();
if (description != null) {
retval.addMember("description", "$S", description.toMarkdown());
}
retval.addMember("useName", "$S", modelInstance.getEffectiveName());
String namespace = modelInstance.getXmlNamespace();
if (namespace == null) {
retval.addMember("namespace", "$S", "##none");
} else if (!modelInstance.getContainingModule().getXmlNamespace().toASCIIString().equals(namespace)) {
retval.addMember("namespace", "$S", namespace);
} // otherwise use the ##default
int minOccurs = modelInstance.getMinOccurs();
if (minOccurs != MetaschemaModelConstants.DEFAULT_GROUP_AS_MIN_OCCURS) {
retval.addMember("minOccurs", "$L", minOccurs);
}
int maxOccurs = modelInstance.getMaxOccurs();
if (maxOccurs != MetaschemaModelConstants.DEFAULT_GROUP_AS_MAX_OCCURS) {
retval.addMember("maxOccurs", "$L", maxOccurs);
}
MarkupMultiline remarks = modelInstance.getRemarks();
if (remarks != null) {
retval.addMember("remarks", "$S", remarks.toMarkdown());
}
if (modelInstance instanceof IFieldInstance) {
IFieldInstance fieldInstance = (IFieldInstance) modelInstance;
if (MetaschemaModelConstants.DEFAULT_FIELD_IN_XML_WRAPPED != fieldInstance.isInXmlWrapped()) {
retval.addMember("inXmlWrapped", "$L", fieldInstance.isInXmlWrapped());
}
}
return retval;
}
@NonNull
protected AnnotationSpec.Builder generateGroupAsAnnotation(
@NonNull IModelInstanceTypeInfo typeInfo) {
AnnotationSpec.Builder groupAsAnnoation = AnnotationSpec.builder(GroupAs.class);
INamedModelInstance modelInstance = typeInfo.getInstance();
groupAsAnnoation.addMember("name", "$S",
ObjectUtils.requireNonNull(modelInstance.getGroupAsName(), "The grouping name must be non-null"));
String groupAsNamespace = modelInstance.getGroupAsXmlNamespace();
if (groupAsNamespace == null) {
groupAsAnnoation.addMember("namespace", "$S", "##none");
} else if (!modelInstance.getContainingModule().getXmlNamespace().toASCIIString().equals(groupAsNamespace)) {
groupAsAnnoation.addMember("namespace", "$S", groupAsNamespace);
} // otherwise use the ##default
JsonGroupAsBehavior jsonGroupAsBehavior = modelInstance.getJsonGroupAsBehavior();
assert jsonGroupAsBehavior != null;
if (!MetaschemaModelConstants.DEFAULT_JSON_GROUP_AS_BEHAVIOR.equals(jsonGroupAsBehavior)) {
groupAsAnnoation.addMember("inJson", "$T.$L",
JsonGroupAsBehavior.class, jsonGroupAsBehavior.toString());
}
XmlGroupAsBehavior xmlGroupAsBehavior = modelInstance.getXmlGroupAsBehavior();
assert xmlGroupAsBehavior != null;
if (!MetaschemaModelConstants.DEFAULT_XML_GROUP_AS_BEHAVIOR.equals(xmlGroupAsBehavior)) {
groupAsAnnoation.addMember("inXml", "$T.$L",
XmlGroupAsBehavior.class, xmlGroupAsBehavior.toString());
}
return groupAsAnnoation;
}
@SuppressWarnings("PMD.CognitiveComplexity")
public Set<IFlagContainer> buildFieldForModelInstance(
@NonNull IModelInstanceTypeInfo typeInfo,
@NonNull FieldSpec.Builder builder) { // NOPMD - intentional
buildFieldForInstance(typeInfo, builder);
builder.addAnnotation(generateBindingAnnotation(typeInfo).build());
INamedModelInstance modelInstance = typeInfo.getInstance();
IFlagContainer definition = modelInstance.getDefinition();
if (modelInstance instanceof IFieldInstance) {
// handle the field value related info
IFieldDefinition fieldDefinition = (IFieldDefinition) definition;
if (fieldDefinition.isSimple()) {
// this is a simple field, without flags
// we need to add the BoundFieldValue annotation to the property
// fieldAnnoation.addMember("valueName", "$S",
// fieldDefinition.getJsonValueKeyName());
IDataTypeAdapter<?> valueDataType = fieldDefinition.getJavaTypeAdapter();
Object defaultValue = fieldDefinition.getDefaultValue();
if (!MetaschemaDataTypeProvider.DEFAULT_DATA_TYPE.equals(valueDataType) || defaultValue != null) {
AnnotationSpec.Builder boundFieldValueAnnotation = AnnotationSpec.builder(BoundFieldValue.class);
if (!MetaschemaDataTypeProvider.DEFAULT_DATA_TYPE.equals(valueDataType)) {
boundFieldValueAnnotation.addMember("typeAdapter", "$T.class", valueDataType.getClass());
}
if (defaultValue != null) {
boundFieldValueAnnotation.addMember("defaultValue", "$S", valueDataType.asString(defaultValue));
}
builder.addAnnotation(boundFieldValueAnnotation.build());
}
AnnotationGenerator.buildValueConstraints(builder, fieldDefinition);
}
}
int maxOccurs = modelInstance.getMaxOccurs();
if (maxOccurs == -1 || maxOccurs > 1) {
// requires a group-as
builder.addAnnotation(generateGroupAsAnnotation(typeInfo).build());
}
Set<IFlagContainer> retval = new HashSet<>();
if (definition.isInline() && !(definition instanceof IFieldDefinition && definition.isSimple())) {
// this is an inline definition that must be built as a child class
retval.add(definition);
}
return retval.isEmpty() ? CollectionUtil.emptySet() : CollectionUtil.unmodifiableSet(retval);
}
/**
* This method can be implemented by subclasses to create additional methods.
*
* @param typeInfo
* the type information for the Java property to build
* @param builder
* the class builder
* @param valueField
* the field corresponding to this property
*/
@SuppressWarnings("PMD.LooseCoupling") // need implementation classes
protected void buildExtraMethods( // NOPMD - intentional
@NonNull IModelInstanceTypeInfo typeInfo,
@NonNull TypeSpec.Builder builder,
@NonNull FieldSpec valueField) {
INamedModelInstance instance = typeInfo.getInstance();
int maxOccurance = instance.getMaxOccurs();
if (maxOccurance == -1 || maxOccurance > 1) {
TypeName itemType = typeInfo.getJavaItemType();
ParameterSpec valueParam = ParameterSpec.builder(itemType, "item").build();
String itemPropertyName = ClassUtils.toPropertyName(typeInfo.getItemBaseName());
if (JsonGroupAsBehavior.KEYED.equals(instance.getJsonGroupAsBehavior())) {
IFlagInstance jsonKey = instance.getDefinition().getJsonKeyFlagInstance();
if (jsonKey == null) {
throw new IllegalStateException(
String.format("JSON key not defined for property: %s", instance.toCoordinates()));
}
// get the json key property on the instance's definition
IModelDefinitionTypeInfo instanceTypeInfo = typeResolver.getTypeInfo(instance.getDefinition());
IFlagInstanceTypeInfo jsonKeyTypeInfo = instanceTypeInfo.getFlagInstanceTypeInfo(jsonKey);
if (jsonKeyTypeInfo == null) {
throw new IllegalStateException(
String.format("Unable to identify JSON key for property: %s", instance.toCoordinates()));
}
{
// create add method
MethodSpec.Builder method = MethodSpec.methodBuilder("add" + itemPropertyName)
.addParameter(valueParam)
.returns(itemType)
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Add a new {@link $T} item to the underlying collection.\n", itemType)
.addJavadoc("@param item the item to add\n")
.addJavadoc("@return the existing {@link $T} item in the collection or {@code null} if not item exists\n",
itemType)
.addStatement("$1T value = $2T.requireNonNull($3N,\"$3N value cannot be null\")",
itemType, ObjectUtils.class, valueParam)
.addStatement("$1T key = $2T.requireNonNull($3N.$4N(),\"$3N key cannot be null\")",
String.class, ObjectUtils.class, valueParam, "get" + jsonKeyTypeInfo.getPropertyName())
.beginControlFlow("if ($N == null)", valueField)
.addStatement("$N = new $T<>()", valueField, LinkedHashMap.class)
.endControlFlow()
.addStatement("return $N.put(key, value)", valueField);
builder.addMethod(method.build());
}
{
// create remove method
MethodSpec.Builder method = MethodSpec.methodBuilder("remove" + itemPropertyName)
.addParameter(valueParam)
.returns(TypeName.BOOLEAN)
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Remove the {@link $T} item from the underlying collection.\n", itemType)
.addJavadoc("@param item the item to remove\n")
.addJavadoc("@return {@code true} if the item was removed or {@code false} otherwise\n")
.addStatement("$1T value = $2T.requireNonNull($3N,\"$3N value cannot be null\")",
itemType, ObjectUtils.class, valueParam)
.addStatement("$1T key = $2T.requireNonNull($3N.$4N(),\"$3N key cannot be null\")",
String.class, ObjectUtils.class, valueParam, "get" + jsonKeyTypeInfo.getPropertyName())
.addStatement("return $1N == null ? false : $1N.remove(key, value)", valueField);
builder.addMethod(method.build());
}
} else {
{
// create add method
MethodSpec.Builder method = MethodSpec.methodBuilder("add" + itemPropertyName)
.addParameter(valueParam)
.returns(TypeName.BOOLEAN)
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Add a new {@link $T} item to the underlying collection.\n", itemType)
.addJavadoc("@param item the item to add\n")
.addJavadoc("@return {@code true}\n")
.addStatement("$T value = $T.requireNonNull($N,\"$N cannot be null\")",
itemType, ObjectUtils.class, valueParam, valueParam)
.beginControlFlow("if ($N == null)", valueField)
.addStatement("$N = new $T<>()", valueField, LinkedList.class)
.endControlFlow()
.addStatement("return $N.add(value)", valueField);
builder.addMethod(method.build());
}
{
// create remove method
MethodSpec.Builder method = MethodSpec.methodBuilder("remove" + itemPropertyName)
.addParameter(valueParam)
.returns(TypeName.BOOLEAN)
.addModifiers(Modifier.PUBLIC)
.addJavadoc("Remove the first matching {@link $T} item from the underlying collection.\n", itemType)
.addJavadoc("@param item the item to remove\n")
.addJavadoc("@return {@code true} if the item was removed or {@code false} otherwise\n")
.addStatement("$T value = $T.requireNonNull($N,\"$N cannot be null\")",
itemType, ObjectUtils.class, valueParam, valueParam)
.addStatement("return $1N == null ? false : $1N.remove(value)", valueField);
builder.addMethod(method.build());
}
}
}
}
}