How to pass a subclass of a model into a Template


(Shane) #1

Hi,

I have a problem with passing wildcards in package names into Twirl templates.

I have created a model and I’m inheriting other models from it e.g.

package1.Item extends Model
		|
		|
package2.Item2 extends package1.Item

For this I have created a single generic Controller that selects the model based on a parameter passed in on the URL.

This takes any Item subclass and passes it into the Twirl template so in theory I can have a single controller, a consistant set of templates and easy deployment of new models as required which is perfect for this particular usecase. It’s a work in progress but the compiler isn’t complaining about anything except the templates :)

The problem I have is that I’m unable to pass in anything other than the Super Class (package1.Item) to the templates. I’m unable to pass in a subclass i.e. package2.Item2.

I’ve tried using Any but then the methods aren’t recognised as they don’t belong to Any.
I’ve also tried using the _ wildcard without success.

I’m not brilliant with Scala so it could be just down to a lack of knowledge on my part.
Any help would be appreciated.

Thanks


(Tim Moore) #2

@Shane I hope you don’t mind: I reformatted your post a little bit to make it easier to read.

I don’t totally understand your description of the problem, however. Can you post the code that you’re asking about (both the controller and the view code), and the error messages that you get from the compiler?


(Shane) #3

Hi Tim,

Thanks for your quick reply to my question :)
I hope the below helps a to clarify my question a little.

I have a model called ItemModel that extends an EBean model:

package models.system.items.base_item;

@MappedSuperclass
public abstract class ItemModel extends Model {

	public ItemModel() {
		super();
	}

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private long id;
	
	
	....
	
}

From that I have other models that extend ItemModel using a <?> Generic e.g.:

package models.system.items.music;

import javax.persistence.Entity;
import com.avaje.ebean.PagedList;

import models.system.items.base_item.ItemModel;

@Entity
public class ExampleItemModel extends ItemModel {

	@Override
	public PagedList<? extends ItemModel> pagedList(int page, int pageSize, String sortBy, String order, String filter) {
		
		return ExampleItemModel.page(page, pageSize, sortBy, order, filter);
	}
}

The ExampleItemModel or any other Model that extends ItemModel is used in a Controller as follows:

public Result edit(Long pictureId, String itemType) {
	User user = User.findByEmail(request().username());
	
	...
	
	//Get form for a given itemType from a custom Factory Object which is horribly hacky 
	Form<? extends ItemModel> ItemForm = itemFormFactory.getItemForm(itemType, pictureId);
	
	return ok(editItem.render(pictureId, user, ItemForm));
}

And a Form:

@(id: Long,user: User,itemForm: Form[models.system.items.base_item.ItemModel])

@import helper._

@import b3.vertical.fieldConstructor

@main(user) {
	@if(user != null) {
		 <h1 class="title-font">Edit Entry</h1>
		 
		 ...

  } @*end if(user != null) *@ 
} @*end main *@

The problem I have is that I would like to be able to pass in any ExampleItem or other Item that extends ItemModel into the Form in a generic way.

Basically I need a Form that can take any type of ItemModel.
This would save a lot of duplication and having lots of Templates that have nothing but the same content.

In Java I could just use a Generic so instead of this in my Form:
@(id: Long,user: User,itemForm: Form[models.system.items.base_item.ItemModel])

I could use this and not need a custom Factory Object:
@(id: Long,user: User,itemForm: Form[<? extends models.system.items.base_item.ItemModel>])

But I don’t know how to do that with a Twirl Template.

I hope this won’t add additional confusion.

Thanks
Shane


(Tim Moore) #4

The equivalent to Form<? extends ItemModel> in Scala syntax is Form[_ <: ItemModel]


(Shane) #5

Hi Tim,

Thanks for your help with this.
I’ll give that a try :slight_smile:
It should save me a lot of template duplication all going well.

Thanks
Shane


(Shane) #6

Hi Tim,

I’m afraid I had no luck with that approach. :(

The error I’m getting is:

[error] .../app/controllers/system/items/generic_item/GenericItemController.java:130:  
play.data.Form<capture#1 of ? extends models.system.items.base_item.ItemModel> cannot be converted to play.data.Form<java.lang.Object>

For example in the edit() method in the GenericItemController:

public Result edit(Long itemId) {
	User user = User.findByEmail(request().username());
	
	if(user == null){
		return GO_HOME;	
	}
	
	ItemFactory itemFactory = new ItemFactory(formFactory);
	
	Form<? extends models.system.items.base_item.ItemModel>itemForm = itemFactory.getItemForm("music", itemId);
	return ok(editItem.render(itemId, user, itemForm));
}

The itemFactory returns a filled form for the Item sub-class based on the string passed in.

package controllers.system.items.item_factory;

import com.google.inject.Inject;

import models.system.items.base_item.ItemModel;
import models.system.items.music.ItemMusicModel;
import models.system.items.tv.ItemTVModel;
import play.data.Form;
import play.data.FormFactory;

public class ItemFactory {
	
	private FormFactory formFactory;
	
	@Inject
	public ItemFactory(FormFactory formFactory) {
		super();
		this.formFactory = formFactory;
	}
		
	
	/**
	 * @param itemName
	 * @param itemID
	 * @return A form wrapping an ItemType and populated with the content of the Item with itemID
	 */
	public Form <? extends models.system.items.base_item.ItemModel> getItemForm(String itemName,Long itemID) {
		
		if (itemName.equalsIgnoreCase("music")) {
			
			return formFactory.form(ItemMusicModel.class)
					.fill((ItemMusicModel) ItemMusicModel.find.byId(itemID));

		} else if (itemName.equalsIgnoreCase("tv")) {
			return formFactory.form(ItemTVModel.class)
					.fill((ItemTVModel) ItemTVModel.find.byId(itemID));		
		}

		return null;
	}
}

And that’s passed to the Edit Form which now starts with:

@(id: Long,user: User,ItemForm: Form[_ <: models.system.items.base_item.ItemModel])

In the usecase where I just want to view a single entry of Item and I am not therefore using a Form but instead the Model directly I get the following error:

.../app/views/system/items/base_item/viewItem.scala.html:1: unbound wildcard type
[error] @(user: User,item: _ <: models.system.items.base_item.ItemModel)
[error]                    ^

So it doesn’t seem to like the wildcard in this case.

If I can get this solution to work it would save a ton of implementation and maintainance headaches in the future as there will be several of these types.

Thanks
Shane


(Shane) #7

I finally got this to work , at least for the create usecase , by adding a cast to Form<superclass to the render method called in the controller. e.g.

	ItemModel itemModel = itemFactory.getItem(itemType);
	Form<? extends models.system.items.base_item.ItemModel> itemForm = formFactory.form(itemModel.getClass());
	
	return ok(createMemoryItem.render(user, userID, (Form<ItemModel>)itemForm));

The template remains as it was originally, starting with:
@(id: Long,user: User, itemForm: Form[models.system.items.base_item.ItemModel])