|
|
In the
basic profile handler
tutorial
, 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
null
, meaning "not found by
this handler." Useful, huh? Not really.
But it did get our profile server ready for
this
tutorial, where we'll write a
real
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.
And yes, you'll need to have gone through
all
of the
following before proceeding:
Let's say you've got an OODT
product
server
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
urn:sk:tr5B7E.mp3
and
you get the matching data.
But since you're not in the habit of memorizing hexadecimal
numbers inside of URIs, let's write a profile ser
ver 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
Identifier
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.
For this demonstration, we'll just focus on three kinds of
metadata instead of going all-out,
iTunes
style:
-
Artist name
-
Album name
-
Track name
Each profile will describe a single track. The resource
attributes will have:
-
The URI of the track as the
Identifier
.
-
The name of the track as the
Title
.
-
The name of the artist as the
Creator
.
In addition, we'll put in two profile elements:
-
The name of the album as an
EnumeratedProfileElement
.
-
The name of the artist as an
EnumeratedProfileElement
. Yes, this is
redundant with the artist named as the
Creator
in the resource attributes; but one profile element by
itself would get too lonely!
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.
The queries will use element names
artist
,
album
, and
track
only, to match
what we'll save in our music database. Here are a couple example queries:
artist = Beatles AND album = Revolver
track = 'Blue Suede Shoes'
We'll develop the handler in parts, so we can discuss each
section, and then show the entire source file.
Our music database will be nothing more than Java objects
kept in memory. We'll create separate objects of three
classes:
-
Artist
.
Artist
objects represent
people or groups who create music.
Artist
s
will have zero or more
Track
s.
-
Album
.
Album
objects are
collections of
Track
s.
-
Track
.
Track
objects appear
on one
Album
and are made by one
Artist
. They have the URN necessary to pass
to the hypothetical music product server in order to
actually play music.
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.
Here's class
Artist
:
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;
}
As you can see,
Artist
s have a name and a
List
of
Track
s they've made. Now,
here's class
Album
:
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;
}
As with
Artist
s,
Album
s (or
should that be
Alba
?) have names and collections of
Track
s. Finally, here's class
Track
:
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;
}
As you can see from the code, a track belongs to
an
Artist
and to an
Album
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:
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);
}
}
(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:
Brandenburg Concerti 1, 2,
3
and
4, 5, 6
; and
Semantic Spaces
and
Poem
. 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
Recall that the
XMLQuery
's query
language uses triples of the form (element, relation,
literal) like
album != Poem
. 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
Track
s. Then all we have to do is descibe the
matching
Track
s as
Profile
objects.
Let's start by defining a node in our expression tree:
interface Expr {
Set evaluate();
}
The
evaluate
method means "evaluate into a
Set
of matching
Track
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
every
track available (constant true) or
none
of them
(constant false):
class Constant implements Expr {
public Constant(boolean value) {
this.value = value;
}
public Set evaluate() {
return value? DB.TRACKS
: Collections.EMPTY_SET;
}
private boolean value;
}
Next, let's do negation. This takes the set complement of
an existing tree node:
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;
}
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
doesn't
occur in the
expression's matching set.
The union tree node takes two expressions and adds the two
sets of matching tracks together:
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;
}
The intersection tree node evaluates to
Track
s
that occur only in both expressions' tracks:
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;
}
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
Artist
s:
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;
}
For an expression like
artist = Bach
or
artist != Delerium
we use a expression node
object of the above class. When it's
EQ
, 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
NE
, we
instead add all of the artists' tracks whose name
doesn't
match. (The other relational operators,
LT
,
GT
,
LE
,
GE
,
LIKE
, and
NOTLIKE
curr
ently throw an exception. You're welcome to try to
implement those.)
The
AlbumExpr
expression node is quite similar:
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;
}
(Another exercise for the reader: refactor out common code
between these two classes.) Finally, the
TrackExpr
node is for expressions like
track = Poem
:
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;
}
For
EQ
, we just iterate through every track in
the database and a
dd it to the set of matching tracks if the
names match the name passed into the user's query. For
NE
, we add them if their names
don't
match.
That completes all the code for the expression tree. Now
we can start working on the class that implements the
ProfileHandler
interface,
MusicHandler
. Here, we'll build that
expression tree with the incoming
XMLQuery
,
which provides its "where" element stack as a postfix
boolean expression. Here's the approach:
-
Make a new, empty stack.
-
For each element in the "where" stack:
-
If it's an element name (
artist
,
album
,
track
), push the name
onto the stack.
-
If it's a literal value (
Bach
,
Poem
, etc.), push it onto the stack.
-
If it's a relational operator (
EQ
,
NE
, etc.):
-
Pop two values off.
-
Push an
ArtistExpr
,
AlbumExpr
, or
TrackExpr
.
-
It it's a logical operator:
-
For
AND
, pop two values off and push an
And
node.
-
For
OR
, pop two values off and push an
Or
node.
-
For
NOT
, pop one value off and push a
Not
node.
In the end, there will be one element left on the stack, an
Expr
node representing the root of the
expression tree. Here's the method of
MusicHandler
that implements the algorithm:
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();
}
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
Expr
node on the stack, the root of
the expression tree.
To handle adding a
RELOP
, we pop two values
off, the element name (
artist
,
album
, or
track
), and the literal
value the user wants (
Bach
,
Poem
,
etc.), along with the operator and the stack:
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);
}
This method then replaces the popped off values with the
matching
Expr
class for artists, albums, or
tracks.
To handle adding a
LOGOP
, we pass the logical
operator and the entire stack to this method:
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);
}
With all this code in place we can generate the expression
tree. Let's look at an example. Suppose when constructing the
XMLQuery
, the user passed in
artist = Bach and not album = Poem or track != Aria
The XMLQuery query language generates a postfix stack of
QueryElement
objects in the "where" list:
And we then create this tree:
Calling the root's
evaluate
method then yields
a
java.util.Set
of
Track
objects
that match that expression.
OK, we've got a set of
Track
s. But what we
want are a set of
Profile
s
. The next
step is to describe those tracks using the profile metadata
model.
Query handlers serve up
List
s of
Profile
objects, where
Profile
s
contain metadata descriptions of resources. For this
tutorial, the resources we're describing are music tracks,
represented by instances of
Track
objects.
When the handler's
findProfiles
and
get
methods are called by the OODT framework to
service a request, all we have to do is find the matching
Track
(or
Track
s) and create
matching
Profile
s.
Recall that we'r
e setting up the resource attributes of the
profile so that
-
The URI of the track appears in the
Identifier
.
-
The name of the track appears in the
Title
.
-
The name of the artist appears the
Creator
.
In addition, we'll put in two profile elements:
-
The name of the album as an
EnumeratedProfileElement
.
-
The name of the artist redundantly as an
EnumeratedProfileElement
.
Now, let's create a utility method
describe
which takes a
java.util.Set
of matching
Track
s and yields a
java.util.List
of corresponding
Profile
s:
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;
}
We build a list of
Profile
s by calling another
method,
createProfile
. 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
Profile
:
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;
}
The profile attributes say that
-
The ID of the profile itself is the same as the track's URI.
-
The version of the profile is 1.0.
-
The type is "profile".
-
It's currently active.
-
It's not top-secret, it's "unclassified".
-
It has no parent profile.
-
It has no child profiles.
-
The registration authority has OID 1.3.6.1.4.1.7655
-
There are no revision notes.
The resource attributes say that
-
The Identifier is the track's URI.
-
The Title is
the track's title.
-
The sole Format in which the
track is available is
audio/mpeg
.
-
There's no description.
-
The sole Creator is the
name of the artist.
-
There are no subject keywords,
publishers, contributors, dates, types, sources, languages,
relations, coverages,
nor rights.
-
The sole resource context is "Tutorial.Music".
-
The resource's aggregation is "granule", meaning this profile is describing a single, discrete resource.
-
The resource's class is "system.productServer", meaning you need to contact a product server at the resource location to retrieve the resource.
-
The resource location is
urn:eda:rmi:MyProductServer
.
Finally, the two profile elements tell (again) who the
artist was and also on what album the track appears.
What's with all the
EL
s? It's just to save on typing:
private static final List EL
= Collections.EMPTY_LIST;
The
ProfileHandler
interface stipulates two
methods, one for finding profiles given an
XMLQuery
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
findProfiles
method:
public List findProfiles(XMLQuery q) {
Expr expr = transform(q);
Set matches = expr.evaluate();
List profiles = describe(matches);
return profiles;
}
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.
The
get
method takes a profile's ID and
returns the matching profile, or
null
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
describe
it:
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;
}
Don't feel like cutting and pasting all of those code
fragments? No problem. All of the source files are
available
in a jar
.
As with the
NullHandler
, we'll use the
J2SDK command-line tools. And if you've gone through the
NullHandler
tutorial
, you've
got all the dependent jars in place already. Just put the
MusicHandler.java
and all the related source files
under
$PS_HOME/src
, compile, and build the jar:
% ls src
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
% javac -extdirs lib -d classes src/*.java
% ls classes
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
% cd classes
% jar -uf ../lib/my-handler.jar *.class
% cd ..
% jar -tf lib/my-handler.jar
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
We also need to update the
$PS_HOME/bin/ps
script. Currently, it's instantiating just the
NullHandler
; we need it to instantiate the
MusicHandler
too. Stop any c
urrently 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:
#!/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
Now the profile server will delegate to
two
handlers: the
NullHandler
and the
MusicHandler
. 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
ProfileClient
. (Of course, the
NullHandler
never actually generates any matching
profiles.)
Start the profile server by running
$PS_HOME/bin/ps
in one window (presumably the RMI
registry is still running in another window). In yet another
window, we'll run our
$PS_HOME/bin/pc
script to
query the profile server:
% $PS_HOME/bin/pc 'artist = Delerium
AND album != Poem OR artist = Bach'
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>...
Whoa! There's a huge load of XML! In fact what the
ProfileClient
is printing is a
java.util.List
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
<
Title
>
elements and see indeed that
we've got six matching tracks:
-
Brandenburg Concerto #1
-
Brandenburg Concerto #2
-
Brandenburg Concerto #3
-
Brandenburg Concerto #4
-
Brandenburg Concerto #5
-
Brandenburg Concerto #6
-
Flowers Become Screens
-
Metaphor
Sure enough, this matches the XMLQuery query language
expression we passed in: There are tracks by Delerium but
not
from the Poem album, and there are all the tracks
by Bach.
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
Profile
s.
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
LightweightProfileHandler
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.
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.
Happy profiling!
|