/** * 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.jackson1; import com.webcohesion.enunciate.EnunciateContext; import com.webcohesion.enunciate.api.ApiRegistry; import com.webcohesion.enunciate.javac.decorations.type.DecoratedTypeMirror; import com.webcohesion.enunciate.metadata.Ignore; import com.webcohesion.enunciate.module.*; import com.webcohesion.enunciate.util.OneTimeLogMessage; import org.apache.commons.configuration.HierarchicalConfiguration; import com.webcohesion.enunciate.modules.jackson1.model.AccessorVisibilityChecker; import com.webcohesion.enunciate.modules.jackson1.model.types.KnownJsonType; import org.codehaus.jackson.annotate.JacksonAnnotation; import org.codehaus.jackson.annotate.JsonAutoDetect; import org.codehaus.jackson.annotate.JsonIgnore; import org.codehaus.jackson.annotate.JsonMethod; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.annotate.JsonSerialize; import org.reflections.adapters.MetadataAdapter; import javax.lang.model.element.*; import javax.lang.model.type.TypeMirror; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlTransient; import javax.xml.bind.annotation.XmlType; import java.util.*; /** * @author Ryan Heaton */ @SuppressWarnings ( "unchecked" ) public class Jackson1Module extends BasicProviderModule implements TypeDetectingModule, MediaTypeDefinitionModule, ApiRegistryProviderModule, ApiFeatureProviderModule { private DataTypeDetectionStrategy defaultDataTypeDetectionStrategy; private boolean jacksonDetected = false; private boolean jaxbSupportDetected = false; private EnunciateJackson1Context jacksonContext; private ApiRegistry apiRegistry; @Override public String getName() { return "jackson1"; } public boolean isHonorJaxbAnnotations() { return this.config.getBoolean("[@honorJaxb]", this.jaxbSupportDetected); } public boolean isCollapseTypeHierarchy() { return this.config.getBoolean("[@collapse-type-hierarchy]", false); } public boolean isWrapRootValue() { return this.config.getBoolean("[@wrapRootValue]", false); } public KnownJsonType getDateFormat() { String dateFormatString = this.config.getString("[@dateFormat]", KnownJsonType.WHOLE_NUMBER.name()); return KnownJsonType.valueOf(dateFormatString.toUpperCase()); } public boolean isDisableExamples() { return this.config.getBoolean("[@disableExamples]", false); } @Override public ApiRegistry getApiRegistry() { return new Jackson1ApiRegistry(this.jacksonContext); } public EnunciateJackson1Context getJacksonContext() { return jacksonContext; } @Override public void call(EnunciateContext context) { this.jacksonContext = new EnunciateJackson1Context(context, isHonorJaxbAnnotations(), getDateFormat(), isCollapseTypeHierarchy(), getMixins(), getDefaultVisibility(), isDisableExamples(), isWrapRootValue()); DataTypeDetectionStrategy detectionStrategy = getDataTypeDetectionStrategy(); switch (detectionStrategy) { case aggressive: for (Element declaration : context.getApiElements()) { addPotentialJacksonElement(declaration, new LinkedList<Element>()); } break; case local: for (Element declaration : context.getLocalApiElements()) { addPotentialJacksonElement(declaration, new LinkedList<Element>()); } //no break, add explicit includes: default: if (context.hasExplicitIncludes()) { //if we're not aggressive, we only want to add the api elements if they've been explicitly included for (Element declaration : context.getApiElements()) { addPotentialJacksonElement(declaration, new LinkedList<Element>()); } } } } @Override public boolean isEnabled() { return !this.config.getBoolean("[@disabled]", !jacksonDetected); } public DataTypeDetectionStrategy getDataTypeDetectionStrategy() { String dataTypeDetection = this.config.getString("[@datatype-detection]", null); if (dataTypeDetection != null) { try { return DataTypeDetectionStrategy.valueOf(dataTypeDetection); } catch (IllegalArgumentException e) { //fall through... } } return this.defaultDataTypeDetectionStrategy == null ? DataTypeDetectionStrategy.local : this.defaultDataTypeDetectionStrategy; } @Override public void setDefaultDataTypeDetectionStrategy(DataTypeDetectionStrategy strategy) { this.defaultDataTypeDetectionStrategy = strategy; } @Override public void addDataTypeDefinitions(TypeMirror type, Set<String> declaredMediaTypes, LinkedList<Element> contextStack) { boolean jsonApplies = false; for (String mediaType : declaredMediaTypes) { if ("*/*".equals(mediaType) || "text/*".equals(mediaType) || "application/*".equals(mediaType) || "application/json".equals(mediaType) || mediaType.endsWith("+json")) { jsonApplies = true; break; } } if (jsonApplies) { type = this.jacksonContext.resolveSyntheticType((DecoratedTypeMirror) type); this.jacksonContext.addReferencedTypeDefinitions(type, contextStack); } else { debug("Element %s is NOT to be added as a Jackson data type because %s doesn't seem to include JSON.", type, declaredMediaTypes); } } public Map<String, String> getMixins() { HashMap<String, String> mixins = new HashMap<String, String>(); List<HierarchicalConfiguration> mixinElements = this.config.configurationsAt("mixin"); for (HierarchicalConfiguration mixinElement : mixinElements) { mixins.put(mixinElement.getString("[@target]", ""), mixinElement.getString("[@source]", "")); } return mixins; } public AccessorVisibilityChecker getDefaultVisibility() { List<HierarchicalConfiguration> visibilityElements = this.config.configurationsAt("accessor-visibility"); AccessorVisibilityChecker checker = AccessorVisibilityChecker.DEFAULT_CHECKER; for (HierarchicalConfiguration visibilityElement : visibilityElements) { JsonMethod method = JsonMethod.valueOf(visibilityElement.getString("[@type]", "").toUpperCase()); JsonAutoDetect.Visibility visibility = JsonAutoDetect.Visibility.valueOf(visibilityElement.getString("[@visibility]", "").toUpperCase()); checker = checker.withVisibility(method, visibility); } return checker; } protected void addPotentialJacksonElement(Element declaration, LinkedList<Element> contextStack) { if (declaration instanceof TypeElement) { if (!this.jacksonContext.isKnownTypeDefinition((TypeElement) declaration) && isExplicitTypeDefinition(declaration, this.jacksonContext.isHonorJaxb())) { OneTimeLogMessage.JACKSON_1_DEPRECATED.log(this.context); this.jacksonContext.add(this.jacksonContext.createTypeDefinition((TypeElement) declaration), contextStack); } } } protected boolean isExplicitTypeDefinition(Element declaration, boolean honorJaxb) { if (declaration.getKind() != ElementKind.CLASS && declaration.getKind() != ElementKind.ENUM) { debug("%s isn't a potential Jackson type because it's not a class or an enum.", declaration); return false; } PackageElement pckg = this.context.getProcessingEnvironment().getElementUtils().getPackageOf(declaration); if ((pckg != null) && (pckg.getAnnotation(Ignore.class) != null)) { debug("%s isn't a potential Jackson type because its package is annotated as to be ignored.", declaration); return false; } if (isThrowable(declaration)) { debug("%s isn't a potential Jackson type because it's an instance of java.lang.Throwable.", declaration); return false; } List<? extends AnnotationMirror> annotationMirrors = declaration.getAnnotationMirrors(); boolean explicitXMLTypeOrElement = false; for (AnnotationMirror mirror : annotationMirrors) { Element annotationDeclaration = mirror.getAnnotationType().asElement(); if (annotationDeclaration != null) { String fqn = annotationDeclaration instanceof TypeElement ? ((TypeElement)annotationDeclaration).getQualifiedName().toString() : ""; //exclude all XmlTransient types and all jaxws types. if (JsonIgnore.class.getName().equals(fqn) || fqn.startsWith("javax.xml.ws") || fqn.startsWith("javax.ws.rs") || fqn.startsWith("javax.jws")) { debug("%s isn't a potential Jackson type because of annotation %s.", declaration, fqn); return false; } else { if (honorJaxb) { if (XmlTransient.class.getName().equals(fqn)) { debug("%s isn't a potential Jackson type because of annotation %s.", declaration, fqn); return false; } if ((XmlType.class.getName().equals(fqn)) || (XmlRootElement.class.getName().equals(fqn))) { debug("%s will be considered a Jackson type because we're honoring the %s annotation.", declaration, fqn); explicitXMLTypeOrElement = true; } } explicitXMLTypeOrElement = explicitXMLTypeOrElement || isJacksonSerializationAnnotation(fqn); } } if (explicitXMLTypeOrElement) { break; } } return explicitXMLTypeOrElement; } /** * Whether the specified declaration is throwable. * * @param declaration The declaration to determine whether it is throwable. * @return Whether the specified declaration is throwable. */ protected boolean isThrowable(Element declaration) { return declaration.getKind() == ElementKind.CLASS && ((DecoratedTypeMirror) declaration.asType()).isInstanceOf(Throwable.class); } @Override public boolean typeDetected(Object type, MetadataAdapter metadata) { String classname = metadata.getClassName(type); this.jacksonDetected |= ObjectMapper.class.getName().equals(classname); this.jaxbSupportDetected |= "org.codehaus.jackson.xc.JaxbAnnotationIntrospector".equals(classname); if (classname.startsWith("org.codehaus.jackson")) { //don't accept jackson system specific types return false; } List<String> classAnnotations = metadata.getClassAnnotationNames(type); if (classAnnotations != null) { for (String fqn : classAnnotations) { if (isJacksonSerializationAnnotation(fqn)) { return true; } } } return false; } boolean isJacksonSerializationAnnotation(String fqn) { return !JacksonAnnotation.class.getName().equals(fqn) && (JsonSerialize.class.getName().equals(fqn)); } }