Search code examples
scalaplayframeworkclasscastexceptionfor-comprehension

ClassCastException when asInstanceOf in for comprehension


In PlayFramework 2.4 I try to transform all controllers methods into JavaScript routes.

val jsRoutesClass = classOf[routes.javascript]
val controllers = jsRoutesClass.getFields.map(_.get(null))

for (
  controller <- controllers;
  method <- controller.getClass.getDeclaredMethods
) yield method.invoke(controller).asInstanceOf[JavaScriptReverseRoute]

But the following error occur:

Error injecting constructor, java.lang.ClassCastException: java.lang.String cannot be cast to play.api.routing.JavaScriptReverseRoute 
at controllers.Application.<init>(Application.scala:21)
while locating controllers.Application
   for parameter 1 at router.Routes.<init>(Routes.scala:35)
while locating router.Routes
while locating play.api.inject.RoutesProvider
while locating play.api.routing.Router

I add some code but I think that is unnecessary code. After that an exception doesn't occur.

for (
  controller <- controllers;
  method <- controller.getClass.getDeclaredMethods;
  action <- method.invoke(controller).toString
) yield method.invoke(controller).asInstanceOf[JavaScriptReverseRoute]

Why does the error occur in the first code sample and not in the second one?


Solution

  • Let's look at the code step by step and look what each line yields. I converted your code to a complete sample code, but I hope I captured the essence of your code.

    package controllers
    
    import play.api._
    import play.api.mvc._
    
    class Sample extends Controller {
      def hello(name: String) = Action {
        implicit req =>
        import routes.javascript._
    
        val jsRoutesClass = classOf[routes.javascript]
        val controllers = jsRoutesClass.getFields.map(_.get(null))
        val met = for (
            controller <- controllers;
            method <- controller.getClass.getDeclaredMethods
          ) yield method
        Ok(met.mkString(", "))
      }
    }
    

    When executing this request you will see something like

    public play.api.routing.JavaScriptReverseRoute controllers.javascript.ReverseSample.hello(), public java.lang.String controllers.javascript.ReverseSample._defaultPrefix()
    

    You should find all your methods from you're routes but note there is also the _defaultPrefix() method with the return type String.

    This is the reason your first code sample doesn't work. One of the methods does not return a JavaScriptReverseRoute and therefore throws an exception.

    This still does not explain why your second code example doesn't work. So let's add some code to our Sample Controller:

    package controllers
    
    import play.api._
    import play.api.mvc._
    
    class Sample extends Controller {
      def hello(name: String) = Action {
        implicit req =>
        import routes.javascript._
    
        val jsRoutesClass = classOf[routes.javascript]
        val controllers = jsRoutesClass.getFields.map(_.get(null))
        val met = for (
            controller <- controllers;
            method <- controller.getClass.getDeclaredMethods
          ) yield method.invoke(controller)
        Ok(met.mkString(", "))
      }
    }
    

    Note that we do not cast the result of the method invocation yet and the request yields something like:

    JavaScriptReverseRoute(controllers.Sample.hello,
        function(name) {
          return _wA({method:"GET", url:"/" + (function(k,v) {return v})("name", encodeURIComponent(name))})
        }
      ), 
    

    Look carefully and you will see that there is a rogue , at the end meaning that the value of our temporary val met is at position 0 a JavaScriptReverseRoute and at postion 1 and empty string.

    Therefore looking at your workaround the action action <- method.invoke(controller).toString is a string with the javascript one time and the other time it is an empty String. Since we are in a for comprehension the String is automatically converted to an array of chars and the yield block is not executed if this array is empty.

    The problem with your workaround is that if the _.defaultPrefix() ever yields a string it will again throw a class cast exception.

    A better solution would be to filter each method which has not one of your excepted result types like so:

    package controllers
    
    import play.api._
    import play.api.mvc._
    
    class Sample extends Controller {
      def hello(name: String) = Action {
        implicit req =>
          import routes.javascript._
    
          val jsRoutesClass = classOf[routes.javascript]
          val controllers = jsRoutesClass.getFields.map(_.get(null))
          val met = for (
              controller <- controllers;
              method <- controller.getClass.getDeclaredMethods if method.getReturnType() == classOf[play.api.routing.JavaScriptReverseRoute]
             ) yield  method.invoke(controller).asInstanceOf[play.api.routing.JavaScriptReverseRoute]
           Ok(met.mkString(", "))
      }
    }
    

    Which should lead to the expected behaviour.