DefaultMetaschemaClassFactory.java
/*
* SPDX-FileCopyrightText: none
* SPDX-License-Identifier: CC0-1.0
*/
package gov.nist.secauto.metaschema.databind.codegen.typeinfo;
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.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.WildcardTypeName;
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.IBoundObject;
import gov.nist.secauto.metaschema.core.model.IDefinition;
import gov.nist.secauto.metaschema.core.model.IFieldDefinition;
import gov.nist.secauto.metaschema.core.model.IMetaschemaData;
import gov.nist.secauto.metaschema.core.model.IModelDefinition;
import gov.nist.secauto.metaschema.core.model.IModule;
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.IGeneratedClass;
import gov.nist.secauto.metaschema.databind.codegen.IGeneratedDefinitionClass;
import gov.nist.secauto.metaschema.databind.codegen.IGeneratedModuleClass;
import gov.nist.secauto.metaschema.databind.codegen.impl.AnnotationGenerator;
import gov.nist.secauto.metaschema.databind.codegen.impl.DefaultGeneratedClass;
import gov.nist.secauto.metaschema.databind.codegen.impl.DefaultGeneratedDefinitionClass;
import gov.nist.secauto.metaschema.databind.codegen.impl.DefaultGeneratedModuleClass;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IAssemblyDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IFieldDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.codegen.typeinfo.def.IModelDefinitionTypeInfo;
import gov.nist.secauto.metaschema.databind.model.AbstractBoundModule;
import gov.nist.secauto.metaschema.databind.model.IBoundModule;
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.MetaschemaModule;
import gov.nist.secauto.metaschema.databind.model.annotations.MetaschemaPackage;
import gov.nist.secauto.metaschema.databind.model.annotations.NsBinding;
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.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
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.LinkedHashSet;
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
})
public 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 IModelDefinition> globalDefinitions = Stream.concat(
module.getAssemblyDefinitions().stream(),
module.getFieldDefinitions().stream());
Set<String> classNames = new LinkedHashSet<>();
@SuppressWarnings("PMD.UseConcurrentHashMap") // map is unmodifiable
Map<IModelDefinition, 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
&& !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 -> {
IModelDefinition 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 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)
.addModifiers(Modifier.FINAL);
builder.superclass(AbstractBoundModule.class);
builder.addAnnotation(buildModuleAnnotation(module).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(IBoundModule.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);
}
// builder.addModifiers(Modifier.FINAL);
builder.addSuperinterface(ClassName.get(IBoundObject.class));
// add field for Metaschema info
builder.addField(FieldSpec.builder(IMetaschemaData.class, "__metaschemaData", Modifier.PRIVATE, Modifier.FINAL)
.build());
builder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addStatement("this(null)")
.build());
builder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(IMetaschemaData.class, "data")
.addStatement("this.$N = $N", "__metaschemaData", "data")
.build());
// generate a toString method that will help with debugging
MethodSpec.Builder getMetaschemaData = MethodSpec.methodBuilder("getMetaschemaData")
.addModifiers(Modifier.PUBLIC)
.returns(IMetaschemaData.class)
.addAnnotation(Override.class)
.addStatement("return __metaschemaData");
builder.addMethod(getMetaschemaData.build());
ClassName baseClassName = typeInfo.getBaseClassName();
if (baseClassName != null) {
builder.superclass(baseClassName);
}
for (ClassName superinterface : typeInfo.getSuperinterfaces()) {
builder.addSuperinterface(superinterface);
}
Set<IModelDefinition> 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 (IModelDefinition definition : additionalChildClasses) {
assert definition != null;
IModelDefinitionTypeInfo childTypeInfo = typeResolver.getTypeInfo(definition);
TypeSpec childClass = newClassBuilder(childTypeInfo, true).build();
builder.addType(childClass);
}
return ObjectUtils.notNull(builder);
}
private AnnotationSpec.Builder buildModuleAnnotation(@NonNull IModule module) {
AnnotationSpec.Builder retval = AnnotationSpec.builder(MetaschemaModule.class);
ITypeResolver typeResolver = getTypeResolver();
for (IFieldDefinition definition : module.getFieldDefinitions()) {
if (definition.hasChildren()) {
retval.addMember("fields", "$T.class", typeResolver.getClassName(definition));
}
}
for (IAssemblyDefinition definition : module.getAssemblyDefinitions()) {
retval.addMember(
"assemblies",
"$T.class",
typeResolver.getClassName(ObjectUtils.notNull(definition)));
}
for (IModule moduleImport : module.getImportedModules()) {
retval.addMember(
"imports",
"$T.class",
typeResolver.getClassName(ObjectUtils.notNull(moduleImport)));
}
Map<String, String> bindings = module.getNamespaceBindings();
if (!bindings.isEmpty()) {
for (Map.Entry<String, String> binding : bindings.entrySet()) {
retval.addMember(
"nsBindings",
"$L",
AnnotationSpec.builder(NsBinding.class)
.addMember("prefix", "$S", binding.getKey())
.addMember("uri", "$S", binding.getValue())
.build());
}
}
MarkupMultiline remarks = module.getRemarks();
if (remarks != null) {
retval.addMember("remarks", "$S", remarks.toMarkdown());
}
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<IModelDefinition> buildClass(
@NonNull IAssemblyDefinitionTypeInfo typeInfo,
@NonNull TypeSpec.Builder 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());
}
AnnotationGenerator.buildValueConstraints(metaschemaAssembly, definition);
AnnotationGenerator.buildAssemblyConstraints(metaschemaAssembly, definition);
builder.addAnnotation(metaschemaAssembly.build());
return new LinkedHashSet<>(buildClass((IModelDefinitionTypeInfo) typeInfo, 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<IModelDefinition> buildClass(
@NonNull IFieldDefinitionTypeInfo typeInfo,
@NonNull TypeSpec.Builder builder) {
AnnotationSpec.Builder metaschemaField = ObjectUtils.notNull(AnnotationSpec.builder(MetaschemaField.class));
buildCommonProperties(typeInfo, metaschemaField);
IFieldDefinition definition = typeInfo.getDefinition();
AnnotationGenerator.buildValueConstraints(metaschemaField, definition);
builder.addAnnotation(metaschemaField.build());
return new LinkedHashSet<>(buildClass((IModelDefinitionTypeInfo) typeInfo, 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
*/
@NonNull
protected Set<IModelDefinition> buildClass(
@NonNull IModelDefinitionTypeInfo typeInfo,
@NonNull TypeSpec.Builder builder) {
MarkupLine description = typeInfo.getDefinition().getDescription();
if (description != null) {
builder.addJavadoc(description.toHtml());
}
Set<IModelDefinition> additionalChildClasses = new LinkedHashSet<>();
// // 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 (IPropertyTypeInfo property : typeInfo.getPropertyTypeInfos()) {
assert property != null;
additionalChildClasses.addAll(property.build(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,
ToStringStyle.class);
builder.addMethod(toString.build());
return CollectionUtil.unmodifiableSet(additionalChildClasses);
}
/**
* 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));
}
}