|
|
In the
last tutorial
, we started a
product server. But this wasn't a very useful product server;
it could answer queries but always respond with no results.
That's because it had no query handlers. Query handlers have
the responsibility of actually handling product queries. In
this tutorial, we'll develop a query handler, install it into
our product server, and query it to see if it works.
To do this tutorial, you'll need mastery of two things:
-
Using the
XMLQuery
class. Follow the
query expression tutorail
now if you're not familiar with it.
-
Running and querying a product server. Follow the
Your First Product Server
tutorial to
get your product server up and running. In this tutorial,
we'll build on that product server, so it's especially
important to have it in good shape.
Product servers delegate to query handlers. It's the job of
query handlers to interpret incoming queries (expressed as
XMLQuery
objects), search for, retrieve, convert,
or synthesize matching product results, adorn the
XMLQuery
object with
Result
objects,
and return the modified query. At that point the OODT
framework takes over again and tries other installed query
handlers, eventually returning the completed
XMLQuery
back to the product client that made the
query in the first place.
We'll make a query handler that serves mathematical
constants. Have you ever been in a position where you needed,
say, the value of the third Fla
jolet number or perhaps
Zeta(9)? No? Well, just pretend for now you did. What we'll
do is develop a query handler for a product server that will
serve values of various mathematical constants.
The approach we'll take has three simple steps:
-
Get some handy constants.
-
Define the query expression.
-
Write a query handler. The query handler will:
-
Examine the query expression to see if it's a request
for a constant, and if so, what constant is
requested.
-
Examine the query's list of acceptable MIME types.
-
If both check out, look up the desired constant's value.
-
If found, add it as a
Result
in the
XMLQuery
.
In this section, we'll build up the query handler source code
in pieces, examining each piece thoroughly. We'll then
present the entire source file.
The wonderful world of science and mathematics is replete
with useful constant values. For this example, let's just pick three:
-
pi
= 3.14159265...
-
e
= 2.7182818285...
-
gamma
= 0.577215664...
In Java code, we can set up those values as a
Map
in a static field. Thus we start forming our
source file,
ConstantHandler.java
:
import java.util.HashMap;
import java.util.Map;
import jpl.eda.product.QueryHandler;
public class ConstantHandler
implements QueryHandler {
private static final Map CONSTANTS = new HashMap();
static {
CONSTANTS.put("pi", "3.14159265...");
CONSTANTS.put("e", "2.7182818285...");
CONSTANTS.put("gamma", "0.577215664...");
}
}
As you can see, we're storing both the constant name and its
value as
java.lang.String
objects.
Recall that the
XMLQuery
class can use parsed
queries (where it generates postfix boolean stacks) or
unparsed ones. While unparsed ones are easier, we'll go with
parsed ones to demonstrate how on the server-side you deal
with those postfix stacks.
Using the XMLQuery's expression language, we'll look for
queries of the form:
constant =
name
where
name
is the name of a constant. That will
form a postfix "where" stack with exactly three
QueryElement
objects on it:
-
The first (top)
QueryElement
will have role =
elemName
and value =
constant
.
-
The second (middle)
QueryElement
will have
role =
LITERAL
and a value equal to the
constant
name
.
-
The third (bottom)
QueryElement
will have
role =
RELOP
and value =
EQ
.
If we get any other kind of stack, we'll reject it and return
no matching results. That's reasonable behavior; after all, a
query for
donutsEaten
>
5 AND RETURN =
episodeNumber
may be handled by a
SimpsonsEpisodeQueryHandler
that's
also
installed in the same product server.
We'll define a utility method,
getConstantName
,
that will take the
XMLQuery
, check for the
postfix "where" stack as described, and return the matching
constant
name
. If it gets a stack whose structure
doesn't match, it will return
null
. We'll add
this method to our
ConstantHandler.java
file:
import java.util.List;
import jpl.eda.xmlquery.XMLQuery;
import jpl.eda.xmlquery.QueryElement;
...
public class ConstantHandler
implements QueryHandler {
...
private static String getConstantName(XMLQuery q) {
List stack = q.getWhereElementSet();
if (stack.size() != 3) return null;
QueryElement e = (QueryElement) stack.get(0);
if (!"elemName".equals(e.getRole())
|| !"constant".equals(e.getValue()))
return null;
e = (QueryElement) stack.get(2);
if (!"RELOP".equals(e.getRole())
|| !"EQ".equals(e.getValue()))
return null;
e = (QueryElement) stack.get(1);
if (!"LITERAL".equals(e.getRole()))
return null;
return e.getValue();
}
}
Here, we first check to make sure there's exactly three
elements, returning null if not. There's no need to go further.
Assuming there's three elements, the code then checks the
topmost element. For an expression
constant =
name
, the topmost element will have role
elemName
and value
constant
. If
neither condition is true, we return null right away. No need
to check further.
If the topmost element checks out, we then check the
bottommost element. For
constant =
name
, the bottom element is generated from
the equals sign. It will have role
RELOP
(relational operator) and value
EQ
, meaning
"equals".
If it checks out, all we have to do is check the middle
element. The infix expression
constant =
name
generates a postfix middle element of
name
as the value, with a role of
LITERAL
. We make sure it's
LITERAL
.
If not, we're done; it's not a valid expression for our query
handler.
But if so, then the value of that query element is the name
of the desired constant. So we return it, regardless of what
it is.
Since all of our mathematical constants are strings, we'll
say that the result MIME type of our products is
text/plain
. That means that any incoming
XMLQuery
must include any of the following MIME types:
-
text/plain
-
text/*
-
*/*
All of these match
text/plain
, which is the only
product type we're capable of serving. (In your own product
servers, you might have more complex logic; for example, you
could write code to draw the numbers into an image file if the
requested type is
image/jpeg
... but I wouldn't
want to.)
To support this in our query handler, we'll write another
utility method. It'll be called
isAcceptableType
, and it will take the
XMLQuery
and examine it to see what MIME types
are acceptable to the caller. If it finds any of the ones in
the above list, it will return
true
, and the
caller can continue to process the query. If not, it will
return
false
, and the query handler will stop
processing and return the
XMLQuery
unadorned with
any results.
Here's the code:
import java.util.Iterator;
...
public class ConstantHandler
implements QueryHandler {
...
private static boolean isAcceptableType(XMLQuery q) {
List mimes = q.getMimeAccept();
if (mimes.isEmpty()) return true;
for (Iterator i = mimes.iterator(); i.hasNext();) {
String type = (String) i.next();
if ("text/plain".equals(type)
|| "text/*".equals(type)
|| "*/*".equals(type)) return true;
}
return false;
}
}
Here, we check if the list of acceptable MIME types is empty.
An empty list is the same as saying
*/*
, so that
automatically says we've got an acceptable type. For a
non-empty list, we go t
hrough each type one-by-one. If it's
any of the strings
text/plain
,
text/*
, or
*/*
, then that's an
acceptable type.
However, if we get through the entire list and we don't find
any type that the user wants that we can provide, we return
false
. The query handler will check for a
false
value and return early from handling the
query, leaving the
XMLQuery
untouched.
Assuming the query handler has found an acceptable MIME type,
and has found a valid query and the name of the desired
constant, it can lookup the constant in the
CONSTANTS
map. And assuming it finds a matching
constant in that map, it can insert the value as a
Result
object.
To insert the constant's value, we'll develop yet another
utility method, this time called
insert
. This
method will take the name of the constant, its value, and the
XMLQuery
. It will add a
Result
object to the
XMLQuery
. When the query handler
returns this modified
XMLQuery
object, the
framework will return it to the product client, which can then
display the matching result.
Result
objects can also have optional
Header
objects that serve as "column headings"
for tabular like results. Our result isn't tabular, it's just
a single value, but we'll add a heading anyway just to
demonstrate how it's done. (You could argue that it's a
one-by-one table, too!) The header's name will be the same as
the constant's name; the data type will be
real
and the units will be
none
.
Here's the code:
import java.util.Collections;
import jpl.eda.xmlquery.Header;
import jpl.eda.xmlquery.Result;
...
public class ConstantHandler
implements QueryHandler {
...
private static void insert(String name,
String value, XMLQuery q) {
Header h = new Header(name, "real", "none");
Result r = new Result(name, "text/plain",
/*profileID*/null, /*resourceID*/null,
Collections.singletonList(h),
value, /*classified*/false, Result.INFINITE);
q.getResults().add(r);
}
}
In this method, we first create the header. Then we create
the result; the result's ID (which differentiates it from
other results in the same
XMLQuery
is just the
name of the constant. Its MIME type is
text/plain
. We set the profile ID and resource
ID fields to
null
, as recommended back in the
XMLQuery Tutorial
. Then we
add our sole header. Then we add the mathematical constant's
value. Finally, this constant isn't classified, so we set the
classified flag to
false
. Also, these
mathematical constants should be valid forever, so we set the
validity period to
Result.INFINITE
, a special
value that means a never-ending validity period.
With all of these utility methods in hand, it's easy to
handle the query now. The
jpl.eda.product.QueryHandler
interface
specifies a single method that we must implement,
query
. This method accepts an
XMLQuery
object and returns an
XMLQuery
object. The returned one may or may
not be adorned with matching results.
Here's what we have to do:
-
Get the constant name with
getConstantName
.
If we get
null
, it means the query's not of
the form
constant =
name
, so we
ignore it.
-
See if the user's willing to accept a
text/plain
MIME type. If not, we
ignore this query.
-
Find the constant in our
CONSTANTS
map. If
it's not there, we ignore this query.
-
Insert the constant's value into the
XMLQuery
.
-
Returned the modified
XMLQuery
.
The source:
public class ConstantHandler
implements QueryHandler {
...
public XMLQuery query(XMLQuery q) {
String name = getConstantName(q);
if (name == null) return q;
if (!isAcceptableType(q)) return q;
String value = (String) CONSTANTS.get(name);
if (value == null) return q;
insert(name, value, q);
return q;
}
}
Here is the complete source file,
ConstantHandler.java
:
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import jpl.eda.product.QueryHandler;
import jpl.eda.xmlquery.Header;
import jpl.eda.xmlquery.Result;
import jpl.eda.xmlquery.QueryElement;
import jpl.eda.xmlquery.XMLQuery;
public class ConstantHandler
implements QueryHandler {
private static final Map CONSTANTS = new HashMap();
static {
CONSTANTS.put("pi", "3.14159265...");
CONSTANTS.put("e", "2.7182818285...");
CONSTANTS.put("gamma", "0.577215664...");
}
private static String getConstantName(XMLQuery q) {
List stack = q.getWhereElementSet();
if (stack.size() != 3) return null;
QueryElement e = (QueryElement) stack.get(0);
if (!"elemName".equals(e.getRole())
|| !"constant".equals(e.getValue()))
return null;
e = (QueryElement) stack.get(2);
if (!"RELOP".equals(e.getRole())
|| !"EQ".equals(e.getValue()))
return null;
e = (QueryElement) stack.get(1);
if (!"LITERAL".equals(e.getRole()))
return null;
return e.getValue();
}
private static boolean isAcceptableType(XMLQuery q) {
List mimes = q.getMimeAccept();
if (mimes.isEmpty()) return true;
for (Iterator i = mimes.iterator(); i.hasNext();) {
String type = (String) i.next();
if ("text/plain".equals(type)
|| "text/*".equals(type)
|| "*/*".equals(type)) return true;
}
return false;
}
private static void insert(String name,
String value, XMLQuery q) {
Header h = new Header(name, "real", "none");
Result r = new Result(name, "text/plain",
/*profileID*/null, /*resourceID*/null,
Collections.singletonList(h),
value, /*classified*/false, Result.INFINITE);
q.getResults().add(r);
}
public XMLQuery query(XMLQuery q) {
String name = getConstantName(q);
if (name == null) return q;
if (!isAcceptableType(q)) return q;
String value = (String) CONSTANTS.get(name);
if (value == null) return q;
insert(name, value, q);
return q;
}
}
How should you go about compiling this and installing it in
a product server? Read on!
We'll compile this code using the J2SDK command-line tools,
but if you're more comfortable with some kind of Integrated
Development Environment (IDE), adjust as necessary.
First, let's go back to the
$PS_HOME
directory
we made earlier and make directories to hold both the source
code and classes that we'll compile from it:
% cd $PS_HOME
% mkdir classes src
Then, create
$PS_HOME/src/ConstantHandler.java
using
your favorite text editor (or by cutting and pasting the source
from this page, or whatever). Finally, compile the file as follows:
% javac -extdirs lib \
-d classes src/ConstantHandler.java
% ls -l classes
total 4
-rw-r--r-- 1 kelly kelly 2524 25 Feb 15:46 ConstantHandler.class
The
javac
command is the Java compiler. The
-extdirs lib
arguments tell the compiler where to
find extension jars. In this case, the code references things
defined in edm-query-2.0.2.jar and grid-product-3.0.3.jar.
The
-d classes
tells where compiled classes
should go.
Next, make a jar file that contains your compiled class:
% jar -cf lib/my-handlers.jar \
-C classes ConstantHandler.class
% jar -tf lib/my-handlers.jar
META-INF/
META-INF/MANIFEST.MF
ConstantHandler.class
We now have a new jar file of our own creation in the
$PS_HOME/lib
directory; this means that the
product server will be able to find out new query handler.
All we have to do now is tell our product server about it.
Query handlers aren't really
installed
into product
servers. What you do is tell the product server what query
handlers you want it to use by naming their classes. The
product server will instantiate an object of each class and,
as queries come in, it will delegate queries to each
instantiated query handler.
To tell a product server what query handlers to instantiate,
you specify a system property called
handlers
.
You set this property to a comma-separated list of class
names. These should be fully-qualified class names (with
package prefixes, if you used packages when making your query
handlers), separated by commas. In this tutorial, we made
just one query handler, and we didn't put it into a package,
so we'll just use
ConstantHandler
.
First, stop any product server you have running now by
pressing CTRL+C (or whatever your interrupt key is) in the
window that was running the product server. Next, modify the
$PS_HOME/bin/ps
file so it reads as follows:
#!/bin/sh
exec java -Djava.ext.dirs=$PS_HOME/lib \
-Dhandlers=ConstantHandler \
jpl.eda.ExecServer \
jpl.eda.product.rmi.ProductServiceImpl \
urn:eda:rmi:MyProductService
We specified a system property on the command line using
Java's
-D
option. This defines the system
property
handlers
as having the value
ConstantHandler
. Finally, start the product
server again by running
$PS_HOME/bin/ps
.
Once again, edit the
$PS_HOME/bin/pc
script and
change
-xml
back to
-out
so that
instead of the XML output we'll see the raw product data.
Then run it and see what happens:
% $PS_HOME/bin/pc 'constant = pi'
3.14159265...%
Because the raw product data was the string
3.14159265...
without any trailing newline, the
shell's prompt appeared right at the end of the product
result. You might try piping the output of the above command
through a pager like
more
or
less
to
avoid this.
Here's what happened when we ran this command:
-
The product client created an
XMLQuery
object
out of the string query
constant = pi
.
-
It asked the RMI Registry to tell it where (network
address) it could find the product service named
MyProductService
.
-
After getting the response back from the RMI Registry, it
then contacted the product service over a network connection
(even if to the same local system) and asked it to handle
the query, passing the query object.
-
The product service had only one query handler, the
ConstantHandler
, to which to delegate, so it
passed the XMLQuery to it.
-
The
ConstantHandler
's
query
method was called. It checked if the query was the kind it
wanted, extracted the desired mathematical constant's name,
checked for an acceptable requested MIME type, looked up the
constant's value, inserted it as a
Result
into
the
XMLQuery
, and returned the modified query.
-
The product service, seeing it had no other handlers to
try, returned the modified
XMLQuery
to the
product client over the network connection.
-
The product client took the first
Result
out
of the
XMLQuery
, called
Result.getInputStream
, and copied each byte of
the result to the standard output. This wrote
3.14159265...
to your window.
If you change the
$PS_HOME/bin/pc
script again
so that instead of
-out
it's
-xml
,
you'll again see the XMLQuery as an XML document. The interesting part is the
<
queryResultSet
>
:
<queryResultSet>
<resultElement classified="false" validity="-1">
<resultId>pi</resultId>
<resultMimeType>text/plain</resultMimeType>
<profId/>
<identifier/>
<resultHeader>
<headerElement>
<elemName>pi</elemName>
<elemType>real</elemType>
<elemUnit>none</elemUnit>
</headerElement>
</resultHeader>
<resultValue xml:space="preserve">3.14159265...</resultValue>
</resultElement>
</queryResultSet>
I'll let you figure out how this maps to the
Result
object we created in the code. OK, so is
this really interesting? Not really, except to note that the
actual result data,
3.1415265...
, appears in the
XML document. In the OODT framework, we call this a "small"
result, because the product data was embedded in the XMLQuery
object. Text data like
text/plain
products
appear just as text. Binary d
ata like
image/jpeg
and
application/octet-stream
get base-64 encoded
into text.
As you can guess, there's a point at which encoded data goes
from being nice and small and tidy to just too large to
contain in a single object. In the OODT framework, we can use
a special query handler for such large products, but that's
another tutorial.
In this tutorial, we learned how to write a complete query
handler for a product server, including handling of postfix
boolean "where" stacks, lists of acceptable MIME types, and
result headers. We compiled the query handler, put it into a
jar file, and specified it to our product server. And we
could even query it and get data back.
Don't toss out this product server yet, though. Another
tutorial will use it and cover the infamous
LargeProductQueryHandler
.
|