pro.projo.generation.dtd.DtdElementCollector Maven / Gradle / Ivy
The newest version!
// //
// Copyright 2022 - 2023 Mirko Raner //
// //
// Licensed under the Apache License, Version 2.0 (the "License"); //
// you may not use this file except in compliance with the License. //
// You may obtain a copy of the License at //
// //
// http://www.apache.org/licenses/LICENSE-2.0 //
// //
// Unless required by applicable law or agreed to in writing, software //
// distributed under the License is distributed on an "AS IS" BASIS, //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and //
// limitations under the License. //
// //
package pro.projo.generation.dtd;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.text.Format;
import java.text.MessageFormat;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import com.sun.xml.dtdparser.DTDParser;
import pro.projo.generation.dtd.model.Attribute;
import pro.projo.generation.dtd.model.AttributeUse;
import pro.projo.generation.dtd.model.ChildElement;
import pro.projo.generation.dtd.model.ContentModel;
import pro.projo.generation.dtd.model.ContentModelType;
import pro.projo.generation.dtd.model.DtdElement;
import pro.projo.generation.dtd.model.ModelGroup;
import pro.projo.generation.interfaces.InterfaceTemplateProcessor;
import pro.projo.generation.utilities.DefaultConfiguration;
import pro.projo.generation.utilities.DefaultNameComparator;
import pro.projo.generation.utilities.Reduction;
import pro.projo.generation.utilities.TypeMirrorUtilities;
import pro.projo.interfaces.annotation.Alias;
import pro.projo.interfaces.annotation.Dtd;
import pro.projo.interfaces.annotation.Options;
import pro.projo.interfaces.annotation.utilities.AttributeNameConverter;
import pro.projo.template.annotation.Configuration;
import static java.lang.String.join;
import static java.util.Arrays.asList;
import static java.util.Collections.emptyList;
import static java.util.stream.IntStream.range;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static javax.tools.Diagnostic.Kind.ERROR;
import static pro.projo.interfaces.annotation.Ternary.FALSE;
/**
* The {@link DtdElementCollector} is a type that collects information from
* DTD parsing events and builds template {@link Configuration}s based on the
* collected data.
*
* @author Mirko Raner
**/
public class DtdElementCollector implements TypeMirrorUtilities
{
private final Comparator importOrder = new DefaultNameComparator();
private final Name packageName;
private final Name generated;
private final AttributeNameConverter attributeNameConverter;
private final TypeElement baseInterface;
private final TypeElement baseInterfaceEmpty;
private final TypeElement baseInterfaceText;
private final TypeElement mixedContentInterface;
private final String baseVariables;
private final String baseVariablesEmpty;
private final String baseVariablesText;
private final String mixedContentVariables;
private final int implicitTypeParameters;
private final Map> aliases;
private final Map> attributes;
private final Options options;
private final boolean addAnnotations;
private final Format elementTypeName;
private final Format contentTypeName;
private final Elements elements;
private final Messager messager;
public DtdElementCollector(Name packageName, Dtd dtd, Elements elements, Messager messager)
{
this.packageName = packageName;
this.elements = elements;
this.messager = messager;
baseInterface = getTypeElement(dtd::baseInterface);
baseInterfaceEmpty = getTypeElement(dtd::baseInterfaceEmpty);
baseInterfaceText = getTypeElement(dtd::baseInterfaceText);
mixedContentInterface = getTypeElement(dtd::mixedContentInterface);
implicitTypeParameters = dtd.implicitTypeParameters();
baseVariables = additionalTypeVariables(baseInterface, implicitTypeParameters+3);
baseVariablesEmpty = additionalTypeVariables(baseInterfaceEmpty, implicitTypeParameters+1);
baseVariablesText = additionalTypeVariables(baseInterfaceText, implicitTypeParameters+1);
mixedContentVariables = additionalTypeVariables(mixedContentInterface, implicitTypeParameters+1);
elementTypeName = new MessageFormat(dtd.elementNameFormat());
contentTypeName = new MessageFormat(dtd.contentNameFormat());
aliases = Stream.of(dtd.aliases()).map(Alias::value).collect(toMap(key -> key[0], value -> asList(value)));
attributes = Stream.of(dtd.attributes()).collect(toMap(key -> key.name(),
value -> new SimpleEntry<>(
getTypeElement(value::type),
value.typeArguments().length == 0? "":"<" + join(", ", value.typeArguments()) + ">")));
options = dtd.options();
addAnnotations = options.addAnnotations() != FALSE;
generated = new pro.projo.generation.utilities.Name("javax.annotation.Generated");
AttributeNameConverter attributeNameConverter = null;
try
{
Constructor> constructor = getType(dtd::attributeNameConverter).getDeclaredConstructor();
attributeNameConverter = (AttributeNameConverter)constructor.newInstance();
}
catch (Exception exception)
{
this.messager.printMessage(ERROR, "Could not instantiate attribute name converter");
}
this.attributeNameConverter = attributeNameConverter;
}
@Override
public Elements elements()
{
return elements;
}
public Stream configurations(InputSource source) throws IOException, SAXException
{
DtdModelBuilder builder = new DtdModelBuilder();
DTDParser parser = new DTDParser();
parser.setDtdHandler(builder);
parser.parse(source);
Stream contentModels = builder.getDtd().contentModels();
return contentModels.flatMap(this::createElementAndContent);
}
public Stream createElementAndContent(ContentModel contentModel)
{
Stream contentConfiguration = createContentTypes(contentModel);
return Stream.concat(createElementInterface(contentModel), contentConfiguration);
}
public Stream createElementInterface(ContentModel contentModel)
{
String elementName = contentModel.name();
List requiredAttributes = contentModel.attributes()
.filter(attribute -> attribute.use() == AttributeUse.REQUIRED)
.collect(toList());
boolean currentContentModelHasChildren = contentModel.nonAttributes().count() > 0;
TypeElement superType = contentModel.type() == ContentModelType.EMPTY? baseInterfaceEmpty:
currentContentModelHasChildren? baseInterface:baseInterfaceText;
String extraVariables = contentModel.type() == ContentModelType.EMPTY? baseVariablesEmpty:
currentContentModelHasChildren? baseVariables:baseVariablesText;
boolean isObject = superType.getQualifiedName().toString().equals(Object.class.getName());
String extend = isObject? "":(" extends " + superType.getSimpleName());
String contentType = contentTypeName.format(new Object[] {typeName(elementName)});
if (!extraVariables.isEmpty())
{
contentType += "<" + extraVariables.substring(2) + ">";
}
String typeParameters = "":extraVariables + ">");
boolean superTypeSamePackage = packageName(superType).equals(packageName);
Name superTypeName = superType.getQualifiedName();
if (requiredAttributes.isEmpty())
{
Stream generated = addAnnotations? Stream.of(this.generated):Stream.empty();
Stream imported = superTypeSamePackage? generated:Stream.concat(generated, Stream.of(superTypeName));
String typeName = elementTypeName.format(new Object[] {typeName(elementName)});
Configuration configuration = elementConfiguration(typeName, imported, extend + (isObject? "":typeParameters), extraVariables);
configuration = contentModel.attributes().reduce(configuration, (conf, attr) -> attributeDecl(conf, contentModel, attr, extraVariables), (a, b) -> a);
return Stream.of(configuration);
}
else
{
// For required attributes, multiple element interfaces need to be created
// (specifically, 2^n interfaces, where n is the number of required attributes)
//
Stream> combinations = powerList(requiredAttributes);
List optionalAttributes = contentModel.attributes()
.filter(attribute -> attribute.use() != AttributeUse.REQUIRED)
.collect(toList());
return combinations.map(attributes ->
{
String presentAttributes = attributes.stream().map(Attribute::name).map(this::typeName).collect(joining());
List missingRequired = new ArrayList<>(requiredAttributes);
missingRequired.removeAll(attributes);
boolean attributesComplete = attributes.size() == requiredAttributes.size();
String typeName = elementTypeName.format(new Object[] {typeName(elementName) + presentAttributes});
String extension = attributesComplete? extend + (isObject? "":typeParameters):"";
Stream generated = addAnnotations? Stream.of(this.generated):Stream.empty();
Stream imports = !attributesComplete || superTypeSamePackage? generated:Stream.concat(generated, Stream.of(superTypeName));
Configuration configuration = elementConfiguration(typeName, imports, extension, extraVariables);
ContentModel returnTypeContentModel = contentModel(typeName(elementName) + presentAttributes);
return missingRequired.stream().reduce
(
optionalAttributes.stream().reduce
(
configuration,
(conf, attr) -> attributeDecl(conf, returnTypeContentModel, attr, extraVariables),
(a, b) -> a
),
(conf, attr) -> attributeDecl
(
conf,
missingAttributeReturnType(elementName, requiredAttributes, attributes, attr),
attr,
extraVariables
),
(a, b) -> a
);
});
}
}
public ContentModel missingAttributeReturnType(String elementName, List all, List present, Attribute attribute)
{
// Unfortunately, just appending the new attribute to the list of already present attributes
// will not produce the correct behavior because the attributes will possibly end up out of
// order. To retain the original order, the attribute list is generated by starting with the
// entire list of required attributes and then retaining the pre-existing attributes and the
// additional new attribute. This guarantees the correct order of attributes.
//
List attributes = new ArrayList<>(all);
List retain = new ArrayList<>(present);
retain.add(attribute);
attributes.retainAll(retain);
String name = typeName(elementName) + attributes.stream().map(Attribute::name).map(this::typeName).collect(joining());
return contentModel(name);
}
public ContentModel contentModel(String name)
{
return new ContentModel()
{
@Override
public String name()
{
return name;
}
@Override
public ContentModelType type()
{
return null;
}
};
}
public Configuration elementConfiguration(String typeName, Stream imported, String extendSpec, String extraVariables)
{
String[] imports = imported
.sorted(importOrder)
.map(Name::toString)
.filter(name -> !name.startsWith("java.lang."))
.toArray(String[]::new);
// TODO: consolidate import code with code in InterfaceTemplateProcessor.getInterfaceConfiguration
Map parameters = new HashMap<>();
parameters.put("package", packageName.toString());
parameters.put("imports", imports);
parameters.put("javadoc", "THIS IS A GENERATED INTERFACE - DO NOT EDIT!");
parameters.put("generatedBy", addAnnotations? "@Generated(\"" + InterfaceTemplateProcessor.class.getName() + "\")":"");
parameters.put("InterfaceTemplate", typeName + "" + extendSpec);
parameters.put("methods", new String[] {});
String fullyQualifiedClassName = packageName.toString() + "." + typeName;
return new DefaultConfiguration(fullyQualifiedClassName, parameters, options);
}
public Configuration attributeDecl(Configuration configuration, ContentModel contentModel, Attribute attribute, String extraVariables)
{
String attributeName = attribute.name();
String elementName = contentModel.name();
// TODO: next line duplicated from above
String typeName = elementTypeName.format(new Object[] {typeName(elementName)});
String methodName = attributeNameConverter.convertAttributeName(attributeName);
Entry parameterTypeAndArguments = attributes.get(methodName);
String parameterTypeName = parameterTypeAndArguments != null? parameterTypeAndArguments.getKey().getSimpleName().toString():"String";
String parameterTypeArguments = parameterTypeAndArguments != null? parameterTypeAndArguments.getValue():"";
String method = typeName + " " + methodName + "(" + parameterTypeName + parameterTypeArguments + " " + methodName + ")";
String[] methods = (String[])configuration.parameters().get("methods");
List newMethods = new ArrayList<>(Arrays.asList(methods));
newMethods.add(method);
configuration.parameters().put("methods", newMethods.toArray(new String[] {}));
if (parameterTypeAndArguments != null && !packageName(parameterTypeAndArguments.getKey()).equals(packageName))
{
String[] imported = (String[])configuration.parameters().get("imports");
Stream stream = Stream.of(imported != null? imported:new String[] {});
Set imports = new HashSet<>(stream.collect(toList()));
imports.add(parameterTypeAndArguments.getKey().getQualifiedName().toString());
// TODO: this will sort the entire array after each new import
// TODO: consolidate with code in elementConfiguration(...)
configuration.parameters().put("imports",
imports.stream()
.map(pro.projo.generation.utilities.Name::new)
.sorted(importOrder)
.map(Name::toString)
.toArray(String[]::new));
}
return configuration;
}
public Configuration childElement(Configuration configuration, TypedChildElement childElement, String contentTypeArgument)
{
Map parameters = configuration.parameters();
String contentType = (String)parameters.get("InterfaceTemplate");
@SuppressWarnings("unchecked")
Set methodNames = (Set)parameters.getOrDefault("methodNames", new HashSet<>());
// TODO: next line duplicated from above
String methodName = childElement.name();
if (!methodNames.contains(methodName))
{
String typeName = elementTypeName.format(new Object[] {typeName(childElement.typeName())});
String method = typeName + "<" + (contentTypeArgument != null? contentTypeArgument:contentType) + "> " + methodName + "()";
// TODO: next four lines also duplicated
String[] methods = (String[])parameters.get("methods");
List newMethods = new ArrayList<>(Arrays.asList(methods));
newMethods.add(method);
parameters.put("methods", newMethods.toArray(new String[] {}));
methodNames.add(methodName);
parameters.put("methodNames", methodNames);
}
return configuration;
}
private Stream createContentTypes(ContentModel contentModel)
{
String contentType = contentTypeName.format(new Object[] {typeName(contentModel.name())});
String additionalTypeVariables = mixedContentVariables.isEmpty()? "":"<" + mixedContentVariables.substring(2) + ">";
Stream children = contentModel.nonAttributes()
.flatMap(it -> Stream.concat(Stream.of(it), it.children().stream()))
.filter(ChildElement.class::isInstance)
.map(ChildElement.class::cast);
List childList = children.collect(toList());
DtdElement modelGroup;
if (contentModel.nonAttributes().count() == 1
&& (modelGroup = contentModel.nonAttributes().iterator().next()) instanceof ModelGroup
&& ((ModelGroup)modelGroup).isStrictSequence())
{
// To implement a strict sequence:
//
// - generate a content base interface (e.g., HtmlContent) without any methods
// - generate a content interface (n=0...) with the nth (0th, 1st, ...) method
// - each interface's content method parameter points to the next interface
// - the last interface's content method parameter points to the base interface
// - the element interface uses content<0> as in type and the base interface as out type
//
Stream base = Stream.of(createContentType(contentType, null, Stream.empty(), "", emptyList(), mixedContentVariables));
Stream sequence = IntStream.range(0, childList.size()).mapToObj
(
index -> createContentType
(
contentType + typeName(childList.get(index).name()),
contentType + (index+1 < childList.size()? typeName(childList.get(index+1).name()):""),
Stream.of(childList.get(index)),
additionalTypeVariables + " extends " + contentType + additionalTypeVariables,
emptyList(),
mixedContentVariables
)
);
return Stream.concat(base, sequence);
}
else if (contentModel.type() != ContentModelType.EMPTY
&& (contentModel.nonAttributes().count() > 0))
{
String extend = "";
String[] imports = {};
if (contentModel.type() == ContentModelType.MIXED
&& !mixedContentInterface.getQualifiedName().toString().equals(Object.class.getName()))
{
extend = additionalTypeVariables + " extends " + mixedContentInterface.getSimpleName() + "<" + contentType + additionalTypeVariables + mixedContentVariables + ">";
String mixedContentTypeName = mixedContentInterface.getQualifiedName().toString();
String mixedContentPackage = mixedContentTypeName.substring(0, mixedContentTypeName.lastIndexOf('.'));
if (!mixedContentPackage.equals(packageName.toString()))
{
imports = new String[] {mixedContentTypeName};
}
}
return Stream.of(createContentType(contentType, null, childList.stream(), extend, asList(imports), mixedContentVariables));
}
return Stream.empty();
}
private Configuration createContentType(String contentType, String contentTypeArgument,
Stream children, String extend, List imports, String extraTypeVariables)
{
List importStatements = new ArrayList<>();
if (addAnnotations)
{
importStatements.add(generated.toString());
}
importStatements.addAll(imports);
if (!extraTypeVariables.isEmpty())
{
contentTypeArgument = (contentTypeArgument != null? contentTypeArgument:contentType) + "<" + extraTypeVariables.substring(2) + ">";
}
if (extend.isEmpty() && !extraTypeVariables.isEmpty())
{
extend += "<" + extraTypeVariables.substring(2) + ">";
}
Map parameters = new HashMap<>();
// TODO: consolidate import code with code in InterfaceTemplateProcessor.getInterfaceConfiguration
parameters.put("package", packageName.toString());
parameters.put("imports", importStatements.toArray(new String[] {}));
parameters.put("javadoc", "THIS IS A GENERATED INTERFACE - DO NOT EDIT!");
parameters.put("generatedBy", addAnnotations? "@Generated(\"" + InterfaceTemplateProcessor.class.getName() + "\")":"");
parameters.put("InterfaceTemplate", contentType + extend);
parameters.put("methods", new String[] {});
String fullyQualifiedClassName = packageName.toString() + "." + contentType;
Function> aliased = name -> aliases.getOrDefault(name, asList(name)).stream();
Stream typedChildren = children.flatMap
(
child -> aliased.apply(child.name()).map(it -> new TypedChildElement(child, it))
);
Configuration configuration = new DefaultConfiguration(fullyQualifiedClassName, parameters, options);
String typeArgument = (contentTypeArgument != null? contentTypeArgument:contentType) + extraTypeVariables;
Reduction reducer = (conf, child) -> childElement(conf, child, typeArgument);
return typedChildren.reduce(configuration, reducer, (a, b) -> a);
}
/**
* This method produces a power set, but uses lists to preserve the order.
**/
private Stream> powerList(List list)
{
return range(0, 1 << list.size())
.mapToObj(pattern -> range(0, list.size()).filter(bit -> (pattern & (1 << bit)) != 0).toArray())
.map(indexArray -> IntStream.of(indexArray).mapToObj(list::get).collect(toList()));
}
private String additionalTypeVariables(TypeElement element, int baseCount)
{
return element
.getTypeParameters()
.stream()
.skip(baseCount)
.map(it -> it.getSimpleName().toString())
.collect(joining(", ", element.getTypeParameters().size() > baseCount? ", ":"", ""));
}
private String typeName(String elementName)
{
String firstLetter = elementName.substring(0, 1);
String remainingLetters = elementName.substring(1);
return firstLetter.toUpperCase() + remainingLetters;
}
private Name packageName(TypeElement element)
{
String name = element.getQualifiedName().toString();
int lastDot = name.lastIndexOf('.');
if (lastDot == -1)
{
return new pro.projo.generation.utilities.Name("");
}
return new pro.projo.generation.utilities.Name(name.substring(0, lastDot));
}
}