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

com.dimajix.util.Reflection.scala Maven / Gradle / Ivy

There is a newer version: 1.2.0-synapse3.3-spark3.3-hadoop3.3
Show newest version
/*
 * Copyright (C) 2018 The Flowman Authors
 *
 * 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.dimajix.util

import java.lang.reflect.Constructor
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
import java.lang.reflect.Parameter


object Reflection {
    private val OPTION_TYPE = classOf[Option[_]]
    private val SEQ_TYPE = classOf[Seq[_]]
    private val SET_TYPE = classOf[Set[_]]
    private val MAP_TYPE = classOf[Map[_,_]]

    def companion[T](name : String)(implicit man: Manifest[T]) : Option[T] = {
        try {
            Some(Class.forName(name + "$").getField("MODULE$").get(man.runtimeClass).asInstanceOf[T])
        }
        catch {
            case _:ClassNotFoundException => None
        }
    }

    /**
      * Invokes the "copy" method from case classes with the given optional parameter map. Helps to bridge binary
      * incompatible Spark libraries, where some case classes have more fields and therefore the ABI does not match
      * any more.
      * @param src
      * @param args
      * @tparam T
      * @return
      */
    def copy[T <: AnyRef](src:T, args:Map[String,Any]) : T = {
        val clazz = src.getClass
        val argNames = args.keySet
        val allCopies = clazz.getDeclaredMethods
            .filter(_.getName == "copy")

        def isCandidate(con: Method): Boolean = {
            val paramNames = con.getParameters.map(_.getName).toSet
            argNames.forall(arg => paramNames.contains(arg))
        }
        def createParameter(param:Parameter) : AnyRef = {
            val paramName = param.getName
            if (args.contains(paramName)) {
                toAnyRef(args(paramName), param)
            }
            else {
                val getter = clazz.getDeclaredMethod(paramName)
                getter.invoke(src)
            }
        }
        def tryCopy(copy: Method): Option[T] = {
            val params = copy.getParameters
            try {
                val pvals = params.map(createParameter)
                Some(copy.invoke(src, pvals:_*).asInstanceOf[T])
            }
            catch {
                case ex:InvocationTargetException => throw ex.getTargetException
                case _: Throwable => None
            }
        }

        val sortedConstructors = allCopies
            .filter(isCandidate)
            .sortBy(_.getParameters.length)

        for (c <- sortedConstructors) {
            tryCopy(c) match {
                case Some(i) => return i
                case None =>
            }
        }

        throw new NoSuchMethodError(s"Class ${clazz.getName} does not provide a copy method")
    }

    /**
     * This function will try to instantiate a specific class with the given arguments. The method is clever and
     * tries to fill in missing parameters with default values, like an empty Map for a Map parameter or None for
     * an Option. This is not possible for fundamental data types like ints, booleans etc. For missing parameters
     * of other types, the code will reccursively try to construct a default instance of the corresponding type with
     * the same rules
     * @param clazz
     * @param args
     * @tparam T
     * @return
     */
    def construct[T <: AnyRef](clazz:Class[T], args:Map[String,Any])(implicit man: Manifest[T]) : T = {
        val argNames = args.keySet
        val allConstructors = clazz.getDeclaredConstructors()

        def isCandidate(con:Constructor[_]) : Boolean = {
            val paramNames = con.getParameters.map(_.getName).toSet
            argNames.forall(arg => paramNames.contains(arg))
        }
        def tryConstruct(con:Constructor[_]) : Option[T] = {
            val params = con.getParameters
            try {
                val pvals = params.map(p => createParameterOrDefault(p, args))
                Some(con.newInstance(pvals:_*).asInstanceOf[T])
            }
            catch {
                case ex:InvocationTargetException => throw ex.getTargetException
                case _:Throwable => None
            }
        }

        val sortedConstructors = allConstructors
            .filter(isCandidate)
            .sortBy(_.getParameters.length)

        for (c <- sortedConstructors) {
            tryConstruct(c) match {
                case Some(i) => return i
                case None =>
            }
        }

        try {
            apply[T](clazz.getName, args)
        }
        catch {
            case _:NoSuchMethodError =>
                throw new NoSuchMethodError(s"Cannot construct instance of ${clazz.getName} with parameters (${args.map(kv => kv._1 + ":" + kv._2.getClass.getName).mkString(",")})")
        }
    }

    def apply[T <: AnyRef](clazzName:String, args:Map[String,Any])(implicit man: Manifest[T]) : T = {
        val companion =
            try {
                Class.forName(clazzName + "$").getField("MODULE$").get(man.runtimeClass).asInstanceOf[T]
            }
            catch {
                case _:ClassNotFoundException => throw new NoSuchMethodError(s"No companion object found for class $clazzName")
            }
        invoke[T,T](companion, "apply", man.runtimeClass.asInstanceOf[Class[T]], args)
    }

    /**
      * Invokes the given method name
      * @param src
      * @param methodName
      * @param rc
      * @param args
      * @tparam T
      * @tparam R
      * @return
      */
    def invoke[T <: AnyRef,R](src:T, methodName:String, rc:Class[R],args:Map[String,Any]) : R = {
        val argNames = args.keySet
        val clazz = src.getClass

        // Only allow methods with correct name and where all parameters can be fullfilled
        def score(method: Method): Int = {
            val paramNames = method.getParameters.map(_.getName).toSet
            paramNames.intersect(argNames).size - argNames.diff(paramNames).size - paramNames.diff(argNames).size
        }

        val superClazz = if (clazz.getSuperclass != classOf[Object]) Seq(clazz.getSuperclass) else Seq.empty[Class[_]]
        val allClazzes = Seq(clazz) ++ superClazz ++ clazz.getInterfaces.toSeq
        val method = allClazzes.flatMap(_.getDeclaredMethods())
            .filter(_.getName == methodName)
            .sortBy(m => score(m))
            .lastOption
            .getOrElse(throw new NoSuchMethodError(s"No appropriate method '$methodName' found in class ${clazz.getName}"))


        val params = method.getParameters
        val pvals = params.map(p => createParameterOrDefault(p, args))
        try {
            method.invoke(src, pvals: _*).asInstanceOf[R]
        }
        catch {
            case ex:InvocationTargetException =>
                throw ex.getTargetException
        }
    }

    private def createParameterOrDefault(param: Parameter, args: Map[String, Any]): AnyRef = {
        if (args.contains(param.getName)) {
            toAnyRef(args(param.getName), param)
        }
        else {
            param.getType() match {
                case OPTION_TYPE => None
                case SEQ_TYPE => Seq.empty
                case SET_TYPE => Set.empty
                case MAP_TYPE => Map.empty
                case other => construct(other.asInstanceOf[Class[AnyRef]], Map.empty)
            }
        }
    }

    private def toAnyRef(value:Any, param:Parameter) : AnyRef = {
        value match {
            case s: Short => java.lang.Short.valueOf(s)
            case i: Int => Integer.valueOf(i)
            case l: Long => java.lang.Long.valueOf(l)
            case f: Float => java.lang.Float.valueOf(f)
            case d: Double => java.lang.Double.valueOf(d)
            case b: Boolean => java.lang.Boolean.valueOf(b)
            case r: AnyRef => r
            case _ => throw new UnsupportedOperationException(s"Parameter '${param.getName}' has unsupported type")
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy