Integrate faceted search with Wicket and Guice (Part Three)

In this part, I will write about the Wicket and Guice bits in the sample Facet Search project. In Part Two, I have already introduced two Wicket classes, the WicketApplication and the MoviePage. The WicketApplication was only 30 lines long. It used a GuiceComponentInjector to wire all dependencies via Guice and returned the MoviePage as the homepage of the application.

MoviePage then, used a private member that is referencing the Facetmap.



@Inject
private Facetmap m_facetmap;



Notice the @Inject annotation from Google Guice. But how does Guice know what to dependency-inject for this? This is defined in a class called MainModule. Modules are the essence of all Guice applications. So if you have something like this in your Module class



protected void configure()
{
bind(Facetmap.class)
.to(MovieFacetmap.class)
.asEagerSingleton();
}



then Guice will use a single instance of the MovieFacetmap class wherever a Facetmap Interface is needed. Let's have a look in the MovieFacetmap again.



public class MovieFacetmap extends SimpleFacetmap
{
@Inject
public MovieFacetmap(@Named("movieFacetmapName") String name, ResourceSpace resourceSpace, Properties properties)
{
super(name, resourceSpace, properties);
}
}

MovieFacetmap.java

Again Guice stuff is present here. The constructor is annotated with @Inject, thereby putting all parameters under Guice control. We need to tell Guice what parameters to send in when calling the MovieFacetmap constructor. This instruction is again done in the configure() method of the MainModule.



bindConstant()
.annotatedWith(Names.named("movieFacetmapName"))
.to("80s Movies");

bind(ResourceSpace.class)
.to(MovieResourceSpace.class)
.asEagerSingleton();
bind(Properties.class)
.toProvider(MovieSelectionPropertyProvider.class)
.asEagerSingleton();



The instruction reads as this for Guice: whenever something is annotated with @Named("movieFacetmapName"), use the String „80s Movies“. Whenever a ResourceSpace type is needed, use a single instance of MovieResourceSpace. Whenever Properties are needed use the MovieSelectionPropertyProvider to get to these Properties. This might not be working in large applications where you have a lot of Properties used everywhere, but it is enough in this small webapplication.

Basically the same guice stuff is used in the MovieResourceSpace constructor.



bind(Map.class)
.annotatedWith(Names.named("movies"))
.to(MovieList.class)
.asEagerSingleton();



Guice Instruction: Whenever a Map interface is annotated with @Named("movies"), use a single instance of MovieList instead. It is very important to use the Singleton scope which Google Guice provides. Otherwise Guice will create a new instance every time a "guiced" object is needed and then the whole system will just not work properly. It is assuming that you have exactly one instance of the Facetmap, ResourceSpace and FacetSpace. Finally this is the full MainModule that you need.



public class MainModule extends AbstractModule
{
protected void configure()
{
bind(Facetmap.class)
.to(MovieFacetmap.class)
.asEagerSingleton();

bind(Properties.class)
.toProvider(MovieSelectionPropertyProvider.class)
.asEagerSingleton();

bindConstant()
.annotatedWith(Names.named("movieFacetmapName"))
.to("80s Movies");

bind(Map.class)
.annotatedWith(Names.named("movies"))
.to(MovieList.class)
.asEagerSingleton();

bind(ResourceSpace.class)
.to(MovieResourceSpace.class)
.asEagerSingleton();

bind(FacetSpace.class)
.to(MovieFacets.class)
.asEagerSingleton();
}
}

MainModule.java

Now that is should be clear, how the Facetmap is injected into the MoviePage for Wicket, let's take a look under the hood. The MoviePage constructor just calls the setupFacetMap method (the name is not quite appropriate I just realized as the Facetmap is already set up). Anyway the method looks like this



private void setupFacetMap(final PageParameters parameters)
{
String ref = parameters.getString(REF_PARAMETER, "");
Facetmap facetmap = m_facetmap;

Selection currentSelection = getCurrentSelection(facetmap, ref);
setupForwardSelections(currentSelection);
setupResources(currentSelection);
}



I am getting a request parameter that Wicket wraps for me into a PageParameters object. The parameter will be abscent in the initial request, so I will set it to an empty String per default. Next step is getting the current Selection object based on the request parameter. If the request parameter is empty or null, I will use the root Selection.



private Selection getCurrentSelection(Facetmap facetmap, String ref)
{
Selection selection;
try
{
selection = StringUtils.isBlank(ref) ? facetmap.getRootSelection() : facetmap.getSelection(ref);
}
catch (InternalException e)
{
throw new RuntimeException("An error occured accessing a Selection.", e);
}
catch (UnknownReferenceException e)
{
throw new RuntimeException("An error occured accessing a Selection.", e);
}
return selection;
}



With the Selection object, it is time to populate the Wicket ListView's with the forward Selection's and the Resources that match the current Selection. A forward Selection is basically a Selection that the user may choose from his current Selection. Any forward Selection that will return at least 1 Resource is considered a valid forward Selection. In part two I wrote about 2 properties that will influence this behaviour. It is possible to make every forward Selection a valid one by setting com.facetmap.Selection.showEmptySelections to true.



/**
* Returns a list of {@link MovieCriteria} objects from the specified Selection.
*/
private List getForwardSelections(Selection selection)
{
List movieCriterias = new ArrayList();

int count = selection.getFacetCount();
for (int i = 0; i < count; i++)
{
String facetTitle = extractFacetTitle(selection.getHeadings(i));
Iterator iterator;
try
{
iterator = selection.getForwardSelections(i);
}
catch (InternalException e)
{
throw new RuntimeException("Unable to get forward selections", e);
}

if (iterator != null)
{
List



First get the Facet count from the current Selection, then iterate over the Facets to get the forward Selection's. Perform an inner loop over the forward Selections to get the information that you want to display in the page (matched Movies, display name). All this information is wrapped into two domain classes Option and MovieCriteria. Remember that these domain classes need to be Serializable to work properly with Wicket.

The final step in the MoviePage is showing the Resource's (Movies) that match the current Selection. This is done in the setupResources() method. Everything you need to do is call getResources() on the current Selection and then display the Resources somehow using the Wicket Framework. No big magic happening here. This is the full MoviePage again.



public class MoviePage extends WebPage {

private static final long serialVersionUID = 1L;

public static final String REF_PARAMETER = "refId";

@Inject
private Facetmap m_facetmap;

/**
* Will be called by the Wicket framework.
*/
public MoviePage(final PageParameters parameters)
{
setupFacetMap(parameters);
}

/**
* Initializes all Wicket components based on the current selection
* and the (global) {@link Facetmap}.
*/
private void setupFacetMap(final PageParameters parameters)
{
String ref = parameters.getString(REF_PARAMETER, "");
Facetmap facetmap = m_facetmap;

Selection currentSelection = getCurrentSelection(facetmap, ref);
setupForwardSelections(currentSelection);
setupResources(currentSelection);
}

/**
* Initializes all Wicket components based on the current {@link Selection}.
*/
private void setupResources(Selection currentSelection)
{
List resources = new ArrayList();
SelectedResourceIterator resourceIterator = null;
try
{
resourceIterator = currentSelection.getResources();
}
catch (InternalException e)
{
throw new RuntimeException("An error occured while getting resources.", e);
}

if (resourceIterator != null)
{
while (resourceIterator.hasNext())
{
Resource resource;
try
{
resource = resourceIterator.next();
resources.add(resource);
}
catch (InternalException e)
{
throw new RuntimeException("An error occured while iterating and adding resources.", e);
}
}
}

ListView resourceList = new ListView("resourceList", resources)
{
@Override
protected void populateItem(ListItem item)
{
Movie movie = (Movie) item.getModelObject();
Label text = new Label("resourceName", movie.getTitle());
item.add(text);
}
};
add(resourceList);
}

/**
* Populates a ListView component based on the given {@link Selection }.
*/
private void setupForwardSelections(Selection currentSelection)
{
List selections = new ArrayList();
if (currentSelection != null)
{
selections = getForwardSelections(currentSelection);
}

ListView selectionList = new ListView("facetList", selections)
{
@Override
protected void populateItem(ListItem item)
{
MovieCriteria movieCriteria = (MovieCriteria) item.getModelObject();
String facetName = movieCriteria.getName();

Label facetNameLabel = new Label("facetName", facetName);
facetNameLabel.setRenderBodyOnly(true);
item.add(facetNameLabel);

List

MoviePage.java

Now for the part that I like most about Apache Wicket - unit testing. No Selenium, no Watir is needed to test Wicket web applications. The framework is shipped with a bunch of testing classes that will work almost like GUI testing. So if it works in your unit test, you know it will work in your browser as well (Layout testing not included of course). Here is my TestNG test for the MoviePage class.



public class MoviePageTest
{
@Test
public void testRootSelection()
{
WicketTester app = new WicketTester();
app.getApplication().addComponentInstantiationListener(new GuiceComponentInjector(app.getApplication(), new MainModule()));

app.startPage(MoviePage.class);
app.assertRenderedPage(MoviePage.class);

app.assertComponent("facetList", ListView.class);
ListView facetListView = (ListView) app.getComponentFromLastRenderedPage("facetList");
List facets = facetListView.getList();
assertEquals(facets.size(), 5);

ListView genresListView = (ListView) facetListView.get("0:optionList");
assertEquals(genresListView.getList().size(), 3);

ListView boxOfficeGrossListView = (ListView) facetListView.get("1:optionList");
List gross = boxOfficeGrossListView.getList();
assertTrue(gross.isEmpty(), "We should not have box office gross information.");

ListView releaseDateListView = (ListView) facetListView.get("2:optionList");
assertEquals(releaseDateListView.getList().size(), 9);

ListView studiosListView = (ListView) facetListView.get("3:optionList");
assertEquals(studiosListView.getList().size(), 24);

ListView actorsListView = (ListView) facetListView.get("4:optionList");
assertEquals(actorsListView.getList().size(), 450);

MovieList movieList = new MovieList(new MovieFacets());
app.assertComponent("resourceList", ListView.class);
ListView resourceListView = (ListView) app.getComponentFromLastRenderedPage("resourceList");
List resourceList = resourceListView.getList();
assertEquals(resourceList.size(), movieList.size());
}

@Test
public void testDrewBarrymore()
{
testActor("Drew Barrymore", 1);
}

@Test
public void testHarrisonFord()
{
testActor("Harrison Ford", 8);
}

/**
* Queries the {@link MoviePage} with the specified actor and expects the
* given number of movies in the result list.
*
* @param actorName the name of an actor from the MovieList
* @param expectedMovies the number of movies that he or she plays in
*/
private void testActor(final String actorName, final int expectedMovies)
{
WicketTester app = new WicketTester();
app.getApplication().addComponentInstantiationListener(new GuiceComponentInjector(app.getApplication(), new MainModule()));

app.startPage(MoviePage.class);
app.assertRenderedPage(MoviePage.class);

app.assertComponent("facetList", ListView.class);
ListView facetListView = (ListView) app.getComponentFromLastRenderedPage("facetList");
List facets = facetListView.getList();
assertEquals(facets.size(), 5);

ListView actorsListView = (ListView) facetListView.get("4:optionList");
List

MoviePageTest.java

I am creating a new WicketTester. This WicketTester is replacing our own WicketApplication class in the test, that is why I have to call addComponentInstantiationListener(..) manually (since I also do it in WicketApplication). With the WicketTester class you essentially get these methods for testing:

startPage – request any WebPage subclass you want to test
assertRenderedPage – assert the WebPage really showed up
assertComponent – make sure the components you have added in your WebPage subclass are really contained
getComponentFromLastRenderedPage – get the component from the WebPage after it was rendered

Each Component then has specific methods to validate their state. For instance the ListView Component has a getList() method, which I use to get me the MovieList in my test. All I have left in the Test, is to assert that there were indeed 450 movies in that list. This is all done in the testRootSelection() method. I have written two additional TestNG test cases, which will verify the use case in which a user has made a forward Selection from the initial request. The two test cases are called testDrewBarrymore() and testHarrisonFord(). I am setting up a PageParameters object for Wicket that will contain the right request parameter to render all movies in which Drew Barrymore and Harrison Ford are playing. There should be 1 movie with Barrymore (E.T. yeehaw) and 8 movies with Ford in the result list. You could easily come up with more complicated scenarios as the forward selections continue from there. For now this test should be just enough.

This concludes the part about Apache Wicket and Google Guice.