Skip to content

Commit

Permalink
Fixing bug which causes inner classes be skipped during recursive loo…
Browse files Browse the repository at this point in the history
…kup of models.

So the inner classes marked with $ sign like List[com.something.Container$Inner] and regexp doesn't contain $

val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-]*)\\].*".r
should be
val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-\\$]*)\\].*".r
  • Loading branch information
Andrey Antonov committed Apr 28, 2015
1 parent 8a1bf01 commit 6006610
Show file tree
Hide file tree
Showing 5 changed files with 322 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.wordnik.swagger.annotations.ApiOperation;
import com.wordnik.swagger.annotations.ApiResponse;
import com.wordnik.swagger.annotations.ApiResponses;
import com.wordnik.swagger.converter.ModelConverters;
import com.wordnik.swagger.converter.CustomModelConverters;
import com.wordnik.swagger.model.Model;
import com.wordnik.swagger.model.Operation;
import com.wordnik.swagger.model.Parameter;
Expand Down Expand Up @@ -193,7 +193,7 @@ void setResponseClass(Class<?> responseClass) {
return;
}

Option<Model> model = ModelConverters.read(responseClass);
Option<Model> model = CustomModelConverters.read(responseClass);
if (model.nonEmpty()) {
this.responseClass = model.get().name();
} else {
Expand Down Expand Up @@ -280,7 +280,7 @@ public void setResponseContainer(final String container) {
}

public void setResponseContainer(final Class<?> type) {
Option<Model> model = ModelConverters.read(type);
Option<Model> model = CustomModelConverters.read(type);
if (model.nonEmpty()) {
setResponseContainer(model.get().name());
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.knappsack.swagger4springweb.util;

import com.wordnik.swagger.converter.ModelConverters;
import com.wordnik.swagger.converter.CustomModelConverters;
import com.wordnik.swagger.model.Model;
import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.multipart.MultipartFile;
Expand Down Expand Up @@ -66,7 +66,7 @@ static boolean isIgnorableModel(String name) {
}

public static void addModels(final Class<?> clazz, final Map<String, Model> models) {
scala.collection.immutable.List<Model> modelOption = ModelConverters.readAll(clazz);
scala.collection.immutable.List<Model> modelOption = CustomModelConverters.readAll(clazz);
scala.collection.Iterator<Model> iterator = modelOption.iterator();
while (iterator.hasNext()) {
Model model = iterator.next();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package com.wordnik.swagger.converter

import com.wordnik.swagger.core._
import com.wordnik.swagger.model._
import org.slf4j.LoggerFactory

import scala.collection.mutable.{HashMap, HashSet, ListBuffer}

object CustomModelConverters {
private val LOGGER = LoggerFactory.getLogger(CustomModelConverters.getClass)
val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-\\$]*)\\].*".r // TODO fix for generics

val converters = new ListBuffer[ModelConverter]() ++ List(
new JodaDateTimeConverter,
new CustomSwaggerSchemaConverter
)

def addConverter(c: ModelConverter, first: Boolean = false) = {
if(first) {
val reordered = List(c) ++ converters
converters.clear
converters ++= reordered
}
else converters += c
}

def read(cls: Class[_]): Option[Model] = {
var model: Option[Model] = None
val itr = converters.iterator
while(model == None && itr.hasNext) {
itr.next.read(cls) match {
case Some(m) => {
val filteredProperties = m.properties.filter(prop => skippedClasses.contains(prop._2.qualifiedType) == false)
model = Some(m.copy(properties = filteredProperties))
}
case _ => model = None
}
}
model
}

def readAll(cls: Class[_]): List[Model] = {
val output = new HashMap[String, Model]
val model = read(cls)

// add subTypes
model.map(_.subTypes.map(typeRef => {
try{
LOGGER.debug("loading subtype " + typeRef)
val cls = SwaggerContext.loadClass(typeRef)
read(cls) match {
case Some(model) => output += cls.getName -> model
case _ =>
}
}
catch {
case e: Exception => LOGGER.error("can't load class " + typeRef)
}
}))

// add properties
model.map(m => {
output += cls.getName -> m
val checkedNames = new HashSet[String]
addRecursive(m, checkedNames, output)
})
output.values.toList
}

def addRecursive(model: Model, checkedNames: HashSet[String], output: HashMap[String, Model]): Unit = {
if(!checkedNames.contains(model.name)) {
val propertyNames = new HashSet[String]
for((name, property) <- model.properties) {
val propertyName = property.items match {
case Some(item) => item.qualifiedType.getOrElse(item.`type`)
case None => property.qualifiedType
}
val name = propertyName match {
case ComplexTypeMatcher(containerType, basePart) => basePart
case e: String => e
}
propertyNames += name
}
for(typeRef <- propertyNames) {
if(ignoredPackages.contains(getPackage(typeRef))) None
else if(ignoredClasses.contains(typeRef)) {
None
}
else if(!checkedNames.contains(typeRef)) {
try{
checkedNames += typeRef
val cls = SwaggerContext.loadClass(typeRef)
LOGGER.debug("reading class " + cls)
CustomModelConverters.read(cls) match {
case Some(model) => {
output += typeRef -> model
addRecursive(model, checkedNames, output)
}
case None =>
}
}
catch {
case e: ClassNotFoundException =>
}
}
}
}
}

def toName(cls: Class[_]): String = {
var name: String = null
val itr = converters.iterator
while(name == null && itr.hasNext) {
name = itr.next.toName(cls)
}
name
}

def getPackage(str: String): String = {
str.lastIndexOf(".") match {
case -1 => ""
case e: Int => str.substring(0, e)
}
}

def ignoredPackages: Set[String] = {
(for(converter <- converters) yield converter.ignoredPackages).flatten.toSet
}

def ignoredClasses: Set[String] = {
(for(converter <- converters) yield converter.ignoredClasses).flatten.toSet
}

def skippedClasses: Set[String] = {
(for(converter <- converters) yield converter.skippedClasses).flatten.toSet
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.wordnik.swagger.converter

import java.lang.annotation.Annotation
import java.lang.reflect.Type

import com.wordnik.swagger.model._
import org.slf4j.LoggerFactory

import scala.collection.mutable.LinkedHashMap

class CustomModelPropertyParser(cls: Class[_])(implicit properties: LinkedHashMap[String, ModelProperty]) extends ModelPropertyParser(cls) {
private val LOGGER = LoggerFactory.getLogger(classOf[ModelPropertyParser])

override def parsePropertyAnnotations(returnClass: Class[_], propertyName: String, propertyAnnotations: Array[Annotation], genericReturnType: Type, returnType: Type): Any = {
val e = extractGetterProperty(propertyName)
var name = e._1
val isGetter = e._2

var isFieldExists = false
var isJsonProperty = false
val hasAccessorNoneAnnotation = false
val processedAnnotations = processAnnotations(name, propertyAnnotations)
var required = processedAnnotations("required").asInstanceOf[Boolean]
var position = processedAnnotations("position").asInstanceOf[Int]

var description = {
if (processedAnnotations.contains("description") && processedAnnotations("description") != null)
Some(processedAnnotations("description").asInstanceOf[String])
else None
}
var isTransient = processedAnnotations("isTransient").asInstanceOf[Boolean]
var isXmlElement = processedAnnotations("isXmlElement").asInstanceOf[Boolean]
val isDocumented = processedAnnotations("isDocumented").asInstanceOf[Boolean]
var allowableValues = {
if (returnClass.isEnum)
Some(AllowableListValues((for (v <- returnClass.getEnumConstants) yield v.toString).toList))
else
processedAnnotations("allowableValues").asInstanceOf[Option[AllowableValues]]
}

try {
val fieldAnnotations = getDeclaredField(this.cls, name).getAnnotations()
val propAnnoOutput = processAnnotations(name, fieldAnnotations)
val propPosition = propAnnoOutput("position").asInstanceOf[Int]

if (allowableValues == None)
allowableValues = propAnnoOutput("allowableValues").asInstanceOf[Option[AllowableValues]]
if (description == None && propAnnoOutput.contains("description") && propAnnoOutput("description") != null)
description = Some(propAnnoOutput("description").asInstanceOf[String])
if (propPosition != 0) position = propAnnoOutput("position").asInstanceOf[Int]
if (required == false) required = propAnnoOutput("required").asInstanceOf[Boolean]
isFieldExists = true
if (!isTransient) isTransient = propAnnoOutput("isTransient").asInstanceOf[Boolean]
if (!isXmlElement) isXmlElement = propAnnoOutput("isXmlElement").asInstanceOf[Boolean]
isJsonProperty = propAnnoOutput("isJsonProperty").asInstanceOf[Boolean]
} catch {
//this means there is no field declared to look for field level annotations.
case e: java.lang.NoSuchFieldException => isTransient = false
}

//if class has accessor none annotation, the method/field should have explicit xml element annotations, if not
// consider it as transient
if (!isXmlElement && hasAccessorNoneAnnotation)
isTransient = true

if (!(isTransient && !isXmlElement && !isJsonProperty) && name != null && (isFieldExists || isGetter || isDocumented)) {
var paramType = getDataType(genericReturnType, returnType, false)
LOGGER.debug("inspecting " + paramType)
var simpleName = getDataType(genericReturnType, returnType, true)

if (!"void".equals(paramType) && null != paramType && !processedFields.contains(name)) {
if (!excludedFieldTypes.contains(paramType)) {
val items = {
val ComplexTypeMatcher = "([a-zA-Z]*)\\[([a-zA-Z\\.\\-\\$0-9_]*)\\].*".r // TODO fix for generics
paramType match {
case ComplexTypeMatcher(containerType, basePart) => {
LOGGER.debug("containerType: " + containerType + ", basePart: " + basePart + ", simpleName: " + simpleName)
paramType = containerType
val ComplexTypeMatcher(t, simpleTypeRef) = simpleName
val typeRef = {
if (simpleTypeRef.indexOf(",") > 0) // it's a map, use the value only
simpleTypeRef.split(",").last
else simpleTypeRef
}
simpleName = containerType
if (isComplex(simpleTypeRef)) {
Some(ModelRef(null, Some(simpleTypeRef), Some(basePart)))
}
else Some(ModelRef(simpleTypeRef, None, Some(basePart)))
}
case _ => None
}
}
val param = ModelProperty(
validateDatatype(simpleName),
paramType,
position,
required,
description,
allowableValues.getOrElse(AnyAllowableValues),
items)
LOGGER.debug("added param type " + paramType + " for field " + name)
properties += name -> param
}
else {
LOGGER.debug("field " + paramType + " is has been explicitly excluded")
}
}
else {
LOGGER.debug("skipping " + name)
}
processedFields += name
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.wordnik.swagger.converter

import com.fasterxml.jackson.annotation.{JsonSubTypes, JsonTypeInfo}
import com.wordnik.swagger.annotations.ApiModel
import com.wordnik.swagger.model._

import scala.collection.mutable.LinkedHashMap

class CustomSwaggerSchemaConverter
extends ModelConverter
with BaseConverter {

def read(cls: Class[_]): Option[Model] = {
Option(cls).flatMap({
cls => {
implicit val properties = new LinkedHashMap[String, ModelProperty]()
new CustomModelPropertyParser(cls).parse

val p = (for((key, value) <- properties)
yield (value.position, key, value)
).toList

val sortedProperties = new LinkedHashMap[String, ModelProperty]()
p.sortWith(_._1 < _._1).foreach(e => sortedProperties += e._2 -> e._3)

val parent = Option(cls.getAnnotation(classOf[ApiModel])) match {
case Some(e) => Some(e.parent.getName)
case _ => None
}
val discriminator = {
val v = {
val apiAnno = cls.getAnnotation(classOf[ApiModel])
if(apiAnno != null && apiAnno.discriminator != null)
apiAnno.discriminator
else if(cls.getAnnotation(classOf[JsonTypeInfo]) != null)
cls.getAnnotation(classOf[JsonTypeInfo]).property
else ""
}
if(v != null && v != "") Some(v)
else None
}
val subTypes = {
if(cls.getAnnotation(classOf[ApiModel]) != null)
cls.getAnnotation(classOf[ApiModel]).subTypes.map(_.getName).toList
else if(cls.getAnnotation(classOf[JsonSubTypes]) != null)
(for(subType <- cls.getAnnotation(classOf[JsonSubTypes]).value) yield (subType.value.getName)).toList
else List()
}
sortedProperties.size match {
case 0 => None
case _ => Some(Model(
toName(cls),
toName(cls),
cls.getName,
sortedProperties,
toDescriptionOpt(cls),
parent,
discriminator,
subTypes
))
}
}
})
}
}

0 comments on commit 6006610

Please sign in to comment.