org.clapper.classutil.ScalaObjectToBean.scala Maven / Gradle / Ivy
                 Go to download
                
        
                    Show more of this group  Show more artifacts with this name
Show all versions of classutil_2.10 Show documentation
                Show all versions of classutil_2.10 Show documentation
A library for fast runtime class-querying, and more
                
             The newest version!
        
        package org.clapper.classutil
import java.lang.reflect.{InvocationHandler, Method, Proxy}
import scala.language.existentials
/** Contains the actual logic that maps a Scala object to a Java bean.
  *
  * See [[org.clapper.classutil.ScalaObjectToBean]] for documentation.
  */
private[classutil] class ScalaObjectToBeanMapper {
  private val NameGenerator = new ClassNameGenerator {
    val ClassNamePrefix = "org.clapper.classutil.ScalaObjectBean"
  }
  /** Wrap a Scala object in a bean.
    *
    * @param obj       the Scala object
    * @param recurse   `true` to recursively map nested Scala objects,
    *                  `false` otherwise
    *
    * @return an instantiated bean representing the augmented Scala object,
    *         subject to the restrictions listed in the class documentation.
    */
  def wrapInBean(obj: Any, recurse: Boolean): AnyRef =
    wrapInBean(obj, NameGenerator.newGeneratedClassName, recurse, None)
  /** Wrap a Scala object in a bean.
    *
    * @param obj        the Scala object
    * @param className  the name to give the class
    * @param recurse    `true` to recursively map nested Scala objects,
    *                   `false` otherwise
    * @param cb         If defined, this parameter is a function that is
    *                   invoked with the result of any method call, along with
    *                   the called method's name. It can be used to transform
    *                   the result before the result is returned.
    *
    * @return an instantiated bean representing the augmented Scala object,
    *         subject to the restrictions listed in the class documentation.
    */
  def wrapInBean(obj:       Any,
                 className: String,
                 recurse:   Boolean,
                 cb:        Option[(String, AnyRef) => AnyRef]): AnyRef = {
    // Get the set of bean methods, and create a map of names to methods.
    val cls = obj.asInstanceOf[AnyRef].getClass
    val beanMethodMap = methodsForBean(cls)
    if (beanMethodMap.isEmpty) {
      // No mappable methods. Just use the original object.
      obj.asInstanceOf[AnyRef]
    }
    else {
      // methodMap is now a map of bean method names to Method
      // objects. Convert it to a sequence of (bean-name,
      // return-value) tuples, to generate the interface.
      generateBean(obj         = obj,
                   methodMap   = beanMethodMap,
                   className   = className,
                   classLoader = this.getClass.getClassLoader,
                   recurse     = recurse,
                   cb          = cb)
    }
  }
  /** Get a list of methods that should be generated for a bean.
    *
    * @param cls  the class to query
    *
    * @return a map of `(methodName -> method)` pairs
    */
  def methodsForBean(cls: Class[_]): Map[String, Method] = {
    val settersGetters = ClassUtil.scalaAccessorMethods(cls)
    val pubNotFinal = ClassUtil.nonFinalPublicMethods(cls)
    ((settersGetters ++ pubNotFinal).map { m => m.getName -> m } ++
     settersGetters.map { m => ClassUtil.beanName(m) -> m }).toMap
  }
  /** Generate a Java Bean-compliant interface for a class. All Scala getters
    * and setters are represented in the interface as both their Scala methods
    * and the Java Bean equivalents. All other methods in the class appear,
    * as is, in the interface.
    *
    * Generating an implementation for the interface (e.g., by using a
    * `java.lang.reflect.Proxy`) is the responsibility of the caller.
    *
    * @param cls         the class for which to generate the interface
    * @param ifaceName   the name to give the interface. If not specified,
    *                    one is generated.
    * @param methodMap   A map of (name -> method) pairs describing the
    *                    methods to generate/wrap. If empty (the default),
    *                    most existing public methods are represented in the
    *                    interface, and new Java getters are setters are added
    *                    as necessary. (Note that some public methods, notably
    *                    some methods generated by the `Proxy` code, are
    *                    suppressed, no matter what.)
    * @param classLoader The class loader to use. Defaults to the class
    *                    loader for this class.
    * @param recurse     Whether or not method results should also be wrapped
    *                    as beans. Defaults to true.
    *
    * @return the generated interface, which is loaded using the specified
    *         class loader.
    *
    * @see [[generateBean]]
    */
  def generateBeanInterface(
    cls:         Class[_],
    ifaceName:   String = "",
    methodMap:   Map[String, Method] = Map.empty[String, Method],
    classLoader: ClassLoader = getClass.getClassLoader,
    recurse:     Boolean = true
  ): Class[_] = {
    import asm.InterfaceMaker
    def returnTypeFor(method: Method) = {
      if (recurse && shouldWrapReturnType(method.getReturnType))
        classOf[Any]
      else
        method.getReturnType
    }
    val className = cls.getName
    val newName = if (ifaceName.isEmpty)
      NameGenerator.newGeneratedClassName
    else
      ifaceName
    val adjMethodMap = if (methodMap.isEmpty) {
      val allMethods = ClassUtil
        .nonFinalPublicMethods(cls)
        .map { m => m.getName -> m }
      val beanMethods = ClassUtil
        .scalaAccessorMethods(cls)
        .map { m => ClassUtil.beanName(m) -> m }
      (allMethods ++ beanMethods).toMap
    }
    else
      methodMap
    // No matter what, hide methods that should be hidden.
    val methods = adjMethodMap
      .filter { case (name, _) => !Util.hideMethod(name) }
    val methodSeq = methods.map { case (name, method) =>
      (name, method.getParameterTypes, returnTypeFor(method))
    }.toSeq
    val interfaceBytes = InterfaceMaker.makeInterface(methodSeq, newName)
    // Load the class we just generated.
    ClassUtil.loadClass(classLoader, newName, interfaceBytes)
  }
  /** Generate a bean from an object. This method creates an on-the-fly
    * interface containing Java Bean methods and uses that interface to
    * create a `java.lang.reflect.Proxy` to handle the calls. If you just
    * want the interface, call [[generateBeanInterface]].
    *
    * @param obj         the object to wrap
    * @param methodMap   A map of (name -> method) pairs describing the
    *                    methods to generate/wrap. If empty (the default),
    *                    all existing public methods are represented in the
    *                    interface, and new Java getters are setters are added
    *                    as necessary.
    * @param className   The class name to use. If not specified, a random
    *                    one is generated.
    * @param classLoader The class loader to use. If not specified, this class's
    *                    class loader is used.
    * @param recurse     Whether or not method results should also be wrapped
    *                    as beans. Defaults to `true`.
    * @param cb          If defined, this parameter is a function that is
    *                    invoked with the result of any method call, along with
    *                    the called method's name. It can be used to transform
    *                    the result before the result is returned.
    *
    * @return the generated bean
    */
  def generateBean(
    obj:         Any,
    methodMap:   Map[String, Method],
    className:   String = "",
    classLoader: ClassLoader = getClass.getClassLoader,
    recurse:     Boolean = true,
    cb:    Option[(String, AnyRef) => AnyRef] = None): AnyRef = {
    def functionFor(method: Method): (Array[Object] => AnyRef) = {
      val o = obj.asInstanceOf[AnyRef]
      if (recurse && shouldWrapReturnType(method.getReturnType)) {
        // Return a function which, when invoked, will call the method and
        // wrap the result in a bean wrapper.
        a: Array[Object] =>
          wrapInBean(call(o, method, a), recurse = true)
      }
      else {
        // Return a function which, when invoked with an Option of arguments,
        // will call the method, returning the result directly.
        a: Array[Object] => call(o, method, a)
      }
    }
    val actualMethodMap = methodMap.map { case (name, meth) =>
      name -> functionFor(meth)
    }
    val interface = generateBeanInterface(cls         = obj.getClass,
                                          ifaceName   = className,
                                          methodMap   = methodMap,
                                          classLoader = classLoader,
                                          recurse     = recurse)
    // Create a proxy that satisfies its calls from the original
    // object's set of methods.
    makeProxy(methods     = actualMethodMap,
              obj         = obj,
              interface   = interface,
              classLoader = classLoader,
              cb          = cb)
  }
  //--------------------------------------------------------------------------
  // Private methods
  //--------------------------------------------------------------------------
  /** Determine whether a return type should be wrapped as a bean.
    *
    * @param returnType the return type
    *
    * @return `true` or `false`
    */
  private def shouldWrapReturnType(returnType: Class[_]): Boolean = {
    (!ClassUtil.isPrimitive(returnType)) &&
      (returnType ne classOf[String])
  }
  /** Invoke a method on an object, optionally passing it arguments.
    */
  private def call(obj: AnyRef, method: Method, args: Array[Object]): AnyRef = {
    if (args.isEmpty)
      method.invoke(obj)
    else
      method.invoke(obj, args: _*)
  }
  /** Create the proxy object.
    *
    * @param methods     A map of method names to handler functions. If there
    *                    is not handler function for a method, the value
    *                    associated with the method should be None.
    * @param obj         The object to which to delegate the method calls
    * @param interface   The generated interface class to be implemented
    * @param classLoader The class loader to use
    * @param cb          If defined, this parameter is a function that is
    *                    invoked with the result of any method call, along with
    *                    the called method's name. It can be used to transform
    *                    the result before the result is returned.
    *
    * @return the proxy instance
    */
  private def makeProxy(
    methods:     Map[String, Array[Object] => AnyRef],
    obj:         Any,
    interface:   Class[_],
    classLoader: ClassLoader,
    cb:    Option[(String, AnyRef) => AnyRef] = None): AnyRef = {
    val handler = new InvocationHandler {
      def invoke(proxy: Object,
                 method: Method,
                 args:   Array[Object]): Object =  {
        // It could be an invocation of a method that isn't one we
        // generated. In that case, just delegate the call to the
        // original object.
        def delegate(args: Array[Object]): AnyRef =
          call(obj.asInstanceOf[AnyRef], method, args)
        val methodName = method.getName
        val func = methods.getOrElse(methodName, delegate _)
        val res = if (method.getParameterTypes.isEmpty)
          func(Array.empty[AnyRef])
        else
          func(args)
        cb.map { f => f(methodName, res) }.getOrElse(res)
      }
    }
    Proxy.newProxyInstance(classLoader, Array(interface), handler)
  }
}
/**
  * `ScalaObjectToBean` contains functions that allow you to map a Scala object
  * into a read-only Java bean or merely generate an interface for such a bean.
  * The functions take a Scala object or class (depending), locate the Scala
  * accessors (using simple heuristics defined in [[ClassUtil]]), and generate
  * a new interface or object with additional Java Bean get` and `set` methods
  * for the accessors.
  *
  * This kind of wrapping is an alternative to using the `@BeanProperty`
  * annotation on classes, so it is useful for mapping case classes into Java
  * Beans, or for mapping classes from other APIs into Java Beans without having
  * to extend them.
  *
  * `ScalaObjectToBean` uses the following heuristics to determine which fields
  * to map.
  *
  * First, it recognizes that any Scala `val` or `var` is really a getter method
  * returning some type. That is:
  *
  * {{{
  * val x: Int = 0
  * var y: Int = 10
  * }}}
  *
  * is compiled down to the equivalent of the following Java code:
  *
  * {{{
  * private int _x = 0;
  * private int _y = 10;
 *
  * public int x() { return _x; }
  * public int y() { return _y; }
  * public void y_\$eq(int newY) { _y = newY; }
  * }}}
 *
  * So, the mapper looks for Scala getter methods that take no parameters
  * and return some non-void (i.e., non-`Unit`) value, and it looks for
  * Scala setter methods that take one parameter, return void (`Unit`) and
  * have names ending in "_\$eq". Then, from that set of methods, the mapper
  * discards:
  *
  * 
  * -  Methods starting with "get"
  * -  Methods that have a corresponding "get" method. In the above example,
  *        if there's a `getX()` method that returns an `int`, the mapper will
  *        assume that it's the bean version of `x()`, and it will ignore `x()`.
  * -  Methods that aren't public.
  * -  Any method in `java.lang.Object`.
  * -  Any method in `scala.Product`.
  * 
  *
  * If there are any methods in the remaining set, then the mapper returns a
  * new wrapper object that contains Java Bean versions of those methods;
  * otherwise, the mapper returns the original Scala object. The resulting
  * bean delegates its calls to the original object, instead of capturing the
  * object's method values at the time the bean is called. That way, if the
  * underlying Scala object's methods return different values for each call,
  * the bean will reflect those changes.
  */
object ScalaObjectToBean {
  private val mapper = new ScalaObjectToBeanMapper
  /** Transform an object into an object. The class name will be generated,
    * will be in the `org.clapper.classutil` package, and will have
    * a class name prefix of `ScalaObjectBean_`.
    *
    * @param obj       the Scala object
    * @param recurse   `true` to recursively map nested maps, `false` otherwise
    *
    * @return an instantiated object representing the map
    */
  def apply(obj: Any, recurse: Boolean = true): AnyRef =
    mapper.wrapInBean(obj, recurse)
  /** Transform an object into an object. The class name will be generated,
    * will be in the `org.clapper.classutil` package, and will have
    * a class name prefix of `ScalaObjectToBean_`.
    *
    * @param obj       the Scala object
    * @param className the desired class name
    * @param recurse   `true` to recursively map nested maps, `false`
    *                  otherwise. Recursively mapped maps will have generated
    *                  class names.
    *
    * @return an instantiated object representing the map
    */
  def apply(obj: Any, className: String, recurse: Boolean): AnyRef =
    mapper.wrapInBean(obj, className, recurse, None)
  /** Transform an object into an object. The class name will be generated,
    * will be in the `org.clapper.classutil` package, and will have
    * a class name prefix of `ScalaObjectToBean_`. This version of the call
    * allows you to intercept each method call and possibly transform the
    * results.
    *
    * Note that the `cb` parameter will be receiving an `AnyRef` result
    * (i.e., `java.lang.Object`). That means any Scala primitives are really
    * boxed Java values. In addition, it's important that the method return an
    * object. For instance, consider wrapping the following class in a bean:
    *
    * {{{
    * case class Foo(x: Int, y: Double)
    * }}}
    *
    * To post-process a bean generated from that class, you might use code
    * like the following:
    *
    * {{{
    * val bean = ScalaObjectToBean.withResultMapper(Foo(10, 20.0)) { (name, res) =>
    *   (name, res) match {
    *     case ("getX", n: java.lang.Integer) => new Integer(n * 100)
    *     case ("getY", d: java.lang.Double)  => new java.lang.Double(d / 10.0)
    *     case _                              => res
    *   }
    * }
    * }}}
    *
    * By contrast, the following will not compile, because (a) it attempts to
    * pattern match against `Int`, and (b) it attempts to return a Scala
    * `Double`.
    *
    * {{{
    * val bean = ScalaObjectToBean.withResultMapper(Foo(10, 20.0)) { (name, res) =>
    *   (name, res) match {
    *     case ("getX", n: Int) => new Integer(n * 100)
    *     //               ^ pattern type is incompatible with expected type
    *
    *     case ("getY", d: java.lang.Double) => d / 10.0
    *     //                                      ^ the result type of an
    *     //                                        implicit conversaion must
    *     //                                        be more specific than AnyRef
    *     case _ => res
    *   }
    * }
    * }}}
    *
    * Note, too, that if `recurse` is `true` (the default), the result value
    * passed to your post-call function will be a `java.lang.reflect.Proxy` for
    * any non-primitive, non-String object.
    *
    * {{{
    * case class Foo(i: Int)
    * case class Bar(name: String, foo: Foo)
    * val bean = ScalaObjectToBean(Bar("quux", Foo(10)) { (name, res) =>
    *   (name, res) match {
    *     case ("getName", n) => // n will be a String
    *     case ("getFoo", f)  => // f will be a java.lang.reflect.Proxy
    *     case ("name",   n)  => // n will be a String
    *     case ("foo",    f)  => // f will be a java.lang.reflect.Proxy
    *   }
    * }
    * }}}
    *
    *
    * @param obj       the Scala object
    * @param className the desired class name, or an empty string to generate
    *                  a default. (Empty string is the default.)
    * @param recurse   `true` to recursively map nested maps, `false`
    *                  otherwise. Recursively mapped maps will have generated
    *                  class names. Defaults to `false`.
    * @param cb        If defined, this parameter is a function that is
    *                  invoked with the result of any method call, along with
    *                  the called method's name. It can be used to transform
    *                  the result before the result is returned.
    *
    * @return an instantiated object representing the map
    */
  def withResultMapper(obj: Any, className: String = "", recurse: Boolean = true)
                      (cb: (String, AnyRef) => AnyRef): AnyRef = {
    mapper.wrapInBean(obj, className, recurse, Some(cb))
  }
}
    © 2015 - 2025 Weber Informatics LLC | Privacy Policy