All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.fasterxml.jackson.module.scala.introspect.BeanIntrospector.scala Maven / Gradle / Ivy

The newest version!
/*
 * Derived from source code of scalabeans:
 * https://raw.github.com/scalastuff/scalabeans/62b50c4e2482cbc1f494e0ac5c6c54fadc1bbcdd/src/main/scala/org/scalastuff/scalabeans/BeanIntrospector.scala
 *
 * The scalabeans code is covered by the copyright statement that follows.
 */

/*
 * Copyright (c) 2011 ScalaStuff.org (joint venture of Alexander Dvorkovyy and Ruud Diterwich)
 *
 *    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.fasterxml.jackson.module.scala.introspect

import java.lang.reflect.{Constructor, Field, Method, Modifier}
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.module.scala.util.ClassW

import scala.annotation.tailrec
import scala.reflect.NameTransformer

//TODO: This might be more efficient/type safe if we used Scala reflection here
//but we have to support 2.9.x and 2.10.x - once the scala reflection APIs
//settle down, it might be a good idea. Using java.lang.reflect.* means we have to
//suffer with type erasure, so we can only compare gross types of arguments and return values

object BeanIntrospector {

  def apply[T <: AnyRef](cls: Class[_]) = {

    /**
     * Find the index of the named parameter in the primary constructor of the mentioned class.
     */
    @tailrec
    def findConstructorParam(c: Class[_], name: String): Option[ConstructorParameter] = {
      //c can be null if we're asked for the superclass of Object or AnyRef
      if (c == null || c == classOf[AnyRef]) {
        None
      } else {
        //try to use constructor with most parameters to avoid https://github.com/FasterXML/jackson-module-scala/issues/330
        val primaryConstructor = c.getConstructors.sortBy(c => (-c.getParameterCount, c.toString)).headOption
        val debugCtorParamNames = primaryConstructor.toIndexedSeq.flatMap(getCtorParams)
        val index = debugCtorParamNames.indexOf(name)
        val companion = findCompanionObject(c)
        if (index >= 0) {
          Some(ConstructorParameter(primaryConstructor.get, index, findConstructorDefaultValue(companion, index)))
        } else {
          findConstructorParam(c.getSuperclass, name)
        }
      }
    }

    def findConstructorDefaultValue(maybeCompanion: Option[AnyRef], index: Int): Option[() => AnyRef] = {
      val methodName = "$lessinit$greater$default$" + (index + 1)
      maybeCompanion.flatMap(companion => companion.getClass.getMethods.collectFirst {
        case method if method.getName == methodName && method.getParameterTypes.length == 0 =>
          () => method.invoke(companion)
      })
    }

    def findCompanionObject(c: Class[_]): Option[AnyRef] = {
      try {
        Some(c.getClassLoader.loadClass(c.getName + "$").getDeclaredField("MODULE$").get(null))
      } catch {
        case e: Exception => None
      }
    }

    val hierarchy: Seq[Class[_]] = {
      @tailrec
      def next(c: Class[_], acc: List[Class[_]]): List[Class[_]] = {
        if (c == null || c == classOf[AnyRef]) {
          acc
        } else {
          next(c.getSuperclass, acc :+ c)
        }
      }
      next(cls, Nil)
    }

    def listMethods(cls: Class[_]): LazyListSupport.LazyListType[Method] = cls match {
      case null => LazyListSupport.empty
      case c if c == classOf[AnyRef] => LazyListSupport.empty
      case c => LazyListSupport.fromArray(c.getDeclaredMethods) #::: listMethods(c.getSuperclass)
    }

    def isNotSyntheticOrBridge(m: Method): Boolean = !(m.isBridge || m.isSynthetic)

    def findMethod(cls: Class[_], name: String): Seq[Method] =
      listMethods(cls).filter(isNotSyntheticOrBridge).filter(m => NameTransformer.decode(m.getName) == name)

    def listFields(cls: Class[_]): LazyListSupport.LazyListType[Field] = cls match {
      case null => LazyListSupport.empty
      case c if c == classOf[AnyRef] => LazyListSupport.empty
      case c => LazyListSupport.fromArray(c.getDeclaredFields) #::: listFields(c.getSuperclass)
    }

    def findField(cls: Class[_], fieldName: String): Option[Field] =
      listFields(cls).filter(!_.isSynthetic).find(f => NameTransformer.decode(f.getName) == fieldName).headOption

    def isAcceptableField(field: Field): Boolean = {
      //TODO: check for private/public? Do we care?
      val modifiers = field.getModifiers
      !(Modifier.isStatic(modifiers) || Modifier.isVolatile(modifiers) || Modifier.isTransient(modifiers) || field.isSynthetic)
    }

    //ignore static methods - we can actually have one of these if a class
    //inherits from a java class
    def isAcceptableMethod(method: Method): Boolean = {
      //can't be static, a bridge or synthetic method (which are generated by the compiler)
      !Modifier.isStatic(method.getModifiers) && isNotSyntheticOrBridge(method)
    }

    def findGetter(cls: Class[_], propertyName: String): Option[Method] = {
      findMethod(cls, propertyName).find(isAcceptableGetter)
    }

    def findBeanGetter(cls: Class[_], propertyName: String): Option[Method] = {
      findMethod(cls, "get" + propertyName.capitalize).find(isAcceptableGetter)
    }

    //True if the method fits the 'getter' pattern
    def isAcceptableGetter(m: Method): Boolean = {
      isAcceptableMethod(m) && m.getParameterTypes.length == 0 && m.getReturnType != Void.TYPE
    }

    def findSetter(cls: Class[_], propertyName: String): Option[Method] = {
      findMethod(cls, propertyName + "_=").find(isAcceptableSetter)
    }

    def findBeanSetter(cls: Class[_], propertyName: String): Option[Method] = {
      findMethod(cls, "set" + propertyName.capitalize).find(isAcceptableSetter)
    }

    //True if the method fits the 'setter' pattern
    def isAcceptableSetter(m: Method): Boolean = {
      isAcceptableMethod(m) && m.getParameterTypes.length == 1 && m.getReturnType == Void.TYPE
    }

    val privateRegex = """(.*)\$\$(.*)""".r
    def maybePrivateName(field: Field): String = {
      val definedName = NameTransformer.decode(field.getName)

      val canonicalName = try {
        Option(field.getDeclaringClass.getCanonicalName)
      } catch {
        // This gets thrown in the REPL
        case e: InternalError => None
      }
      canonicalName.flatMap { cn =>
        val PrivateName = cn.replace('.','$')
        definedName match {
          case privateRegex(PrivateName, rest) => Some(rest)
          case _ => None
        }
      }.getOrElse(definedName)

    }

    def isScalaCaseObject(cls: Class[_]): Boolean = {
      ClassW.productClass.isAssignableFrom(cls) && cls.getName.endsWith("$")
    }

    //TODO - as we walk the classes, it would be nice to use a language specific introspector
    //for example, a scala class can inherit from a java class - when we're examining
    //a scala class, we should use the behavior defined here, but if the base class is a Java
    //class we should use the default JavaBean rules for determining fields, getters and setters.

    //TODO - this ignores the possibility that the types may disagree between the field, the getter and the setter.
    //Theoretically, the getter should have an identical return type as the field, and the setter should take
    //one argument of the same type as the field

    //create properties for all appropriate fields
    val fields = for {
      cls <- hierarchy
      field <- cls.getDeclaredFields
      isScalaObject = ClassW(cls).isScalaObject || isScalaCaseObject(cls)
      name = maybePrivateName(field)
      if !name.contains('$')
      if isScalaObject || isAcceptableField(field)
      beanGetter = findBeanGetter(cls, name)
      beanSetter = findBeanSetter(cls, name)
    } yield PropertyDescriptor(name, findConstructorParam(hierarchy.head, name), Some(field), findGetter(cls, name), findSetter(cls, name), beanGetter, beanSetter)

    //this will create properties for all methods with a non-Unit/Void return type and no arguments
    //that also have a setter present that matches the pattern 'propertyName'+'_='.
    //We also require that there is no field defined on the class matching the param name -
    //the assumption is that if we have made it this far, the field/getter/setter combo would have been
    //picked up if the field was valid. If we have not already added the field, and it's present, then
    //it must be somehow inappropriate (static, volatile, etc)
    //this catches the case where people explicitly write getter-like and setter-like methods
    //(for example, compound properties)
    //Note - this will discard 'dangling' getters as an introspected property - getters must have setters,
    //and also the reverse.
    //TODO: Ensure the getters and setters with matching names also have matching types
    val methods = for {
      cls <- hierarchy
      getter <- cls.getDeclaredMethods
      name = NameTransformer.decode(getter.getName)
      if isAcceptableGetter(getter)
      if findField(cls, name).isEmpty
      if !name.contains('$')
      if !fields.exists(_.name == name)
      getterProperty = Option(getter.getAnnotation(classOf[JsonProperty]))
      setter = findSetter(cls, name) if setter.isDefined || (getterProperty.isDefined && getterProperty.get.value != JsonProperty.USE_DEFAULT_NAME)
      beanGetter = findBeanGetter(cls, name)
      beanSetter = findBeanSetter(cls, name)
    } yield PropertyDescriptor(name, None, None, Some(getter), setter, beanGetter, beanSetter)

    val lazySuffix = "$lzy1"
    val lazyValMethods = {
      hierarchy.flatMap { hcls =>
        val fieldNames = hcls.getDeclaredFields.map(_.getName)
          .filter(_.endsWith(lazySuffix))
          .map(s => s.substring(0, s.length - lazySuffix.length))
          .toSet
        for {
          getter <- hcls.getDeclaredMethods
          name = NameTransformer.decode(getter.getName)
          if !name.contains('$')
          if fieldNames.contains(name)
          if isAcceptableGetter(getter)
          setter = findSetter(cls, name)
          beanGetter = findBeanGetter(cls, name)
          beanSetter = findBeanSetter(cls, name)
        } yield PropertyDescriptor(name, None, None, Some(getter), setter, beanGetter, beanSetter)
      }
    }

    BeanDescriptor(cls, fields ++ methods ++ lazyValMethods)
  }

  private def getCtorParams(ctor: Constructor[_]): Seq[String] = {
    val names = JavaParameterIntrospector.getCtorParamNames(ctor)
    names.map(NameTransformer.decode)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy