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

org.clapper.classutil.ScalaObjectToBean.scala Maven / Gradle / Ivy

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