forked from wkennedy/swagger4spring-web
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixing bug which causes inner classes be skipped during recursive loo…
…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
Showing
5 changed files
with
322 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
137 changes: 137 additions & 0 deletions
137
src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
115 changes: 115 additions & 0 deletions
115
src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
)) | ||
} | ||
} | ||
}) | ||
} | ||
} |