
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.12 Show documentation
Show all versions of classutil_2.12 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