From 60066105b7921ea60d3937950819a59c163c3267 Mon Sep 17 00:00:00 2001
From: Andrey Antonov <andrey.antonov@alertme.com>
Date: Tue, 28 Apr 2015 16:50:45 +0300
Subject: [PATCH] Fixing bug which causes inner classes be skipped during
 recursive lookup 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
---
 .../parser/ApiOperationParser.java            |   6 +-
 .../swagger4springweb/util/ModelUtils.java    |   4 +-
 .../converter/CustomModelConverters.scala     | 137 ++++++++++++++++++
 .../converter/CustomModelPropertyParser.scala | 115 +++++++++++++++
 .../CustomSwaggerSchemaConverter.scala        |  65 +++++++++
 5 files changed, 322 insertions(+), 5 deletions(-)
 create mode 100644 src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala
 create mode 100644 src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala
 create mode 100644 src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala

diff --git a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
index 8c16519..57e5517 100644
--- a/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
+++ b/src/main/java/com/knappsack/swagger4springweb/parser/ApiOperationParser.java
@@ -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;
@@ -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 {
@@ -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 {
diff --git a/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java b/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java
index 6f5e5b5..96e8727 100644
--- a/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java
+++ b/src/main/java/com/knappsack/swagger4springweb/util/ModelUtils.java
@@ -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;
@@ -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();
diff --git a/src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala b/src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala
new file mode 100644
index 0000000..6aba150
--- /dev/null
+++ b/src/main/scala/com/wordnik/swagger/converter/CustomModelConverters.scala
@@ -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
+  }
+}
diff --git a/src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala b/src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala
new file mode 100644
index 0000000..b001791
--- /dev/null
+++ b/src/main/scala/com/wordnik/swagger/converter/CustomModelPropertyParser.scala
@@ -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
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala b/src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala
new file mode 100644
index 0000000..7f30882
--- /dev/null
+++ b/src/main/scala/com/wordnik/swagger/converter/CustomSwaggerSchemaConverter.scala
@@ -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
+          ))
+        }
+      }
+    })
+  }
+}
\ No newline at end of file