blob: 3c8b913654e97c96d226b49a6fcb3befb499bfd6 [file] [log] [blame]
<?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 =, !=, &lt;, &gt;, &lt;=, &gt;=, 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>&lt;Title&gt;</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>