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 = ModelConverters.read(responseClass); + Option 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 = ModelConverters.read(type); + Option 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 models) { - scala.collection.immutable.List modelOption = ModelConverters.readAll(clazz); + scala.collection.immutable.List modelOption = CustomModelConverters.readAll(clazz); scala.collection.Iterator 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