scalafunctional-programmingplayframework-2.0generic-programmingscala-template

Play Framework 2 - Scala template: How to call a specific method on a generic object?


I am using Play Framework 2.1.5 in a Java application.

I've got a view component that takes a list of generic objects as a parameter. In this component, I want to iterate on the list and get some properties of each element.

This would look like something like this:

@(elements: List[_])

@for((element, i) <- elements.view.zipWithIndex) {
    @i
    @element.id
    @element.name
}

(I need those 3 values)

But, of course, element.id and element.name would not compile even if the type of objects I put in the list contained these methods. So I did this:

@for((element, i) <- elements.view.zipWithIndex) {
    @defining(
        ViewsUtils.getGenericElementId(element),
        ViewsUtils.getGenericElementName(element)) {
            case (id, name, something) =>
                @i
                @id
                @name
        }
}

And in a Java utility class:

public final class ViewsUtils {

    public static String getGenericElementId(Object object) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return object.getClass().getMethod("getId").invoke(object).toString();
    }

    public static String getGenericElementName(Object object) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        return object.getClass().getMethod("getName").invoke(object).toString();
    }
}

This works but I know it's not right because it could throw a RuntimeException in case one of these methods didn't exist for the type of object I put in the list.

Here are my suppositons:

  1. Casting the elements

  2. Using inheritance

  3. As I just need 2 properties of each object (id and name), I could use a map, but I need the index of the loop (is it possible to get it from a map?) and it wouldn't work if I needed more than 2 properties.

  4. Maybe Scala provides the syntax for this kind of stuff.

Or maybe I'm just looking in the wrong direction.

Thank you for your help.


Solution

  • Ok, that is a bit too much for a comment, so I'll risk to post it as an answer.

    Assuming you wish to never get a RuntimeException you mentioned it makes sense that your object implement some interface (or mix in a trait, as it is said in Scala), so you won't risk getting an exception and no more need reflection to get values.

    Suppose, you declare such a trait:

    trait GenericObject {
      val id: Long
      val name: String
    }
    

    Then you declare some case classes:

    case class A(id: Long, name: String, someOtherField: SomeType) extends GenericObject {
      //your implementation
    }
    
    case class B(id: Long, name: String) extends GenericObject
    
    case class C(id: Long, name: String) extends B(id, name)
    

    Now you can change your template like this:

    @(elements: List[GenericObject])
    
    @for((element, i) <- elements.view.zipWithIndex) {
      @i
      @element.id
      @element.name
    }
    

    And you should pass the list of GenericObjects to your template:

    val myObjects: List[GenericObject] = List(A(1, "A name"), B(2, "B name"), C(3, "C name"))
    Ok(your_template.scala.html(myObjects))