IMDB Web application
From NeoWiki
Contents |
[edit] Overview
In our application, the web layer uses the domain layer and the parser for the IMDB data. The parser in turn relies on the domain layer to provide its service.
From the servlet perspective this is what we have:
As the base for our application we use the following beans:
| Name | Class | Description |
|---|---|---|
| neoService | org.neo4j.api.core.EmbeddedNeo | neo4j engine |
| indexService | org.neo4j.util.index.LuceneIndexService | neo4j index service |
| pathFinder | org.neo4j.examples.imdb.util.SimplePathFinder | utility to find shortest "Bacon path" |
| imdbService | org.neo4j.examples.imdb.domain.ImdbServiceImpl | provides domain layer services; uses neoService, indexService and pathFinder |
| searchEngine | org.neo4j.examples.imdb.domain.ImdbSearchEngineImpl | provides a simple search engine; uses neoService and indexService |
| imdbReader | org.neo4j.examples.imdb.parser.ExampleImdbReader | reads IMDB data into the node space; uses imdbService |
| transactionManager | org.springframework.transaction.jta.JtaTransactionManager | implementation for JTA, delegating to the neo4j JTA provider |
| /actor.html | org.neo4j.examples.imdb.web.FindController | actor search web page; delegates to findActor |
| findActor | org.neo4j.examples.imdb.web.ActorFindControllerDelegate | performs actor search, populates model |
| /movie.html | org.neo4j.examples.imdb.web.FindController | movie search web page; delegates to findMovie |
| findMovie | org.neo4j.examples.imdb.web.MovieFindControllerDelegate | performs movie search, populates model |
| /setup.html | org.neo4j.examples.imdb.web.SetupController | data injection web page; delegates to neoSetup |
| neoSetup | org.neo4j.examples.imdb.web.NeoSetupControllerDelegate | performs data injection; uses imdbReader |
[edit] Search pages
Let's look at the search pages for actor and movie and see how they work - focusing on the neo4j aspects. To begin with, we'll learn about the interfaces and classes involved here.
In the actor and movie form pages, we both want to inherit from SimpleFormController (because it's convenient) and implement an interface (making it possible to use the Spring Framework dependency injection magic). So these are some of the details on this topic:
- The
FindControllerclass inherits fromSimpleFormController, so it only has to provide the application-specific things by itself. -
FindControlleruses delegates that implement theFindControllerDelegateinterface. -
ActorFindControllerDelegateandMovieFindControllerDelegateimplement theFindControllerDelegateinterface and performs most of the actual work of the web part, using theimdbService. -
ActorNameandMovieTitleare simple containers for the form data - which acts as input to theFindController.
[edit] Extending SimpleFormController
The FindController class adds a thin layer upon the
SimpleFormController and forwards requests down to
the respective delegates.
The class and its delegates are instantiated by Spring,
configured in this way (from src/main/webapp/WEB-INF/imdb-app-servlet.xml):
<bean name="/actor.html" class="org.neo4j.apps.imdb.web.FindController"> <constructor-arg index="0" ref="findActor" /> <property name="sessionForm" value="true" /> <property name="commandName" value="findActor" /> <property name="commandClass" value="org.neo4j.apps.imdb.web.ActorName" /> <property name="successView" value="movie-list" /> </bean> <bean id="findActor" class="org.neo4j.apps.imdb.web.ActorFindControllerDelegate" /> <bean name="/movie.html" class="org.neo4j.apps.imdb.web.FindController"> <constructor-arg index="0" ref="findMovie" /> <property name="sessionForm" value="true" /> <property name="commandName" value="findMovie" /> <property name="commandClass" value="org.neo4j.apps.imdb.web.MovieTitle" /> <property name="successView" value="actor-list" /> </bean> <bean id="findMovie" class="org.neo4j.apps.imdb.web.MovieFindControllerDelegate" />
This is the full source code of the FindController class.
public class FindController extends SimpleFormController
{
private final FindControllerDelegate delegate;
public FindController( final FindControllerDelegate delegate )
{
super();
this.delegate = delegate;
}
@Override
protected ModelAndView onSubmit( final Object command ) throws ServletException
{
final Map<String,Object> model = new HashMap<String,Object>();
delegate.getModel( command, model );
return new ModelAndView( getSuccessView(), "model", model );
}
@Override
protected boolean isFormSubmission( final HttpServletRequest request )
{
final String field = request.getParameter( delegate.getFieldName() );
return field != null && field.trim().length() > 0;
}
}
The onSubmit() method receives the request, and creates a Map
to store the resulting model. After letting the delegate fill the model appropriately, the
view for the page is returned together with the model.
The isFormSubmission() method alters the overridden method by allowing for
GET requests as well (not only POST requests).
This is made to make it possible to link directly to searches.
The delegates then implement the following interface:
public interface FindControllerDelegate
{
void getModel( Object command, Map<String,Object> model )
throws ServletException;
String getFieldName();
}
[edit] The Actor page
Our requirements for the actor page are:
- Find the actor by name.
- Print the movies the actor acted in, including the role name for every movie.
- Print the Bacon number and path for the actor.
- Link all output to the corresponding actor/movie.
We'll work through the implementation step by step.
Setting up the class and doing trivial stuff:
public class ActorFindControllerDelegate implements FindControllerDelegate
{
@Autowired
private ImdbService imdbService;
public String getFieldName()
{
return "name";
}
Now we have an autowired ImdbService that we can use.
We also told the FindController that the field name
we're interested in here is "name".
By now it's time to receive the request.
Note that all operations from here and on are wrapped inside a transaction. It's not possible to read data from neo4j outside of a transaction.
@Transactional
public void getModel( final Object command, final Map<String,Object> model ) throws ServletException
{
final String name = ((ActorName) command).getName();
final Actor actor = imdbService.getActor( name );
populateModel( model, actor );
}
The command received is an ActorName object, that
wraps the name field. To lookup the actor, we make
a simple call to the ImdbService::getActor() method.
Let's move on to the code that populates the model:
private void populateModel( final Map<String,Object> model, final Actor actor )
{
if ( actor == null )
{
model.put( "actorName", "No actor found" );
model.put( "kevinBaconNumber", "" );
model.put( "movieTitles", Collections.emptyList() );
}
else
{
model.put( "actorName", actor.getName() );
final List<?> baconPathList = imdbService.getBaconPath( actor );
model.put( "kevinBaconNumber", baconPathList.size() / 2 );
Now we have the bacon number stored in the model, and
move on to the movie list. The MovieInfo class is
a member class used to provide easy access to the data on the
JSP side of things. The constructor accepts a Movie and a
Role and the class will provide title and role name.
As we want the list of movies to be in alphabetical order, we
simply use a TreeSet to store it.
final Collection<MovieInfo> movieInfo = new TreeSet<MovieInfo>();
for ( Movie movie : actor.getMovies() )
{
movieInfo.add( new MovieInfo( movie, actor.getRole( movie ) ) );
}
model.put( "movieInfo", movieInfo );
Finally, we convert the Bacon path to a List of Strings.
final List<String> baconPath = new LinkedList<String>();
for ( Object actorOrMovie : baconPathList )
{
if ( actorOrMovie instanceof Actor )
{
baconPath.add( ((Actor) actorOrMovie).getName() );
}
else if ( actorOrMovie instanceof Movie )
{
baconPath.add( ((Movie) actorOrMovie).getTitle() );
}
}
model.put( "baconPath", baconPath );
}
}
The two parts of interest in the MovieInfo member class is the constructor
and the compareTo() method (go to the source code to view the full source).
To keep our JSP code clean, we extract the movie title and role name here, using a default value if there is no role name.
MovieInfo( final Movie movie, final Role role )
{
setTitle( movie.getTitle() );
if ( role == null || role.getName() == null )
{
setRole( "(unknown)" );
}
else
{
setRole( role.getName() );
}
}
To make it possible to sort the movies alphabetically, we
need to implement the Comparable interface, requiring us
to implement the compareTo() method. It goes like this:
public int compareTo( final MovieInfo otherMovieInfo )
{
return getTitle().compareTo( otherMovieInfo.getTitle() );
}
The actor page form is found in the actor.jsp file,
while the success view is in the movie-list.jsp file,
both found in src/main/webapp/jsp/.
We'll just show an excerpt of the movie-list.jsp code here.
<h3>Movies</h3>
<ul>
<c:forEach items="${model.movieInfo}" var="movieInfo">
<c:url value="movie.html" var="movieURL">
<c:param name="title" value="${movieInfo.title}" />
</c:url>
<li><em><c:out value="${movieInfo.role}" /></em> in <a
href='<c:out value="${movieURL}"/>'><c:out
value="${movieInfo.title}" /></a></li>
</c:forEach>
</ul>
This code outputs the list of movies, linking every movie title to the corresponding search URL. The output also includes the role names for every movie.
[edit] The Setup page
The setup page is created in a way very similar to that of the search pages.
This is the overall structure:
We'll only look into the NeoSetupControllerDelegate,
the other parts should be obvious at this stage.
This time the ImdbReader is autowired for us to use.
We create a ImdbParser using it, and the this provides
the services we need. We simply call the parser, buffering the messages we get,
and then send the combined messages to the page view.
To make the Kevin Bacon node easy to access, we also autowired the ImdbService
to be able to execute the setupReferenceRelationship() method.
This will connect the Kevin Bacon node to the reference node.
Note: in this case, we don't use any transactions, as this is handled
by the ImdbReader. Otherwise we would use merely one or two
transactions for all the data, which isn't very effective (there would be
loads of data inside every transaction).
public class NeoSetupControllerDelegate implements SetupControllerDelegate
{
@Autowired
private ImdbReader imdbReader;
@Autowired
private ImdbService imdbService;
public void getModel( final Object command, final Map<String,Object> model ) throws ServletException
{
final ImdbParser parser = new ImdbParser( imdbReader );
StringBuffer message = new StringBuffer( 200 );
try
{
message.append( parser.parseMovies( "target/classes/data/test-movies.list" ) ).append( '\n' );
message.append( parser.parseActors( "target/classes/data/test-actors.list" ) ).append( '\n' );
imdbService.setupReferenceRelationship();
}
catch ( IOException e )
{
message.append( "Something went wrong during the setup process:\n" ).append( e.getMessage() );
}
model.put( "setupMessage", message.toString() );
}
}
Next part: IMDB Wrap up Index page: overview





