diff --git a/pom.xml b/pom.xml
index 3abd9e8c6..da0be7d2f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -170,6 +170,11 @@
4.4
runtime
+
javax.media
jai-core
@@ -183,7 +188,9 @@
${project.basedir}/src/docbkx/resources/xsl/fopdf.xsl
- css/html.css
+ ${project.basedir}/src/docbkx/resources/css/html.css
+ ${project.basedir}/src/docbkx/resources/images/
+ ${project.basedir}/src/docbkx/resources/images/
false
${project.basedir}/src/docbkx/resources/xsl/html.xsl
diff --git a/src/docbkx/index.xml b/src/docbkx/index.xml
index fd405e649..3828ec44a 100644
--- a/src/docbkx/index.xml
+++ b/src/docbkx/index.xml
@@ -37,7 +37,7 @@
-
+
About this book
@@ -69,14 +69,21 @@
-
+
+
-
+
+
+
+
+
+
+
diff --git a/src/docbkx/reference/samples.xml b/src/docbkx/reference/samples.xml
index ee8afc299..6e656c767 100644
--- a/src/docbkx/reference/samples.xml
+++ b/src/docbkx/reference/samples.xml
@@ -24,7 +24,7 @@
Executing the application creates the following graph in the Graph Database:
-
+
@@ -41,11 +41,15 @@
See the readme file for instruction on how to compile and run the application.
An excerpt of the data stored in the Graph Database after executing the application:
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ In Java code this looked like. Pretty straightforward.
+
+
+
+ cast;
+ }
+
+ class Actor {
+ int id;
+ String name;
+ Set filmography;
+ Role playedIn(Movie movie, String role);
+ }
+ class Role {
+ Movie movie;
+ Actor actor;
+ String role;
+ }
+ class User {
+ String login;
+ String name;
+ String password;
+ Set ratings;
+ Set friends;
+ Rating rate(Movie movie, int stars, String comment);
+ void befriend(User user);
+ }
+ class Rating {
+ User user;
+ Movie movie;
+ int stars;
+ String comment;
+ }
+]]>
+
+
+ I wrote some basic tests to assure that the basic plumbing worked. Check.
-
-
-
-
-
diff --git a/src/docbkx/tutorial/import.xml b/src/docbkx/tutorial/import.xml
new file mode 100644
index 000000000..6e5ad61fa
--- /dev/null
+++ b/src/docbkx/tutorial/import.xml
@@ -0,0 +1,23 @@
+
+
+
+ The dusty archives - Importing Data
+
+ Now it was time to pull the data from themoviedb.org. Registering there and getting an API key was simple, using the API on the commandline with curl too.
+ Looking at the JSON returned for movies and people I decided to pimp my domain model and add some more fields so that the representation in the UI was worth
+ the effort.
+
+ For the import process I created a separate importer that used HttpClient and JSON to fetch and parse the data and then some transactional methods to actually
+ insert it as movies, roles and actors. User data was not available so I created an anonymous user called 'Cineast' that I attributed all the ratings and comments
+ to. I also created a version of the importer that read the json files from local disk, so that I didn't have to strain the remote API that much and that often.
+
+
+
+
+
+
+
diff --git a/src/docbkx/tutorial/indexing.xml b/src/docbkx/tutorial/indexing.xml
new file mode 100644
index 000000000..540f3e8a0
--- /dev/null
+++ b/src/docbkx/tutorial/indexing.xml
@@ -0,0 +1,37 @@
+
+
+
+ Do I know you? - Indexing
+
+ Then there was an @Indexed annotation for fields. I wanted to try this too. That would guide the next test. I added an @Indexed to the id field of the movie.
+ This field is intended to represent the external id that will be used in URIs and will stable over database imports and updates. This time I went with the
+ Finder to retrieve my indexed movie.
+
+
+
+ movieFinder = finderFactory.createNodeEntityFinder(Movie.class);
+ Movie retrievedMovie = movieFinder.getByPropertyValue(id);
+ assertEqual("retrieved movie matches persisted one",forestGump,retrievedMovie);
+ assertEqual("retrieved movie title matches","Forest Gump",retrievedMovie.getTitle());
+ }
+
+]]>
+
+
+ TODO This failed with an exception about not being in a transaction. Oh, I forgot to add the @Transactional. So I added it to the test.
+
+
diff --git a/src/docbkx/tutorial/neo4j.xml b/src/docbkx/tutorial/neo4j.xml
new file mode 100644
index 000000000..988817a9b
--- /dev/null
+++ b/src/docbkx/tutorial/neo4j.xml
@@ -0,0 +1,37 @@
+
+
+
+ Graphs ahead - Learning Neo4j
+
+ Then came the unknown - how to put these domain objects into the graph. First I read up about graph databases, especially Neo4j. Their datamodel consists
+ of nodes and relationships all of which can have properties. Relationships as first class citizens - I liked that. Then there was the possibility to index
+ both by field, value pairs to quickly get hold of them as starting points for further processing. Other useful operations were manual traversal of relationships
+ and a powerful traversal based on a query like Traversal Description. That all seemed pretty easy.
+
+ I also learned that Neo4j was transactional and provided the known ACID guarantees for my data. This was unsual for a NoSQL database but easier for me to get
+ my head around than non-transactional eventual persistence. That also meant that I had to manage transactions somehow. Keep that in mind.
+
+
+
+
diff --git a/src/docbkx/tutorial/recommendations.xml b/src/docbkx/tutorial/recommendations.xml
index dd72cf4f4..24eccbf0a 100644
--- a/src/docbkx/tutorial/recommendations.xml
+++ b/src/docbkx/tutorial/recommendations.xml
@@ -1,8 +1,27 @@
- Adding recommendations
+ Movies! Friends! Bargains! - Recommendations
-
+ In the last part of this exercise I wanted to add some recommendation algorithms to my app. One was the recommendation of movies that my friends liked very much
+ (and their friends in descending importance). The second was recommendations for new friends that also liked the movies that I liked most.
+
+ Doing this kind of ranking algorithms is the real fun with graph databases. They are applied to the graph by traversing it in a certain order, collecting information
+ on the go and deciding which paths to follow and what to include in the results.
+
+ Lets say I'm only interested in the top 10 recommendations each.
+
+ 5) return EXCLUDE_AND_STOP;
+ Relationship rating = path.lastRelationship();
+ if (rating.getType().equals(RATED)) {
+ rating.getProperty()
+ return INCLUDE_AND_STOP;
+ }
+ return INCLUDE_AND_CONTINUE;
+ })
+]]>
diff --git a/src/docbkx/tutorial/relationships.xml b/src/docbkx/tutorial/relationships.xml
new file mode 100644
index 000000000..7b1e9645b
--- /dev/null
+++ b/src/docbkx/tutorial/relationships.xml
@@ -0,0 +1,107 @@
+
+
+
+ A convincing act - Relationships
+
+ Value in Relationships - Creating them
+
+ Next were relationships. Direct relationships didn't require any annotation. Unfortunately I had none of those. So I went for the Role relationship
+between Movie and Actor. It had to be annotated with @RelationshipEntity and the @StartNode and @EndNode had to be marked.
+So my Role looked like this:
+
+
+
+
+ When writing a test for that I tried to create the relationship entity with new, but got an exception saying that this was not allowed. Some weird restriction
+about having only correctly constructed RelationshipEntities. So I remembered a relateTo method from the list of introduced methods on the NodeEntities. After
+quickly checking it turned out to be exactly what I needed. I added the method for connecting movies and actors to the actor - seemed more natural.
+
+
+
+
+
+
+ Who's there ? - Accessing related entities
+
+ What was left - accessing those relationships. I already had the appropriate fields in both classes. Time to annotate them correctly. For the fields providing
+access to the entities on the other side of the relationship this was straightforward. Providing the target type again (thanks to Java's type erasure) and the
+relationship type (that I learned from the Neo4j lesson before) there was only the direction left. Which defaults to OUTGOING so only for the movie I had to
+specify it.
+
+
+ cast;
+}
+
+@NodeEntity
+class Actor {
+ @Indexed
+ int id;
+ String name;
+ @RelatedTo(elementClass = Movie.class, type = "ACTS_IN")
+ Set cast;
+
+ public Role playedIn(Movie movie, String roleName) {
+ Role role = relateTo(movie, Role.class, "ACTS_IN");
+ role.setRole(roleName);
+ return role;
+ }
+}
+ ]]>
+
+
+
+ May I introduce ? - Accessing Relationships themselves
+
+ While reading about those relationship-sets I learned that they are handled by managed collections of spring data graph. So whenever I add something to the
+ set or remove it, it automatically reflects that in the underlying relationships. Neat. But this also meant I mustn't initialize the fields. Something I will
+ certainly forget not to do in the future, so watch out for it.
+
+ I didn't forget to add test for those. So I could assure that the collections worked as advertised (and also ran into the intialization problem above).
+
+ But I still couldn't access the Role relationships. There was more to read about this. For accessing the relationship in between the nodes there was a separate
+ annotation @RelatedToVia. And I had to declare the field as readonly Iterable<Role>. That should make sure that I never tried to add Roles (which I couldn't create on my own anyway)
+ to this field. Otherwise the annotation attributes were similar to those used for @RelatedTo. So off I went, creating my first real relationship (just kidding).
+
+ cast;
+
+ @RelatedToVia(elementClass = Role.class, type = "ACTS_IN", direction = Direction.INCOMING)
+ Iterable roles;
+ }
+ ]]>
+
+ After the tests proved that those relationship fields really mirrored the underlying relationships in the graph and instantly reflected additions and removals I was
+ satisfied with my domain so far and went for some coffee and chocolate.
+
+
+
diff --git a/src/docbkx/tutorial/repository.xml b/src/docbkx/tutorial/repository.xml
new file mode 100644
index 000000000..708fc4bc6
--- /dev/null
+++ b/src/docbkx/tutorial/repository.xml
@@ -0,0 +1,26 @@
+
+
+
+ Serving a good cause - Repository
+
+ That was the first method to add to the repository. So I created a repository for my application, annotated
+ it with @Repository and @Transactional.
+
+
+ movieFinder;
+ @Autowired
+ public CineastsRepostory(FinderFactory finderFactory) {
+ this.finderFactory = finderFactory;
+ this.movieFinder = finderFactory.createNodeEntityFinder(Movie.class);
+ }
+ public Movie getMovie(int id) {
+ return movieFinder.getById(id);
+ }
+}
+ ]]>
+
+
diff --git a/src/docbkx/tutorial/running.xml b/src/docbkx/tutorial/running.xml
new file mode 100644
index 000000000..e1438db4d
--- /dev/null
+++ b/src/docbkx/tutorial/running.xml
@@ -0,0 +1,55 @@
+
+
+
+ Curtains Up! - Get it running
+
+ Requisites - Populating the database
+
+ Time to put this on display. But I needed some test data first. So I wrote a small class for populating the database which could be called from my controller.
+ To make it safe to call it several times I added index lookups to check for existing entries. A simple /populate endpoint for the controller that called it would
+ be enough for now.
+
+
+
+
+
+
+ Behind the scenes - Peeking at the Datastore
+
+
+ Eye candy - Neoclipse visualization
+
+ After filling the database I wanted to see what the graph looked like. So I checked out two tools that are available for inspecting the graph. First Neoclipse, an
+ eclipse RCP application or plugin that connects to existing graph stores and visualizes their content. After getting an exception about concurrent access, I learned
+ that I have to use Neoclipse in readonly mode when my webapp had an active connection to the store. Good to know.
+
+ TODO neoclipse image
+
+
+
+ Hardcore "Hacking" - Neo4j Shell
+
+ Besides my movies and actors connected by ACTS_IN relationships there were some other nodes. The reference node which is kind of a root node in Neo4j and can be used
+ to anchor subgraphs for easier access. And Spring Data Graph also represented the type hierarchy of my entities in the graph. Obviously for some internal housekeeping
+ and type checking.
+
+ For us console junkies there is also a shell that can reach into a running neo4j store (if that one was started with enableRemoteShell) or provide readonly access
+ to a graph store directory.
+
+
+
+
+ It uses some shell metaphors like cd and ls to navigate the graph. There are also more advanced commands like using indexes and traversals. I tried to play around with them
+ in this shell sesson.
+
+
+
+
+
+
diff --git a/src/docbkx/tutorial/security.xml b/src/docbkx/tutorial/security.xml
index cef8f2da7..7470f8669 100644
--- a/src/docbkx/tutorial/security.xml
+++ b/src/docbkx/tutorial/security.xml
@@ -1,8 +1,98 @@
- Adding security
+ Protecting Assets - Adding Security
+ To use the user in the webapp I had to put it in the session and add login and registration pages. Of course the pages that only worked with a valid user
+account had to be secured as well.
+
+I used Spring Security to that, writing a simple UserDetailsService that used my repository for looking up the users and validating their credentials. The config is located
+in a separate applicationContext-security.xml.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ]]>
+ userFinder;
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ userFinder = finderFactory.createNodeEntityFinder(User.class);
+ }
+
+ @Override
+ public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException, DataAccessException {
+ final User user = findUser(login);
+ if (user==null) throw new UsernameNotFoundException("Username not found",login);
+ return new CineastsUserDetails(user);
+ }
+
+ public User findUser(String login) {
+ return userFinder.findByPropertyValue("users","login",login);
+ }
+}
+
+public class CineastsUserDetails implements UserDetails {
+ private final User user;
+
+ public CineastsUserDetails(User user) {
+ this.user = user;
+ }
+
+ @Override
+ public Collection getAuthorities() {
+ User.Roles[] roles = user.getRoles();
+ if (roles ==null) return Collections.emptyList();
+ return Arrays.asList(roles);
+ }
+
+ @Override
+ public String getPassword() {
+ return user.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+ return user.getLogin();
+ }
+
+....
+ public User getUser() {
+ return user;
+ }
+}
+
+ ]]>
+
+
+ After that a logged in user was available in the session and could so be used for all the social interactions. Most of the work done next was adding controller methods
+and JSPs for the views.
diff --git a/src/docbkx/tutorial/setup.xml b/src/docbkx/tutorial/setup.xml
index f0fd0b077..6c54f47ff 100644
--- a/src/docbkx/tutorial/setup.xml
+++ b/src/docbkx/tutorial/setup.xml
@@ -1,44 +1,46 @@
- Project setup
+ Scope: Spring
- ...
+ Being a Spring Developer, I would, of course, choose components of the Spring Framework to do most of the work. I'd already come up with the ideas -
+ that should be enough.
+
+ What database would fit both the complex network of cineasts, movies, actors, roles, ratings and friends? And also be able to support the
+ recommendation algorithms that I thought of? I had no idea.
+
+ But, wait, there was the new Spring Data project that started in 2010 bringing
+ the convenience of the Spring programming model to NoSQL databases. That should fit my experience and help me getting started. I looked
+ at the list of projects supporting the different NoSQL databases. Only one mentioned the kind of social network I was thinking of -
+ Spring Data Graph for Neo4j, a graph database. Neo4j's pitch of "value in relationships" and the accompanying docs looked like what I needed.
+ I decided to give it a try.
- Spring Data Graph
+ Preparations - Required Setup
- ...
+ To setup the project I created a public github account and began setting up the infrastructure for a spring web project using maven as build
+ system. So I added the dependencies for the springframework libraries, put the web.xml for the DispatcherServlet and the applicationContext.xml
+ in the webapp directory.
+
+
+
+
+
+
+ With this setup I was ready for the first spike: creating a simple MovieController showing a static view. Check. Next was the setup for Spring Data Graph.
+ I looked at the README at github and then checked it with the manual. Quite a lot of maven setup for aspectj but otherwise not so much to add.
+ I added just a few lines to my spring configuration.
+
+
+
+
+
+
+ I spun up jetty to see if there were any obvious issues with the config. Check.
-
- Maven configuration
-
- ...
-
-
-
- Spring configuration
-
- ...
-
-
-
-
- Spring MVC
-
- ...
-
-
- Maven configuration
-
- ...
-
-
-
- Spring configuration
-
- ...
-
-
diff --git a/src/docbkx/tutorial/social.xml b/src/docbkx/tutorial/social.xml
index 2ce723e61..88b696293 100644
--- a/src/docbkx/tutorial/social.xml
+++ b/src/docbkx/tutorial/social.xml
@@ -1,8 +1,79 @@
- Adding social
+ Movies 2.0 - Adding social
- ...
+ But this was just a plain old movie database (POMD). My idea of socializing this business was not realized.
+
+ See, mom a Cineast! - Users
+
+ So I took the User class that I already coded up before and made it a full fledged Spring Data Graph member.
+
+
+ ratings;
+
+ @RelatedTo(elementClass=User.class, type="FRIEND")
+ Set friends;
+
+ public Rating rate(Movie movie, int stars, String comment) {
+ return relateTo(movie, Rating.class, "RATED").rate(stars, comment);
+ }
+ public void befriend(User user) {
+ this.friends.add(user);
+ }
+}
+class Rating {
+ @StartNode User user;
+ @EndNode Movie movie;
+ int stars;
+ String comment;
+ public Rating rate(int stars, String comment) {
+ this.stars=stars; this.comment = comment;
+ return this;
+ }
+}
+ ]]>
+
+
+
+
+ Beware, Critics - Rating
+
+ I also put a ratings field into the movie to be able to show its ratings. And a method to average the stars it got.
+
+
+ ratings;
+
+ public int getStars() {
+ int stars, int count;
+ for (Rating rating : ratings) {
+ stars += rating.getStars(); count++;
+ }
+ return count == 0 ? 0 : stars / count;
+ }
+}
+ ]]>
+
+
+ Fortunately my tests showed my the division by zero error when calculating the stars for a movie without ratings. I also added a few user and ratings to the
+database population code. And three methods to rate movies, lookup users and add friends to the repository.
+
+
+
+
+
diff --git a/src/docbkx/tutorial/spring-data-graph.xml b/src/docbkx/tutorial/spring-data-graph.xml
new file mode 100644
index 000000000..bceab5e2c
--- /dev/null
+++ b/src/docbkx/tutorial/spring-data-graph.xml
@@ -0,0 +1,10 @@
+
+
+
+ Conjuring Magic - Spring Data Graph
+
+ But that was the pure graph database. Using this in my domain would pollute my classes with lots of graph database details. I didn't want that. Spring Data Graph
+ promised to do the heavy lifting for me. So I checked that next. Obviously it heavily depended on aspectj magic. So there would be certain behavour that was
+ just observable without being visible in my code. But I was going to give it a try.
+
+
diff --git a/src/docbkx/tutorial/user-experience.xml b/src/docbkx/tutorial/user-experience.xml
new file mode 100644
index 000000000..dc2b62953
--- /dev/null
+++ b/src/docbkx/tutorial/user-experience.xml
@@ -0,0 +1,9 @@
+
+
+
+ Oh the Glamour - More UI
+
+ TODO screenshots
+
+
+
diff --git a/src/docbkx/tutorial/webapp.xml b/src/docbkx/tutorial/webapp.xml
new file mode 100644
index 000000000..251e5d904
--- /dev/null
+++ b/src/docbkx/tutorial/webapp.xml
@@ -0,0 +1,49 @@
+
+
+
+ Showing off - Web views
+
+ After I had the means to put some data in the graph database, I also wanted to show it. So adding the controller method to show a single movie with its attributes
+and cast in a jsp was straightforward. Actually just using the repository to look the movie up and add it to the model. Then forward to the /movies/show view and voilá.
+
+ TODO screenshot of movie display
+
+
+
+ What was his name? - Searching
+
+ The next thing was to allow users to search for some movies. So I needed some fulltext-search capabilities. As the index provider implementation of Neo4j builds on
+lucene I was delighted to see that fulltext indexes are supported out of the box.
+
+
+ So I happily annotated the title field of my Movie class with @Index(fulltext=true) and was told with an exception that I have to specify a separate index name for that.
+So it became @Indexed(fulltext = true, indexName = "search"). The corresponding finder method is called findAllByQuery. So there was my second repository method for
+searching movies. To restrict the size of the returned set I just added a limit for now that cuts the result after that many entries.
+
+
+ searchForMovie(String query, int count) {
+ List movies=new ArrayList(count);
+ for (Movie movie : movieFinder.findAllByQuery("title", query)) {
+ movies.add(movie);
+ if (count-- == 0) break;
+ }
+ return movies;
+}
+]]>
+
+
+
+ Look what i've found - Listing Results
+
+
+ I then used this result in the controller to render a list of movies driven by a search box. The movie properties and the cast was accessed by the getters in the
+domain classes.
+
+
+
+
+
+