/**
* Copyright © 2006-2016 Web Cohesion (info@webcohesion.com)
*
* 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 com.webcohesion.enunciate.modules.jackson;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.*;
import com.webcohesion.enunciate.EnunciateContext;
import com.webcohesion.enunciate.EnunciateException;
import com.webcohesion.enunciate.javac.decorations.type.DecoratedDeclaredType;
import com.webcohesion.enunciate.javac.decorations.type.DecoratedTypeMirror;
import com.webcohesion.enunciate.metadata.json.JsonSeeAlso;
import com.webcohesion.enunciate.metadata.qname.XmlQNameEnum;
import com.webcohesion.enunciate.module.EnunciateModuleContext;
import com.webcohesion.enunciate.modules.jackson.javac.InterfaceJacksonDeclaredType;
import com.webcohesion.enunciate.modules.jackson.javac.ParameterizedJacksonDeclaredType;
import com.webcohesion.enunciate.modules.jackson.model.*;
import com.webcohesion.enunciate.modules.jackson.model.adapters.AdapterType;
import com.webcohesion.enunciate.modules.jackson.model.types.JsonType;
import com.webcohesion.enunciate.modules.jackson.model.types.KnownJsonType;
import com.webcohesion.enunciate.modules.jackson.model.util.JacksonUtil;
import com.webcohesion.enunciate.modules.jackson.model.util.MapType;
import com.webcohesion.enunciate.util.IgnoreUtils;
import com.webcohesion.enunciate.util.OneTimeLogMessage;
import javax.activation.DataHandler;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.*;
import javax.lang.model.util.Elements;
import javax.lang.model.util.SimpleTypeVisitor6;
import javax.lang.model.util.Types;
import javax.xml.bind.JAXBElement;
import javax.xml.datatype.XMLGregorianCalendar;
import javax.xml.namespace.QName;
import java.sql.Timestamp;
import java.util.*;
/**
* @author Ryan Heaton
*/
@SuppressWarnings("unchecked")
public class EnunciateJacksonContext extends EnunciateModuleContext {
private final Map<String, JsonType> knownTypes;
private final Map<String, TypeDefinition> typeDefinitions;
private final boolean honorJaxb;
private final KnownJsonType dateType;
private final Map<String, TypeDefinition> typeDefinitionsBySlug;
private final boolean collapseTypeHierarchy;
private final Map<String, String> mixins;
private final boolean disableExamples;
private final boolean wrapRootValue;
private final AccessorVisibilityChecker defaultVisibility;
public EnunciateJacksonContext(EnunciateContext context, boolean honorJaxb, KnownJsonType dateType, boolean collapseTypeHierarchy, Map<String, String> mixins, AccessorVisibilityChecker visibility, boolean disableExamples, boolean wrapRootValue) {
super(context);
this.dateType = dateType;
this.mixins = mixins;
this.defaultVisibility = visibility;
this.disableExamples = disableExamples;
this.knownTypes = loadKnownTypes();
this.typeDefinitions = new HashMap<String, TypeDefinition>();
this.honorJaxb = honorJaxb;
this.collapseTypeHierarchy = collapseTypeHierarchy;
this.typeDefinitionsBySlug = new HashMap<String, TypeDefinition>();
this.wrapRootValue = wrapRootValue;
}
public EnunciateContext getContext() {
return context;
}
public boolean isHonorJaxb() {
return honorJaxb;
}
public boolean isCollapseTypeHierarchy() {
return collapseTypeHierarchy;
}
public Collection<TypeDefinition> getTypeDefinitions() {
return this.typeDefinitions.values();
}
public boolean isDisableExamples() {
return disableExamples;
}
public boolean isWrapRootValue() {
return wrapRootValue;
}
public DecoratedTypeMirror resolveSyntheticType(DecoratedTypeMirror type) {
if (type instanceof DeclaredType && !type.isCollection() && MapType.findMapType(type, this) == null) {
if (!((DeclaredType) type).getTypeArguments().isEmpty()) {
//if type arguments apply, create a new "synthetic" declared type that captures the type arguments.
type = new ParameterizedJacksonDeclaredType((DeclaredType) type, getContext().getProcessingEnvironment());
}
else if (type.isInterface()) {
//if it's an interface, create a "synthetic" type that pretends like it's an abstract class.
type = new InterfaceJacksonDeclaredType((DeclaredType) type, getContext().getProcessingEnvironment());
}
}
return type;
}
public JsonType getKnownType(Element declaration) {
if (declaration instanceof TypeElement) {
return this.knownTypes.get(((TypeElement) declaration).getQualifiedName().toString());
}
return null;
}
public TypeDefinition findTypeDefinition(Element declaration) {
if (declaration instanceof TypeElement) {
return this.typeDefinitions.get(((TypeElement) declaration).getQualifiedName().toString());
}
return null;
}
protected Map<String, JsonType> loadKnownTypes() {
HashMap<String, JsonType> knownTypes = new HashMap<String, JsonType>();
knownTypes.put(Boolean.class.getName(), KnownJsonType.BOOLEAN);
knownTypes.put(Byte.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Character.class.getName(), KnownJsonType.STRING);
knownTypes.put(Double.class.getName(), KnownJsonType.NUMBER);
knownTypes.put(Float.class.getName(), KnownJsonType.NUMBER);
knownTypes.put(Integer.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Long.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Short.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Boolean.TYPE.getName(), KnownJsonType.BOOLEAN);
knownTypes.put(Byte.TYPE.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Double.TYPE.getName(), KnownJsonType.NUMBER);
knownTypes.put(Float.TYPE.getName(), KnownJsonType.NUMBER);
knownTypes.put(Integer.TYPE.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Long.TYPE.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Short.TYPE.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(Character.TYPE.getName(), KnownJsonType.STRING);
knownTypes.put(String.class.getName(), KnownJsonType.STRING);
knownTypes.put(Enum.class.getName(), KnownJsonType.STRING);
knownTypes.put(QName.class.getName(), KnownJsonType.STRING);
knownTypes.put(java.math.BigInteger.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(java.math.BigDecimal.class.getName(), KnownJsonType.NUMBER);
knownTypes.put(java.util.Calendar.class.getName(), this.dateType);
knownTypes.put(java.util.Date.class.getName(), this.dateType);
knownTypes.put(Timestamp.class.getName(), this.dateType);
knownTypes.put(java.net.URI.class.getName(), KnownJsonType.STRING);
knownTypes.put(java.net.URL.class.getName(), KnownJsonType.STRING);
knownTypes.put(java.lang.Object.class.getName(), KnownJsonType.OBJECT);
knownTypes.put(byte[].class.getName(), KnownJsonType.STRING);
knownTypes.put(java.nio.ByteBuffer.class.getName(), KnownJsonType.STRING);
knownTypes.put(DataHandler.class.getName(), KnownJsonType.STRING);
knownTypes.put(java.util.UUID.class.getName(), KnownJsonType.STRING);
knownTypes.put(XMLGregorianCalendar.class.getName(), this.dateType);
knownTypes.put(GregorianCalendar.class.getName(), this.dateType);
knownTypes.put(JsonNode.class.getName(), KnownJsonType.OBJECT);
knownTypes.put(ContainerNode.class.getName(), KnownJsonType.OBJECT);
knownTypes.put(ArrayNode.class.getName(), KnownJsonType.ARRAY);
knownTypes.put(ObjectNode.class.getName(), KnownJsonType.OBJECT);
knownTypes.put(ValueNode.class.getName(), KnownJsonType.STRING);
knownTypes.put(TextNode.class.getName(), KnownJsonType.STRING);
knownTypes.put(BinaryNode.class.getName(), KnownJsonType.STRING);
knownTypes.put(MissingNode.class.getName(), KnownJsonType.STRING);
knownTypes.put(NullNode.class.getName(), KnownJsonType.STRING);
knownTypes.put(NumericNode.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(IntNode.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(ShortNode.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(FloatNode.class.getName(), KnownJsonType.NUMBER);
knownTypes.put(DoubleNode.class.getName(), KnownJsonType.NUMBER);
knownTypes.put(DecimalNode.class.getName(), KnownJsonType.NUMBER);
knownTypes.put(LongNode.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(BigIntegerNode.class.getName(), KnownJsonType.WHOLE_NUMBER);
knownTypes.put(POJONode.class.getName(), KnownJsonType.OBJECT);
knownTypes.put(BooleanNode.class.getName(), KnownJsonType.BOOLEAN);
knownTypes.put(Class.class.getName(), KnownJsonType.OBJECT);
knownTypes.put("java.time.Period", KnownJsonType.STRING);
knownTypes.put("java.time.Duration", this.dateType);
knownTypes.put("java.time.Instant", this.dateType);
knownTypes.put("java.time.Year", this.dateType);
knownTypes.put("java.time.YearMonth", KnownJsonType.STRING);
knownTypes.put("java.time.MonthDay", KnownJsonType.STRING);
knownTypes.put("java.time.ZoneId", KnownJsonType.STRING);
knownTypes.put("java.time.ZoneOffset", KnownJsonType.STRING);
knownTypes.put("java.time.LocalDate", KnownJsonType.STRING);
knownTypes.put("java.time.LocalTime", KnownJsonType.STRING);
knownTypes.put("java.time.LocalDateTime", KnownJsonType.STRING);
knownTypes.put("java.time.OffsetTime", KnownJsonType.STRING);
knownTypes.put("java.time.ZonedDateTime", this.dateType);
knownTypes.put("java.time.OffsetDateTime", this.dateType);
knownTypes.put("org.joda.time.DateTime", this.dateType);
knownTypes.put("java.util.Currency", KnownJsonType.STRING);
for (String m : this.mixins.keySet()) {
if (knownTypes.remove(m) != null) {
debug("Unregistering %s from known types, as it is redefined using a mixin.", m);
}
}
return knownTypes;
}
/**
* Find the type definition for a class given the class's declaration.
*
* @param declaration The declaration.
* @return The type definition.
*/
protected TypeDefinition createTypeDefinition(TypeElement declaration) {
declaration = narrowToAdaptingType(declaration);
if (isEnumType(declaration)) {
if (declaration.getAnnotation(XmlQNameEnum.class) != null) {
return new QNameEnumTypeDefinition(declaration, this);
}
else {
return new EnumTypeDefinition(declaration, this);
}
}
else {
ObjectTypeDefinition typeDef = new ObjectTypeDefinition(declaration, this);
if (typeDef.getValue() != null) {
return new SimpleTypeDefinition(typeDef);
}
else {
return typeDef;
}
}
}
/**
* Narrows the existing declaration down to its adapting declaration, if it's being adapted. Otherwise, the original declaration will be returned.
*
* @param declaration The declaration to narrow.
* @return The narrowed declaration.
*/
protected TypeElement narrowToAdaptingType(TypeElement declaration) {
AdapterType adapterType = JacksonUtil.findAdapterType(declaration, this);
if (adapterType != null) {
TypeMirror adaptingType = adapterType.getAdaptingType();
if (adaptingType.getKind() != TypeKind.DECLARED) {
return declaration;
}
else {
TypeElement adaptingDeclaration = (TypeElement) ((DeclaredType) adaptingType).asElement();
if (adaptingDeclaration == null) {
throw new EnunciateException(String.format("Class %s is being adapted by a type (%s) that doesn't seem to be on the classpath.", declaration.getQualifiedName(), adaptingType));
}
return adaptingDeclaration;
}
}
return declaration;
}
/**
* A quick check to see if a declaration defines a enum schema type.
*
* @param declaration The declaration to check.
* @return the value of the check.
*/
protected boolean isEnumType(TypeElement declaration) {
JsonFormat format = declaration.getAnnotation(JsonFormat.class);
if (format != null && format.shape() == JsonFormat.Shape.OBJECT) {
return false;
}
return declaration.getKind() == ElementKind.ENUM;
}
public boolean isKnownTypeDefinition(TypeElement el) {
return findTypeDefinition(el) != null || isKnownType(el);
}
public boolean isIgnored(Element el) {
return IgnoreUtils.isIgnored(el) || (el.getAnnotation(JsonIgnore.class) != null && el.getAnnotation(JsonIgnore.class).value());
}
public AccessorVisibilityChecker getDefaultVisibility() {
return this.defaultVisibility;
}
public void add(TypeDefinition typeDef, LinkedList<Element> stack) {
if (findTypeDefinition(typeDef) == null && !isKnownType(typeDef)) {
this.typeDefinitions.put(typeDef.getQualifiedName().toString(), typeDef);
if (this.context.isExcluded(typeDef)) {
warn("Added %s as a Jackson type definition even though is was supposed to be excluded according to configuration. It was referenced from %s%s, so it had to be included to prevent broken references.", typeDef.getQualifiedName(), stack.size() > 0 ? stack.get(0) : "an unknown location", stack.size() > 1 ? " of " + stack.get(1) : "");
}
else {
debug("Added %s as a Jackson type definition.", typeDef.getQualifiedName());
}
if (getContext().getProcessingEnvironment().findSourcePosition(typeDef) == null) {
OneTimeLogMessage.SOURCE_FILES_NOT_FOUND.log(getContext());
debug("Unable to find source file for %s.", typeDef.getQualifiedName());
}
typeDef.getReferencedFrom().addAll(stack);
try {
stack.push(typeDef);
addSeeAlsoTypeDefinitions(typeDef, stack);
for (Member member : typeDef.getMembers()) {
addReferencedTypeDefinitions(member, stack);
}
Value value = typeDef.getValue();
if (value != null) {
addReferencedTypeDefinitions(value, stack);
}
TypeMirror superclass = typeDef.getSuperclass();
if (!typeDef.isBaseObject() && superclass != null && superclass.getKind() != TypeKind.NONE && !isCollapseTypeHierarchy()) {
addReferencedTypeDefinitions(superclass, stack);
}
}
finally {
stack.pop();
}
}
}
protected void addReferencedTypeDefinitions(Accessor accessor, LinkedList<Element> stack) {
stack.push(accessor);
try {
addSeeAlsoTypeDefinitions(accessor, stack);
TypeMirror enumRef = accessor.getQNameEnumRef();
if (enumRef != null) {
addReferencedTypeDefinitions(enumRef, stack);
}
}
finally {
stack.pop();
}
}
/**
* Add the type definition(s) referenced by the given value.
*
* @param value The value.
* @param stack The context stack.
*/
protected void addReferencedTypeDefinitions(Value value, LinkedList<Element> stack) {
stack.push(value);
try {
addReferencedTypeDefinitions((Accessor) value, stack);
if (value.isAdapted()) {
addReferencedTypeDefinitions(value.getAdapterType(), stack);
}
else if (value.getQNameEnumRef() == null) {
addReferencedTypeDefinitions(value.getAccessorType(), stack);
}
}
finally {
stack.pop();
}
}
/**
* Add the referenced type definitions for the specified element.
*
* @param member The element.
* @param stack The context stack.
*/
protected void addReferencedTypeDefinitions(Member member, LinkedList<Element> stack) {
addReferencedTypeDefinitions((Accessor) member, stack);
stack.push(member);
try {
for (Member choice : member.getChoices()) {
if (choice.isAdapted()) {
addReferencedTypeDefinitions(choice.getAdapterType(), stack);
}
else if (choice.getQNameEnumRef() == null) {
addReferencedTypeDefinitions(choice.getAccessorType(), stack);
}
}
}
finally {
stack.pop();
}
}
/**
* Adds any referenced type definitions for the specified type mirror.
*
* @param type The type mirror.
*/
protected void addReferencedTypeDefinitions(TypeMirror type, LinkedList<Element> stack) {
type.accept(new ReferencedJsonDefinitionVisitor(), new ReferenceContext(stack));
}
/**
* Add any type definitions that are specified as "see also".
*
* @param declaration The declaration.
*/
protected void addSeeAlsoTypeDefinitions(Element declaration, LinkedList<Element> stack) {
JsonSubTypes subTypes = declaration.getAnnotation(JsonSubTypes.class);
if (subTypes != null) {
Elements elementUtils = getContext().getProcessingEnvironment().getElementUtils();
Types typeUtils = getContext().getProcessingEnvironment().getTypeUtils();
JsonSubTypes.Type[] types = subTypes.value();
for (JsonSubTypes.Type type : types) {
try {
stack.push(elementUtils.getTypeElement(JsonSubTypes.class.getName()));
Class clazz = type.value();
add(createTypeDefinition(elementUtils.getTypeElement(clazz.getName())), stack);
}
catch (MirroredTypeException e) {
TypeMirror mirror = e.getTypeMirror();
Element element = typeUtils.asElement(mirror);
if (element instanceof TypeElement) {
add(createTypeDefinition((TypeElement) element), stack);
}
}
catch (MirroredTypesException e) {
List<? extends TypeMirror> mirrors = e.getTypeMirrors();
for (TypeMirror mirror : mirrors) {
Element element = typeUtils.asElement(mirror);
if (element instanceof TypeElement) {
add(createTypeDefinition((TypeElement) element), stack);
}
}
}
finally {
stack.pop();
}
}
}
JsonSeeAlso seeAlso = declaration.getAnnotation(JsonSeeAlso.class);
if (seeAlso != null) {
Elements elementUtils = getContext().getProcessingEnvironment().getElementUtils();
Types typeUtils = getContext().getProcessingEnvironment().getTypeUtils();
stack.push(elementUtils.getTypeElement(JsonSeeAlso.class.getName()));
try {
Class[] classes = seeAlso.value();
for (Class clazz : classes) {
add(createTypeDefinition(elementUtils.getTypeElement(clazz.getName())), stack);
}
}
catch (MirroredTypeException e) {
TypeMirror mirror = e.getTypeMirror();
Element element = typeUtils.asElement(mirror);
if (element instanceof TypeElement) {
add(createTypeDefinition((TypeElement) element), stack);
}
}
catch (MirroredTypesException e) {
List<? extends TypeMirror> mirrors = e.getTypeMirrors();
for (TypeMirror mirror : mirrors) {
Element element = typeUtils.asElement(mirror);
if (element instanceof TypeElement) {
add(createTypeDefinition((TypeElement) element), stack);
}
}
}
finally {
stack.pop();
}
}
if (subTypes == null && seeAlso == null && declaration instanceof TypeElement) {
// No annotation tells us what to do, so we'll look up subtypes and add them
for (Element el : getContext().getApiElements()) {
if ((el instanceof TypeElement) && !((TypeElement)el).getQualifiedName().contentEquals(((TypeElement)declaration).getQualifiedName()) && ((DecoratedTypeMirror) el.asType()).isInstanceOf(declaration)) {
add(createTypeDefinition((TypeElement) el), stack);
}
}
}
}
/**
* Whether the specified type is a known type.
*
* @param typeDef The type def.
* @return Whether the specified type is a known type.
*/
protected boolean isKnownType(TypeElement typeDef) {
return knownTypes.containsKey(typeDef.getQualifiedName().toString()) || ((DecoratedTypeMirror) typeDef.asType()).isInstanceOf(JAXBElement.class);
}
/**
* Get the slug for the given type definition.
*
* @param typeDefinition The type definition.
* @return The slug for the type definition.
*/
public String getSlug(TypeDefinition typeDefinition) {
String[] qualifiedNameTokens = typeDefinition.getQualifiedName().toString().split("\\.");
String slug = "";
for (int i = qualifiedNameTokens.length - 1; i >= 0; i--) {
slug = slug.isEmpty() ? qualifiedNameTokens[i] : slug + "_" + qualifiedNameTokens[i];
TypeDefinition entry = this.typeDefinitionsBySlug.get(slug);
if (entry == null) {
entry = typeDefinition;
this.typeDefinitionsBySlug.put(slug, entry);
}
if (entry.getQualifiedName().toString().equals(typeDefinition.getQualifiedName().toString())) {
return slug;
}
}
return slug;
}
/**
* Look up the mix-in for a given element.
*
* @param element The element for which to look up the mix-in.
* @return The mixin.
*/
public TypeElement lookupMixin(TypeElement element) {
String mixin = this.mixins.get(element.getQualifiedName().toString());
if (mixin != null) {
return getContext().getProcessingEnvironment().getElementUtils().getTypeElement(mixin);
}
return null;
}
/**
* Visitor for JSON-referenced type definitions.
*/
private class ReferencedJsonDefinitionVisitor extends SimpleTypeVisitor6<Void, ReferenceContext> {
@Override
public Void visitArray(ArrayType t, ReferenceContext context) {
return t.getComponentType().accept(this, context);
}
@Override
public Void visitDeclared(DeclaredType declaredType, ReferenceContext context) {
TypeElement declaration = (TypeElement) declaredType.asElement();
if (declaration.getKind() == ElementKind.ENUM) {
if (!isKnownTypeDefinition(declaration)) {
add(createTypeDefinition(declaration), context.referenceStack);
}
}
else if (declaredType instanceof AdapterType) {
((AdapterType) declaredType).getAdaptingType().accept(this, context);
}
else if (MapType.findMapType(declaredType, EnunciateJacksonContext.this) == null) {
String qualifiedName = declaration.getQualifiedName().toString();
if (Object.class.getName().equals(qualifiedName)) {
//skip base object; not a type definition.
return null;
}
if (context.recursionStack.contains(declaration)) {
//we're already visiting this class...
return null;
}
context.recursionStack.push(declaration);
try {
if (!isKnownTypeDefinition(declaration) && !isIgnored(declaration) && declaration.getKind() == ElementKind.CLASS && !((DecoratedDeclaredType) declaredType).isCollection() && !((DecoratedDeclaredType) declaredType).isInstanceOf(JAXBElement.class)) {
add(createTypeDefinition(declaration), context.referenceStack);
}
List<? extends TypeMirror> typeArgs = declaredType.getTypeArguments();
if (typeArgs != null) {
for (TypeMirror typeArg : typeArgs) {
typeArg.accept(this, context);
}
}
}
finally {
context.recursionStack.pop();
}
}
else {
List<? extends TypeMirror> typeArgs = declaredType.getTypeArguments();
if (typeArgs != null) {
for (TypeMirror typeArg : typeArgs) {
typeArg.accept(this, context);
}
}
}
return null;
}
@Override
public Void visitTypeVariable(TypeVariable t, ReferenceContext context) {
return t.getUpperBound().accept(this, context);
}
@Override
public Void visitWildcard(WildcardType t, ReferenceContext context) {
TypeMirror extendsBound = t.getExtendsBound();
if (extendsBound != null) {
extendsBound.accept(this, context);
}
TypeMirror superBound = t.getSuperBound();
if (superBound != null) {
superBound.accept(this, context);
}
return null;
}
@Override
public Void visitUnknown(TypeMirror t, ReferenceContext context) {
return defaultAction(t, context);
}
}
private static class ReferenceContext {
LinkedList<Element> referenceStack;
LinkedList<Element> recursionStack;
public ReferenceContext(LinkedList<Element> referenceStack) {
this.referenceStack = referenceStack;
recursionStack = new LinkedList<Element>();
}
}
}