<?xml version="1.0" encoding="UTF-8"?> | |
<!-- | |
Licensed to the Apache Software Foundation (ASF) under one or more contributor | |
license agreements. See the NOTICE.txt file distributed with this work for | |
additional information regarding copyright ownership. The ASF licenses this | |
file to you under the Apache License, Version 2.0 (the "License"); you may not | |
use this file except in compliance with the License. You may obtain a copy of | |
the License at | |
http://www.apache.org/licenses/LICENSE-2.0 | |
Unless required by applicable law or agreed to in writing, software | |
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | |
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | |
License for the specific language governing permissions and limitations under | |
the License. | |
--> | |
<document> | |
<properties> | |
<title>Developing a Real Profile Handler</title> | |
<author email="Sean.Kelly@jpl.nasa.gov">Sean Kelly</author> | |
</properties> | |
<!-- Wizzy Noise - Haiku --> | |
<body> | |
<section name="Developing a Real Profile Handler"> | |
<p>In the <a href="../handler/">basic profile handler | |
tutorial</a>, we developed a profile handler that answered | |
every query with a list of exactly zero profile objects. It | |
also responded to requests to retrieve a profile by a | |
specific ID with <code>null</code>, meaning "not found by | |
this handler." Useful, huh? Not really. | |
</p> | |
<p>But it did get our profile server ready for <em>this</em> | |
tutorial, where we'll write a <em>real</em> profile handler | |
that analyzes incoming queries, consults a local "database", | |
constructs a set of matching profile results, and responds to | |
requests to profiles by ID. Get some fresh coffee, because | |
this is going to be a tough one. | |
</p> | |
<p>And yes, you'll need to have gone through <em>all</em> of the | |
following before proceeding: | |
</p> | |
<ul> | |
<li><a href="../info/">Information Captured in a Profile</a></li> | |
<li><a href="../querying/">Querying Profiles</a></li> | |
<li><a href="../rep">Profile Representation</a></li> | |
<li><a href="../handler">Developing a Profile Handler</a></li> | |
</ul> | |
</section> | |
<section name="The Music Database"> | |
<p>Let's say you've got an OODT <a href="/grid-product/">product | |
server</a> already running that serves up your favorite | |
music files. All you have to do is pass in the URI to a | |
track and it spits back the MP3 data which can run into your | |
favorite media player. You've set it up so your URIs are | |
all unique for each track, and you just have to pass in an | |
unparsed XMLQuery like <code>urn:sk:tr5B7E.mp3</code> and | |
you get the matching data. | |
</p> | |
<p>But since you're not in the habit of memorizing hexadecimal | |
numbers inside of URIs, let's write a profile server who's job | |
it is to take queries for specific artists, genres, albums, | |
ratings, track titles, and so forth, and spit out the matching | |
profiles. The profiles have a places for a URI (in the | |
<code>Identifier</code> field) that you can then pass to the | |
hypothetical product server to get the track data. (In fact, | |
this is a common OODT pattern: profile query to do resource | |
location, product query to do resource retrieval.) While | |
you're listening to the track, you can read all sorts of other | |
juicy metadata about it by examining the returned profile. | |
</p> | |
<subsection name="The Metadata"> | |
<p>For this demonstration, we'll just focus on three kinds of | |
metadata instead of going all-out, <a | |
href="http://www.apple.com/itunes/">iTunes</a> style: | |
</p> | |
<ul> | |
<li>Artist name</li> | |
<li>Album name</li> | |
<li>Track name</li> | |
</ul> | |
<p>Each profile will describe a single track. The resource | |
attributes will have: | |
</p> | |
<ul> | |
<li>The URI of the track as the <code>Identifier</code>.</li> | |
<li>The name of the track as the <code>Title</code>.</li> | |
<li>The name of the artist as the <code>Creator</code>.</li> | |
</ul> | |
<p>In addition, we'll put in two profile elements:</p> | |
<ul> | |
<li>The name of the album as an | |
<code>EnumeratedProfileElement</code>. | |
</li> | |
<li>The name of the artist as an | |
<code>EnumeratedProfileElement</code>. Yes, this is | |
redundant with the artist named as the <code>Creator</code> | |
in the resource attributes; but one profile element by | |
itself would get too lonely!</li> | |
</ul> | |
</subsection> | |
<subsection name="Query Style"> | |
<p>Both product handlers and profile handlers get to choose | |
whether they want unparsed query expressions in their | |
XMLQuery objects or if they want parsed query expressions. | |
Parsed query expressions generate the "where" boolean stack. | |
While the product server that this profile server is meant | |
to work with wants unparsed ones, we'll use parsed | |
expressions for this profile handler. Why? Well, having a | |
well-defined query language and a way to operate on it will | |
save a little trouble from us having to generate a parser. | |
</p> | |
<p>The queries will use element names <code>artist</code>, | |
<code>album</code>, and <code>track</code> only, to match | |
what we'll save in our music database. Here are a couple example queries: | |
</p> | |
<source>artist = Beatles AND album = Revolver | |
track = 'Blue Suede Shoes'</source> | |
</subsection> | |
</section> | |
<section name="Developing the Handler"> | |
<p>We'll develop the handler in parts, so we can discuss each | |
section, and then show the entire source file. | |
</p> | |
<subsection name='Making the "Database"'> | |
<p>Our music database will be nothing more than Java objects | |
kept in memory. We'll create separate objects of three | |
classes: | |
</p> | |
<ul> | |
<li><code>Artist</code>. <code>Artist</code> objects represent | |
people or groups who create music. <code>Artist</code>s | |
will have zero or more <code>Track</code>s. | |
</li> | |
<li><code>Album</code>. <code>Album</code> objects are | |
collections of <code>Track</code>s. | |
</li> | |
<li><code>Track</code>. <code>Track</code> objects appear | |
on one <code>Album</code> and are made by one | |
<code>Artist</code>. They have the URN necessary to pass | |
to the hypothetical music product server in order to | |
actually play music. | |
</li> | |
</ul> | |
<p>A better music model would probably separate out artists | |
and composers, account for remixes, compilation albums, | |
re-issues, multiple renditions, and so forth, but this is | |
government work, and it'll do. | |
</p> | |
<p>Here's class <code>Artist</code>:</p> | |
<source>class Artist { | |
public Artist(String name) { | |
this.name = name; | |
tracks = new ArrayList(); | |
} | |
public String getName() { | |
return name; | |
} | |
public List getTracks() { | |
return tracks; | |
} | |
public int hashCode() { | |
return name.hashCode(); | |
} | |
public boolean equals(Object obj) { | |
if (obj == this) return true; | |
if (!(obj instanceof Artist)) return false; | |
Artist rhs = (Artist) obj; | |
return name.equals(rhs.name); | |
} | |
private String name; | |
private List tracks; | |
}</source> | |
<p>As you can see, <code>Artist</code>s have a name and a | |
<code>List</code> of <code>Track</code>s they've made. Now, | |
here's class <code>Album</code>: | |
</p> | |
<source>class Album { | |
public Album(String name) { | |
this.name = name; | |
tracks = new ArrayList(); | |
} | |
public String getName() { | |
return name; | |
} | |
public List getTracks() { | |
return tracks; | |
} | |
public int hashCode() { | |
return name.hashCode(); | |
} | |
public boolean equals(Object obj) { | |
if (obj == this) return true; | |
if (!(obj instanceof Album)) return false; | |
Album rhs = (Album) obj; | |
return name.equals(rhs.name); | |
} | |
private String name; | |
private List tracks; | |
}</source> | |
<p>As with <code>Artist</code>s, <code>Album</code>s (or | |
should that be <em>Alba</em>?) have names and collections of | |
<code>Track</code>s. Finally, here's class <code>Track</code>: | |
</p> | |
<source>class Track { | |
public Track(String name, URI id, Artist artist, | |
Album album) { | |
this.name = name; | |
this.id = id; | |
this.artist = artist; | |
this.album = album; | |
artist.getTracks().add(this); | |
album.getTracks().add(this); | |
} | |
public String getName() { return name; } | |
public URI getID() { return id; } | |
public Artist getArtist() { return artist; } | |
public Album getAlbum() { return album; } | |
public int hashCode() { | |
return name.hashCode() ^ id.hashCode(); | |
} | |
public boolean equals(Object obj) { | |
if (obj == this) return true; | |
if (!(obj instanceof Track)) return false; | |
Track rhs = (Track) obj; | |
return id.equals(rhs.id); | |
} | |
private String name; | |
private URI id; | |
private Artist artist; | |
private Album album; | |
}</source> | |
<p>As you can see from the code, a track belongs to an | |
<code>Artist</code> and to an <code>Album</code> and has a | |
URI which you can use to get to the track's MP3 data. | |
Finally, with these three "entity" classes in hand, we can | |
create a music database: | |
</p> | |
<source>class DB { | |
public static Set ARTISTS = new HashSet(); | |
public static Set ALBUMS = new HashSet(); | |
public static Set TRACKS = new HashSet(); | |
static { | |
Artist bach = new Artist("Bach"); | |
Album brandenburg123 | |
= new Album("Brandenburg Concerti 1, 2, 3"); | |
Album brandenburg456 | |
= new Album("Brandenburg Concerti 4, 5, 6"); | |
Track brandenburg1 | |
= new Track("Brandenburg Concerto #1", | |
URI.create("urn:sk:tr91BC.mp3"), bach, | |
brandenburg123); | |
Track brandenburg2 | |
= new Track("Brandenburg Concerto #2", | |
URI.create("urn:sk:tr311E.mp3"), bach, | |
brandenburg123); | |
Track brandenburg3 | |
= new Track("Brandenburg Concerto #3", | |
URI.create("urn:sk:trA981.mp3"), bach, | |
brandenburg123); | |
Track brandenburg4 | |
= new Track("Brandenburg Concerto #4", | |
URI.create("urn:sk:tr233A.mp3"), bach, | |
brandenburg456); | |
Track brandenburg5 | |
= new Track("Brandenburg Concerto #5", | |
URI.create("urn:sk:trA6E5.mp3"), bach, | |
brandenburg456); | |
Track brandenburg6 | |
= new Track("Brandenburg Concerto #6", | |
URI.create("urn:sk:tr01E9.mp3"), bach, | |
brandenburg456); | |
Artist delerium = new Artist("Delerium"); | |
Album semantic = new Album("Semantic Spaces"); | |
Album poem = new Album("Poem"); | |
Track flowers | |
= new Track("Flowers Become Screens", | |
URI.create("urn:sk:tr3A5E.mp3"), delerium, | |
semantic); | |
Track metaphor = new Track("Metaphor", | |
URI.create("urn:sk:tr0E13.mp3"), delerium, | |
semantic); | |
Track innocente = new Track("Innocente", | |
URI.create("urn:sk:tr004A.mp3"), delerium, | |
poem); | |
Track aria = new Track("Aria", | |
URI.create("urn:sk:tr004A.mp3"), delerium, | |
poem); | |
ARTISTS.add(bach); | |
ARTISTS.add(delerium); | |
ALBUMS.add(brandenburg123); | |
ALBUMS.add(brandenburg456); | |
ALBUMS.add(semantic); | |
ALBUMS.add(poem); | |
TRACKS.add(brandenburg1); | |
TRACKS.add(brandenburg2); | |
TRACKS.add(brandenburg3); | |
TRACKS.add(brandenburg4); | |
TRACKS.add(brandenburg5); | |
TRACKS.add(brandenburg6); | |
TRACKS.add(flowers); | |
TRACKS.add(metaphor); | |
TRACKS.add(innocente); | |
TRACKS.add(aria); | |
} | |
}</source> | |
<p>(Please don't judge this limited collection as the breadth | |
of my listening tastes. It's actually much narrower now!) | |
In this small database, we've got two artists, Bach and | |
Delerium, with four albums: <i>Brandenburg Concerti 1, 2, | |
3</i> and <i>4, 5, 6</i>; and <i>Semantic Spaces</i> and | |
<i>Poem</i>. And we've got 10 tracks: 3 belonging to one | |
album, 3 belonging to another, 2 belonging to yet another, | |
and the last 2 belonging to the last album. Six are by | |
Bach, and four by Delerium. Each track has | |
</p> | |
</subsection> | |
<subsection name="Querying our Database"> | |
<p>Recall that the <a href="/edm-query/">XMLQuery</a>'s query | |
language uses triples of the form (element, relation, | |
literal) like <code>album != Poem</code>. The relations | |
include =, !=, <, >, <=, >=, LIKE, and NOTLIKE. | |
The triples are linked with AND, OR, and NOT. For this | |
tutorial, we'll do the = and != cases. The rest you can | |
fill in for your own edification. Our approach will be to | |
examine the postfix "where" boolean stack and convert it | |
into an infix boolean expression tree. We'll ask the tree | |
to evaluate itself into a matching set of | |
<code>Track</code>s. Then all we have to do is descibe the | |
matching <code>Track</code>s as <code>Profile</code> | |
objects. | |
</p> | |
<p>Let's start by defining a node in our expression tree:</p> | |
<source>interface Expr { | |
Set evaluate(); | |
}</source> | |
<p>The <code>evaluate</code> method means "evaluate into a | |
<code>Set</code> of matching <code>Track</code> objects." | |
With this interface, we can then define classes that make up | |
different flavors of tree nodes. One of the easier ones is | |
a constant tree node that either matches <em>every</em> | |
track available (constant true) or <em>none</em> of them | |
(constant false): | |
</p> | |
<source>class Constant implements Expr { | |
public Constant(boolean value) { | |
this.value = value; | |
} | |
public Set evaluate() { | |
return value? DB.TRACKS | |
: Collections.EMPTY_SET; | |
} | |
private boolean value; | |
}</source> | |
<p>Next, let's do negation. This takes the set complement of | |
an existing tree node: | |
</p> | |
<source>class Not implements Expr { | |
public Not(Expr expr) { | |
this.expr = expr; | |
} | |
public Set evaluate() { | |
Set matches = expr.evaluate(); | |
Set inverse = new HashSet(); | |
for (Iterator i = DB.TRACKS.iterator(); | |
i.hasNext();) { | |
Track t = (Track) i.next(); | |
if (!matches.contains(t)) | |
inverse.add(t); | |
} | |
return inverse; | |
} | |
private Expr expr; | |
}</source> | |
<p>As you can see, this node is constructed with another tree | |
node expression. To evaluate this node, we evaluate the | |
expression passed in. Then we take its inverse by iterating | |
through each track in the database and adding it to the | |
matching set if it <em>doesn't</em> occur in the | |
expression's matching set. | |
</p> | |
<p>The union tree node takes two expressions and adds the two | |
sets of matching tracks together: | |
</p> | |
<source>class Or implements Expr { | |
public Or(Expr lhs, Expr rhs) { | |
this.lhs = lhs; | |
this.rhs = rhs; | |
} | |
public Set evaluate() { | |
Set left = lhs.evaluate(); | |
Set right = rhs.evaluate(); | |
left.addAll(right); | |
return left; | |
} | |
private Expr lhs; | |
private Expr rhs; | |
}</source> | |
<p>The intersection tree node evaluates to <code>Track</code>s | |
that occur only in both expressions' tracks: | |
</p> | |
<source>class And implements Expr { | |
public And(Expr lhs, Expr rhs) { | |
this.lhs = lhs; | |
this.rhs = rhs; | |
} | |
public Set evaluate() { | |
Set left = lhs.evaluate(); | |
Set right = rhs.evaluate(); | |
left.retainAll(right); | |
return left; | |
} | |
private Expr lhs; | |
private Expr rhs; | |
}</source> | |
<p>With these nodes, we can cover the logical operators AND, | |
OR, and NOT that appear in a postfix "where" stack, as well | |
as an empty "where" stack, which, by convention, is meant to | |
be a constant "true", matching all available resources. Now | |
we just have to handle triples (element, relation, literal). | |
First up, comparisons against <code>Artist</code>s: | |
</p> | |
<source>class ArtistExpr implements Expr { | |
public ArtistExpr(String op, String value) { | |
this.op = op; | |
this.value = value; | |
} | |
public Set evaluate() { | |
Set tracks = new HashSet(); | |
if ("EQ".equals(op)) { | |
for (Iterator i = DB.ARTISTS.iterator(); | |
i.hasNext();) { | |
Artist a = (Artist) i.next(); | |
if (a.getName().equals(value)) | |
tracks.addAll(a.getTracks()); | |
} | |
} else if ("NE".equals(op)) { | |
for (Iterator i = DB.ARTISTS.iterator(); | |
i.hasNext();) { | |
Artist a = (Artist) i.next(); | |
if (!a.getName().equals(value)) | |
tracks.addAll(a.getTracks()); | |
} | |
} else throw new | |
UnsupportedOperationException("NYI"); | |
return tracks; | |
} | |
private String op; | |
private String value; | |
}</source> | |
<p>For an expression like <code>artist = Bach</code> or | |
<code>artist != Delerium</code> we use a expression node | |
object of the above class. When it's <code>EQ</code>, we | |
iterate through all the artists in the database and, when | |
the artist's name matches, add all of that artist's tracks | |
to the set of matches. When it's <code>NE</code>, we | |
instead add all of the artists' tracks whose name | |
<em>doesn't</em> match. (The other relational operators, | |
<code>LT</code>, <code>GT</code>, <code>LE</code>, | |
<code>GE</code>, <code>LIKE</code>, and <code>NOTLIKE</code> | |
currently throw an exception. You're welcome to try to | |
implement those.) | |
</p> | |
<p>The <code>AlbumExpr</code> expression node is quite similar:</p> | |
<source>class AlbumExpr implements Expr { | |
public AlbumExpr(String op, String value) { | |
this.op = op; | |
this.value = value; | |
} | |
public Set evaluate() { | |
Set tracks = new HashSet(); | |
if ("EQ".equals(op)) { | |
for (Iterator i = DB.ALBUMS.iterator(); | |
i.hasNext();) { | |
Album a = (Album) i.next(); | |
if (a.getName().equals(value)) | |
tracks.addAll(a.getTracks()); | |
} | |
} else if ("NE".equals(op)) { | |
for (Iterator i = DB.ALBUMS.iterator(); | |
i.hasNext();) { | |
Album a = (Album) i.next(); | |
if (!a.getName().equals(value)) | |
tracks.addAll(a.getTracks()); | |
} | |
} else throw new | |
UnsupportedOperationException("NYI"); | |
return tracks; | |
} | |
private String op; | |
private String value; | |
}</source> | |
<p>(Another exercise for the reader: refactor out common code | |
between these two classes.) Finally, the | |
<code>TrackExpr</code> node is for expressions like | |
<code>track = Poem</code>: | |
</p> | |
<source>class TrackExpr implements Expr { | |
public TrackExpr(String op, String value) { | |
this.op = op; | |
this.value = value; | |
} | |
public Set evaluate() { | |
Set tracks = new HashSet(); | |
if ("EQ".equals(op)) { | |
for (Iterator i = DB.TRACKS.iterator(); | |
i.hasNext();) { | |
Track t = (Track) i.next(); | |
if (t.getName().equals(value)) | |
tracks.add(t); | |
} | |
} else if ("NE".equals(op)) { | |
for (Iterator i = DB.TRACKS.iterator(); | |
i.hasNext();) { | |
Track t = (Track) i.next(); | |
if (!t.getName().equals(value)) | |
tracks.add(t); | |
} | |
} else throw new | |
UnsupportedOperationException("NYI"); | |
return tracks; | |
} | |
private String op; | |
private String value; | |
}</source> | |
<p>For <code>EQ</code>, we just iterate through every track in | |
the database and add it to the set of matching tracks if the | |
names match the name passed into the user's query. For | |
<code>NE</code>, we add them if their names <em>don't</em> match. | |
</p> | |
<p>That completes all the code for the expression tree. Now | |
we can start working on the class that implements the | |
<code>ProfileHandler</code> interface, | |
<code>MusicHandler</code>. Here, we'll build that | |
expression tree with the incoming <code>XMLQuery</code>, | |
which provides its "where" element stack as a postfix | |
boolean expression. Here's the approach: | |
</p> | |
<ol> | |
<li>Make a new, empty stack.</li> | |
<li>For each element in the "where" stack: | |
<ol> | |
<li>If it's an element name (<code>artist</code>, | |
<code>album</code>, <code>track</code>), push the name | |
onto the stack. | |
</li> | |
<li>If it's a literal value (<code>Bach</code>, | |
<code>Poem</code>, etc.), push it onto the stack. | |
</li> | |
<li>If it's a relational operator (<code>EQ</code>, <code>NE</code>, etc.): | |
<ol> | |
<li>Pop two values off.</li> | |
<li>Push an <code>ArtistExpr</code>, <code>AlbumExpr</code>, or <code>TrackExpr</code>.</li> | |
</ol> | |
</li> | |
<li>It it's a logical operator: | |
<ul> | |
<li>For <code>AND</code>, pop two values off and push an <code>And</code> node.</li> | |
<li>For <code>OR</code>, pop two values off and push an <code>Or</code> node.</li> | |
<li>For <code>NOT</code>, pop one value off and push a <code>Not</code> node.</li> | |
</ul> | |
</li> | |
</ol> | |
</li> | |
</ol> | |
<p>In the end, there will be one element left on the stack, an | |
<code>Expr</code> node representing the root of the | |
expression tree. Here's the method of | |
<code>MusicHandler</code> that implements the algorithm: | |
</p> | |
<source>private static Expr transform(XMLQuery q) { | |
Stack stack = new Stack(); | |
for (Iterator i = q.getWhereElementSet() | |
.iterator(); i.hasNext();) { | |
QueryElement e = (QueryElement) i.next(); | |
String keyword = e.getValue(); | |
String type = e.getRole(); | |
if ("elemName".equals(type)) | |
stack.push(keyword); | |
else if ("LITERAL".equals(type)) | |
stack.push(keyword); | |
else if ("RELOP".equals(type)) | |
addRelational(keyword, (String)stack.pop(), | |
(String)stack.pop(), stack); | |
else if ("LOGOP".equals(type)) | |
addLogical(keyword, stack); | |
else throw new | |
IllegalArgumentException("Unknown query " | |
+ type + " type"); | |
} | |
if (stack.size() == 0) | |
return new Constant(true); | |
else if (stack.size() > 1) | |
throw new IllegalArgumentException("Unbalanced" | |
+ " query"); | |
else return (Expr) stack.pop(); | |
}</source> | |
<p>For relational and logical operators, this method defers to | |
two other utility methods, which we'll see shortly. After | |
iterating through the entire "where" set, we check to see if | |
there's an empty stack. That's the case where the user | |
passes in an empty expression, which by convention we'll | |
take to mean they want everything. Otherwise, there should | |
be just one <code>Expr</code> node on the stack, the root of | |
the expression tree. | |
</p> | |
<p>To handle adding a <code>RELOP</code>, we pop two values | |
off, the element name (<code>artist</code>, | |
<code>album</code>, or <code>track</code>), and the literal | |
value the user wants (<code>Bach</code>, <code>Poem</code>, | |
etc.), along with the operator and the stack: | |
</p> | |
<source>private static void addRelational(String op, | |
String value, String kind, Stack stack) { | |
if ("artist".equals(kind)) | |
stack.push(new ArtistExpr(op, value)); | |
else if ("album".equals(kind)) | |
stack.push(new AlbumExpr(op, value)); | |
else if ("track".equals(kind)) | |
stack.push(new TrackExpr(op, value)); | |
else throw new | |
IllegalArgumentException("Unknown profile" | |
+ " element " + kind); | |
}</source> | |
<p>This method then replaces the popped off values with the | |
matching <code>Expr</code> class for artists, albums, or | |
tracks. | |
</p> | |
<p>To handle adding a <code>LOGOP</code>, we pass the logical | |
operator and the entire stack to this method: | |
</p> | |
<source>private static void addLogical(String op, | |
Stack stack) { | |
if ("AND".equals(op)) | |
stack.push(new And((Expr)stack.pop(), | |
(Expr) stack.pop())); | |
else if ("OR".equals(op)) | |
stack.push(new Or((Expr)stack.pop(), | |
(Expr) stack.pop())); | |
else if ("NOT".equals(op)) | |
stack.push(new Not((Expr)stack.pop())); | |
else throw new | |
IllegalArgumentException("Illegal operator " | |
+ op); | |
}</source> | |
<p>With all this code in place we can generate the expression | |
tree. Let's look at an example. Suppose when constructing the | |
<code>XMLQuery</code>, the user passed in</p> | |
<source>artist = Bach and not album = Poem or track != Aria</source> | |
<p>The XMLQuery query language generates a postfix stack of | |
<code>QueryElement</code> objects in the "where" list: | |
</p> | |
<img src="../images/stack.png" alt="Stack" /> | |
<p>And we then create this tree:</p> | |
<img src="../images/tree.png" alt="Tree" /> | |
<p>Calling the root's <code>evaluate</code> method then yields | |
a <code>java.util.Set</code> of <code>Track</code> objects | |
that match that expression. | |
</p> | |
<p>OK, we've got a set of <code>Track</code>s. But what we | |
want are a set of <em><code>Profile</code>s</em>. The next | |
step is to describe those tracks using the profile metadata | |
model. | |
</p> | |
</subsection> | |
<subsection name="Describing Tracks"> | |
<p>Query handlers serve up <code>List</code>s of | |
<code>Profile</code> objects, where <code>Profile</code>s | |
contain metadata descriptions of resources. For this | |
tutorial, the resources we're describing are music tracks, | |
represented by instances of <code>Track</code> objects. | |
When the handler's <code>findProfiles</code> and | |
<code>get</code> methods are called by the OODT framework to | |
service a request, all we have to do is find the matching | |
<code>Track</code> (or <code>Track</code>s) and create | |
matching <code>Profile</code>s. | |
</p> | |
<p>Recall that we're setting up the resource attributes of the | |
profile so that | |
</p> | |
<ul> | |
<li>The URI of the track appears in the <code>Identifier</code>.</li> | |
<li>The name of the track appears in the <code>Title</code>.</li> | |
<li>The name of the artist appears the <code>Creator</code>.</li> | |
</ul> | |
<p>In addition, we'll put in two profile elements:</p> | |
<ul> | |
<li>The name of the album as an | |
<code>EnumeratedProfileElement</code>. | |
</li> | |
<li>The name of the artist redundantly as an | |
<code>EnumeratedProfileElement</code>. | |
</li> | |
</ul> | |
<p>Now, let's create a utility method <code>describe</code> | |
which takes a <code>java.util.Set</code> of matching | |
<code>Track</code>s and yields a <code>java.util.List</code> | |
of corresponding <code>Profile</code>s: | |
</p> | |
<source>private static List describe(Set tracks) { | |
List profiles = new ArrayList(); | |
for (Iterator i = tracks.iterator(); | |
i.hasNext();) { | |
Track t = (Track) i.next(); | |
String id = t.getID().toString(); | |
String trackName = t.getName(); | |
String albumName = t.getAlbum().getName(); | |
String artistName = t.getArtist().getName(); | |
Profile p = createProfile(id, trackName, | |
albumName, artistName); | |
profiles.add(p); | |
} | |
return profiles; | |
}</source> | |
<p>We build a list of <code>Profile</code>s by calling another | |
method, <code>createProfile</code>. It takes the track's | |
URI, its name, the name of the album on which it appears, | |
and the name of the artist who created it, and yields a | |
<code>Profile</code>: | |
</p> | |
<source>private static Profile createProfile(String id, | |
String trackName, String albumName, | |
String artistName) { | |
Profile p = new Profile(); | |
ProfileAttributes pa=new ProfileAttributes(id, | |
"1.0", "profile", "active", "unclassified", | |
/*parent*/null, /*children*/EL, | |
"1.3.6.1.4.1.7655", /*revNotes*/EL); | |
p.setProfileAttributes(pa); | |
ResourceAttributes ra=new ResourceAttributes(p, | |
id, trackName, | |
Collections.singletonList("audio/mpeg"), | |
/*desc*/null, | |
Collections.singletonList(artistName), | |
/*subjects*/EL, /*pubs*/EL, /*contrib*/EL, | |
/*dates*/EL, /*types*/EL, /*sources*/EL, | |
/*langs*/EL, /*relations*/EL, /*covs*/EL, | |
/*rights*/EL, | |
Collections.singletonList("SK.Music"), | |
"granule", "system.productServer", | |
Collections.singletonList("urn:eda:rmi:" | |
+ "MyProductServer")); | |
p.setResourceAttributes(ra); | |
EnumeratedProfileElement artistElem = | |
new EnumeratedProfileElement(p, "artist", | |
"artist", "Name of the artist of a work", | |
"string", "name", /*syns*/EL, /*ob*/true, | |
/*maxOccur*/1, /*comment*/null, | |
Collections.singletonList(artistName)); | |
p.getProfileElements().put("artist", | |
artistElem); | |
EnumeratedProfileElement albumElem = | |
new EnumeratedProfileElement(p, "album", | |
"album", "Name of album where track occurs", | |
"string", "name", /*syns*/EL, /*ob*/true, | |
/*maxOccur*/1, /*comment*/null, | |
Collections.singletonList(albumName)); | |
p.getProfileElements().put("album", | |
albumElem); | |
return p; | |
}</source> | |
<p>The profile attributes say that</p> | |
<ul> | |
<li>The ID of the profile itself is the same as the track's URI.</li> | |
<li>The version of the profile is 1.0.</li> | |
<li>The type is "profile".</li> | |
<li>It's currently active.</li> | |
<li>It's not top-secret, it's "unclassified".</li> | |
<li>It has no parent profile.</li> | |
<li>It has no child profiles.</li> | |
<li>The registration authority has OID 1.3.6.1.4.1.7655</li> | |
<li>There are no revision notes.</li> | |
</ul> | |
<p>The resource attributes say that</p> | |
<ul> | |
<li>The Identifier is the track's URI.</li> <li>The Title is | |
the track's title.</li> <li>The sole Format in which the | |
track is available is <code>audio/mpeg</code>.</li> | |
<li>There's no description.</li> <li>The sole Creator is the | |
name of the artist.</li> <li>There are no subject keywords, | |
publishers, contributors, dates, types, sources, languages, | |
relations, coverages, nor rights.</li> | |
<li>The sole resource context is "Tutorial.Music".</li> | |
<li>The resource's aggregation is "granule", meaning this profile is describing a single, discrete resource.</li> | |
<li>The resource's class is "system.productServer", meaning you need to contact a product server at the resource location to retrieve the resource.</li> | |
<li>The resource location is <code>urn:eda:rmi:MyProductServer</code>.</li> | |
</ul> | |
<p>Finally, the two profile elements tell (again) who the | |
artist was and also on what album the track appears. | |
</p> | |
<p>What's with all the <code>EL</code>s? It's just to save on typing:</p> | |
<source>private static final List EL | |
= Collections.EMPTY_LIST;</source> | |
</subsection> | |
<subsection name="Implementng the Interface"> | |
<p>The <code>ProfileHandler</code> interface stipulates two | |
methods, one for finding profiles given an | |
<code>XMLQuery</code> and another for retrieving a single | |
profile given its ID. With all of these utility methods in | |
place, these are both easy to write. First, the | |
<code>findProfiles</code> method: | |
</p> | |
<source>public List findProfiles(XMLQuery q) { | |
Expr expr = transform(q); | |
Set matches = expr.evaluate(); | |
List profiles = describe(matches); | |
return profiles; | |
}</source> | |
<p>The algorithm should be painfully obvious by now: transform | |
the query to a tree, evaluate the tree into a set of | |
matching tracks, and describe the tracks. | |
</p> | |
<p>The <code>get</code> method takes a profile's ID and | |
returns the matching profile, or <code>null</code> if it's | |
not found. Since we're using the track's ID as the | |
profile's ID as well, we can just iterate through our | |
tracks, find the one with the matching ID, and | |
<code>describe</code> it: | |
</p> | |
<source>public Profile get(String id) { | |
URI uri = URI.create(id); | |
for (Iterator i = DB.TRACKS.iterator(); | |
i.hasNext();) { | |
Track t = (Track) i.next(); | |
if (t.getID().equals(uri)) | |
return createProfile(t.getID().toString(), | |
t.getName(), t.getAlbum().getName(), | |
t.getArtist().getName()); | |
} | |
return null; | |
}</source> | |
</subsection> | |
<subsection name="Complete Source Code"> | |
<p>Don't feel like cutting and pasting all of those code | |
fragments? No problem. All of the source files are | |
available <a href="../examples/src.jar">in a jar</a>. | |
</p> | |
</subsection> | |
</section> | |
<section name="Compiling the Handler"> | |
<p>As with the <a | |
href="../handler/"><code>NullHandler</code></a>, we'll use the | |
J2SDK command-line tools. And if you've gone through the <a | |
href="../handler/"><code>NullHandler</code> tutorial</a>, you've | |
got all the dependent jars in place already. Just put the | |
<code>MusicHandler.java</code> and all the related source files | |
under <code>$PS_HOME/src</code>, compile, and build the jar: | |
</p> | |
<source>% <b>ls src</b> | |
Album.java Expr.java | |
AlbumExpr.java MusicHandler.java | |
And.java Not.java | |
Artist.java NullHandler.java | |
ArtistExpr.java Or.java | |
Constant.java Track.java | |
DB.java TrackExpr.java | |
% <b>javac -extdirs lib -d classes src/*.java</b> | |
% <b>ls classes</b> | |
Album.class Expr.class | |
AlbumExpr.class MusicHandler.class | |
And.class Not.class | |
Artist.class NullHandler.class | |
ArtistExpr.class Or.class | |
Constant.class Track.class | |
DB.class TrackExpr.class | |
% <b>cd classes</b> | |
% <b>jar -uf ../lib/my-handler.jar *.class</b> | |
% <b>cd ..</b> | |
% <b>jar -tf lib/my-handler.jar</b> | |
META-INF/ | |
META-INF/MANIFEST.MF | |
NullHandler.class | |
Album.class | |
AlbumExpr.class | |
And.class | |
Artist.class | |
ArtistExpr.class | |
Constant.class | |
DB.class | |
Expr.class | |
MusicHandler.class | |
Not.class | |
Or.class | |
Track.class | |
TrackExpr.class</source> | |
<p>We also need to update the <code>$PS_HOME/bin/ps</code> | |
script. Currently, it's instantiating just the | |
<code>NullHandler</code>; we need it to instantiate the | |
<code>MusicHandler</code> too. Stop any currently running | |
profile server by pressing CTRL+C (or whatever your interrupt | |
key is) in the window running the server. Then edit the | |
script so it reads as follows: | |
</p> | |
<source>#!/bin/sh | |
exec java -Djava.ext.dirs=$PS_HOME/lib \ | |
-Dhandlers=NullHandler,MusicHandler \ | |
jpl.eda.ExecServer \ | |
jpl.eda.profile.rmi.ProfileServiceImpl \ | |
urn:eda:rmi:MyProfileService</source> | |
<p>Now the profile server will delegate to <em>two</em> | |
handlers: the <code>NullHandler</code> and the | |
<code>MusicHandler</code>. With more than one handler, the | |
OODT framework calls each one in turn and collects all of the | |
matching profiles together to return to the | |
<code>ProfileClient</code>. (Of course, the | |
<code>NullHandler</code> never actually generates any matching | |
profiles.) | |
</p> | |
</section> | |
<section name="Querying the Profile Server"> | |
<p>Start the profile server by running | |
<code>$PS_HOME/bin/ps</code> in one window (presumably the RMI | |
registry is still running in another window). In yet another | |
window, we'll run our <code>$PS_HOME/bin/pc</code> script to | |
query the profile server: | |
</p> | |
<source>% <b>$PS_HOME/bin/pc 'artist = Delerium | |
AND album != Poem OR artist = Bach'</b><![CDATA[ | |
Object context ready; delegating to: | |
[jpl.eda.object.jndi.RMIContext@dec8b3] | |
[<?xml version="1.0" encoding="UTF-8"?> | |
<profile><profAttributes><profId>urn:sk:tr91BC.mp3</profId>...]]></source> | |
<p>Whoa! There's a huge load of XML! In fact what the | |
<code>ProfileClient</code> is printing is a | |
<code>java.util.List</code> of profiles in XML format, each | |
separated by a comma, and the whole list in square brackets. | |
If you search this output carefully, though, you can pick out | |
the <code><Title></code> elements and see indeed that | |
we've got six matching tracks: | |
</p> | |
<ul> | |
<li>Brandenburg Concerto #1</li> | |
<li>Brandenburg Concerto #2</li> | |
<li>Brandenburg Concerto #3</li> | |
<li>Brandenburg Concerto #4</li> | |
<li>Brandenburg Concerto #5</li> | |
<li>Brandenburg Concerto #6</li> | |
<li>Flowers Become Screens</li> | |
<li>Metaphor</li> | |
</ul> | |
<p>Sure enough, this matches the XMLQuery query language | |
expression we passed in: There are tracks by Delerium but | |
<em>not</em> from the Poem album, and there are all the tracks | |
by Bach. | |
</p> | |
</section> | |
<section name='Conclusion'> | |
<p>In this long tutorial we developed a real profile handler | |
that answered queries by transforming them from postfix stacks | |
into expression trees and using those trees to query an | |
in-memory database made of Java objects. We then described | |
matching data by creating <code>Profile</code>s. | |
</p> | |
<p>You might be thinking that this seems like a lot of work, and | |
there might be some easier ways to go. You could use the | |
<code>LightweightProfileHandler</code> for resources that | |
never change, but only if you don't have too many of them and | |
don't mind managing potentially large XML documents. You | |
could choose to use unparsed XMLQuery expressions and instead | |
make the user query in the same language as your data system, | |
obviating the need for complex expression trees. | |
</p> | |
<p>However, with the tools presented in this tutorial, you could | |
adapt the expression tree code to generating system-specific | |
queries, and describe those results with as much or as little | |
detail as necessary. | |
</p> | |
<p>Happy profiling!</p> | |
</section> | |
</body> | |
</document> |