IMDB Web application

Neo4j Wiki から

This page introduces how to tie a Neo4j application to Spring Framework. The task includes many files, but they are small and should hopefully be easy to comprehend.

目次

[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.

Image:Imdb.packages.png


From the servlet perspective this is what we have:


Image:Imdb.servlet.png


As the base for our application we use the following beans:

Bean overview
Name Class Description
graphDbService org.neo4j.graphdb.EmbeddedNeo Neo4j engine
indexService org.neo4j.index.lucene.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 graphDbService, indexService and pathFinder
searchEngine org.neo4j.examples.imdb.domain.ImdbSearchEngineImpl provides a simple search engine; uses graphDbService and indexService
imdbReader org.neo4j.examples.imdb.parser.ExampleImdbReader reads IMDB data into the graph; 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
imdbSetup org.neo4j.examples.imdb.web.ImdbSetupControllerDelegate 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.

Image:Imdb.web.find.png

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 FindController class inherits from SimpleFormController, so it only has to provide the application-specific things by itself.
  • FindController uses delegates that implement the FindControllerDelegate interface.
  • ActorFindControllerDelegate and MovieFindControllerDelegate implement the FindControllerDelegate interface and performs most of the actual work of the web part, using the imdbService.
  • ActorName and MovieTitle are simple containers for the form data - which acts as input to the FindController.

[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:

  1. Find the actor by name.
  2. Print the movies the actor acted in, including the role name for every movie.
  3. Print the Bacon number and path for the actor.
  4. 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] Movies

    <c:forEach items="${model.movieInfo}" var="movieInfo"> <c:url value="movie.html" var="movieURL"> <c:param name="title" value="${movieInfo.title}" /> </c:url>
  • <c:out value="${movieInfo.role}" /> in <a href='<c:out value="${movieURL}"/>'><c:out value="${movieInfo.title}" /></a>
  • </c:forEach>

</pre>

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:

Image:Imdb.web.setup.png

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() );
    }
}
Some thoughts:
  • letting Spring wire things together lets you do much with small amounts of code
  • you can configure Spring to use the Neo4j transaction manager
  • it's possible to configure less than we did in the example in the servlet configuration and add it through annotations instead


Next part: IMDB Wrap up Index page: overview

Neo4j のサイト
ツールボックス