<?xml version="1.0" encoding="UTF-8"?> | |
<!-- | |
Licensed to the Apache Software Foundation (ASF) under one or more | |
contributor license agreements. See the NOTICE 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 Query Handler</title> | |
<author email="Sean.Kelly@jpl.nasa.gov">Sean Kelly</author> | |
</properties> | |
<body> | |
<section name="Developing a Query Handler"> | |
<p>In the <a href="../ps/">last tutorial</a>, 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. | |
</p> | |
<p>To do this tutorial, you'll need mastery of two things:</p> | |
<ul> | |
<li>Using the <code>XMLQuery</code> class. Follow the <a | |
href="/edm-query/tutorial/">query expression tutorail</a> | |
now if you're not familiar with it. | |
</li> | |
<li>Running and querying a product server. Follow the <a | |
href="../ps/">Your First Product Server</a> 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. | |
</li> | |
</ul> | |
</section> | |
<section name="Serving Up Constants"> | |
<p>Product servers delegate to query handlers. It's the job of | |
query handlers to interpret incoming queries (expressed as | |
<code>XMLQuery</code> objects), search for, retrieve, convert, | |
or synthesize matching product results, adorn the | |
<code>XMLQuery</code> object with <code>Result</code> 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 | |
<code>XMLQuery</code> back to the product client that made the | |
query in the first place. | |
</p> | |
<p>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 Flajolet 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. | |
</p> | |
<p>The approach we'll take has three simple steps:</p> | |
<ol> | |
<li>Get some handy constants.</li> | |
<li>Define the query expression.</li> | |
<li>Write a query handler. The query handler will: | |
<ol> | |
<li>Examine the query expression to see if it's a request | |
for a constant, and if so, what constant is | |
requested. | |
</li> | |
<li>Examine the query's list of acceptable MIME types.</li> | |
<li>If both check out, look up the desired constant's value.</li> | |
<li>If found, add it as a <code>Result</code> in the <code>XMLQuery</code>.</li> | |
</ol> | |
</li> | |
</ol> | |
</section> | |
<section name="Writing the Code"> | |
<p>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. | |
</p> | |
<subsection name='Gathering Handy Constants'> | |
<p>The wonderful world of science and mathematics is replete | |
with useful constant values. For this example, let's just pick three: | |
</p> | |
<ul> | |
<li><var>pi</var> = 3.14159265...</li> | |
<li><var>e</var> = 2.7182818285...</li> | |
<li><var>gamma</var> = 0.577215664...</li> | |
</ul> | |
<p>In Java code, we can set up those values as a | |
<code>Map</code> in a static field. Thus we start forming our | |
source file, <code>ConstantHandler.java</code>: | |
</p> | |
<source>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..."); | |
} | |
}</source> | |
<p>As you can see, we're storing both the constant name and its | |
value as <code>java.lang.String</code> objects. | |
</p> | |
</subsection> | |
<subsection name='Defining the Query Expression'> | |
<p>Recall that the <code>XMLQuery</code> 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. | |
</p> | |
<p>Using the XMLQuery's expression language, we'll look for | |
queries of the form:</p> | |
<p><code>constant = <var>name</var></code></p> | |
<p>where <var>name</var> is the name of a constant. That will | |
form a postfix "where" stack with exactly three | |
<code>QueryElement</code> objects on it: | |
</p> | |
<ol> | |
<li>The first (top) <code>QueryElement</code> will have role = | |
<code>elemName</code> and value = <code>constant</code>. | |
</li> | |
<li>The second (middle) <code>QueryElement</code> will have | |
role = <code>LITERAL</code> and a value equal to the | |
constant <var>name</var>. | |
</li> | |
<li>The third (bottom) <code>QueryElement</code> will have | |
role = <code>RELOP</code> and value = <code>EQ</code>. | |
</li> | |
</ol> | |
<p>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 <code>donutsEaten > 5 AND RETURN = | |
episodeNumber</code> may be handled by a | |
<code>SimpsonsEpisodeQueryHandler</code> that's <em>also</em> | |
installed in the same product server. | |
</p> | |
<p>We'll define a utility method, <code>getConstantName</code>, | |
that will take the <code>XMLQuery</code>, check for the | |
postfix "where" stack as described, and return the matching | |
constant <var>name</var>. If it gets a stack whose structure | |
doesn't match, it will return <code>null</code>. We'll add | |
this method to our <code>ConstantHandler.java</code> file: | |
</p> | |
<source>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(); | |
} | |
}</source> | |
<p>Here, we first check to make sure there's exactly three | |
elements, returning null if not. There's no need to go further. | |
</p> | |
<p>Assuming there's three elements, the code then checks the | |
topmost element. For an expression <code>constant = | |
<var>name</var></code>, the topmost element will have role | |
<code>elemName</code> and value <code>constant</code>. If | |
neither condition is true, we return null right away. No need | |
to check further. | |
</p> | |
<p>If the topmost element checks out, we then check the | |
bottommost element. For <code>constant = | |
<var>name</var></code>, the bottom element is generated from | |
the equals sign. It will have role <code>RELOP</code> | |
(relational operator) and value <code>EQ</code>, meaning | |
"equals". | |
</p> | |
<p>If it checks out, all we have to do is check the middle | |
element. The infix expression <code>constant = | |
<var>name</var></code> generates a postfix middle element of | |
<var>name</var> as the value, with a role of | |
<code>LITERAL</code>. We make sure it's <code>LITERAL</code>. | |
If not, we're done; it's not a valid expression for our query | |
handler. | |
</p> | |
<p>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. | |
</p> | |
</subsection> | |
<subsection name='Checking for Acceptable MIME Types'> | |
<p>Since all of our mathematical constants are strings, we'll | |
say that the result MIME type of our products is | |
<code>text/plain</code>. That means that any incoming | |
<code>XMLQuery</code> must include any of the following MIME types: | |
</p> | |
<ol> | |
<li><code>text/plain</code></li> | |
<li><code>text/*</code></li> | |
<li><code>*/*</code></li> | |
</ol> | |
<p>All of these match <code>text/plain</code>, 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 <code>image/jpeg</code> ... but I wouldn't | |
want to.) | |
</p> | |
<p>To support this in our query handler, we'll write another | |
utility method. It'll be called | |
<code>isAcceptableType</code>, and it will take the | |
<code>XMLQuery</code> 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 <code>true</code>, and the | |
caller can continue to process the query. If not, it will | |
return <code>false</code>, and the query handler will stop | |
processing and return the <code>XMLQuery</code> unadorned with | |
any results. | |
</p> | |
<p>Here's the code:</p> | |
<source>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; | |
} | |
}</source> | |
<p>Here, we check if the list of acceptable MIME types is empty. | |
An empty list is the same as saying <code>*/*</code>, so that | |
automatically says we've got an acceptable type. For a | |
non-empty list, we go through each type one-by-one. If it's | |
any of the strings <code>text/plain</code>, | |
<code>text/*</code>, or <code>*/*</code>, then that's an | |
acceptable type. | |
</p> | |
<p>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 | |
<code>false</code>. The query handler will check for a | |
<code>false</code> value and return early from handling the | |
query, leaving the <code>XMLQuery</code> untouched. | |
</p> | |
</subsection> | |
<subsection name='Inserting the Result'> | |
<p>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 | |
<code>CONSTANTS</code> map. And assuming it finds a matching | |
constant in that map, it can insert the value as a | |
<code>Result</code> object. | |
</p> | |
<p>To insert the constant's value, we'll develop yet another | |
utility method, this time called <code>insert</code>. This | |
method will take the name of the constant, its value, and the | |
<code>XMLQuery</code>. It will add a <code>Result</code> | |
object to the <code>XMLQuery</code>. When the query handler | |
returns this modified <code>XMLQuery</code> object, the | |
framework will return it to the product client, which can then | |
display the matching result. | |
</p> | |
<p><code>Result</code> objects can also have optional | |
<code>Header</code> 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 <code>real</code> | |
and the units will be <code>none</code>. | |
</p> | |
<p>Here's the code:</p> | |
<source>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); | |
} | |
}</source> | |
<p>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 <code>XMLQuery</code> is just the | |
name of the constant. Its MIME type is | |
<code>text/plain</code>. We set the profile ID and resource | |
ID fields to <code>null</code>, as recommended back in the <a | |
href="/edm-query/tutorial/">XMLQuery Tutorial</a>. 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 <code>false</code>. Also, these | |
mathematical constants should be valid forever, so we set the | |
validity period to <code>Result.INFINITE</code>, a special | |
value that means a never-ending validity period. | |
</p> | |
</subsection> | |
<subsection name='Handling the Query'> | |
<p>With all of these utility methods in hand, it's easy to | |
handle the query now. The | |
<code>jpl.eda.product.QueryHandler</code> interface | |
specifies a single method that we must implement, | |
<code>query</code>. This method accepts an | |
<code>XMLQuery</code> object and returns an | |
<code>XMLQuery</code> object. The returned one may or may | |
not be adorned with matching results. | |
</p> | |
<p>Here's what we have to do:</p> | |
<ol> | |
<li>Get the constant name with <code>getConstantName</code>. | |
If we get <code>null</code>, it means the query's not of | |
the form <code>constant = <var>name</var></code>, so we | |
ignore it. | |
</li> | |
<li>See if the user's willing to accept a | |
<code>text/plain</code> MIME type. If not, we ignore this query. | |
</li> | |
<li>Find the constant in our <code>CONSTANTS</code> map. If | |
it's not there, we ignore this query. | |
</li> | |
<li>Insert the constant's value into the <code>XMLQuery</code>.</li> | |
<li>Returned the modified <code>XMLQuery</code>.</li> | |
</ol> | |
<p>The source:</p> | |
<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; | |
} | |
}</source> | |
</subsection> | |
<subsection name='Complete Source Code'> | |
<p>Here is the complete source file, | |
<code>ConstantHandler.java</code>:</p> | |
<source>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; | |
} | |
}</source> | |
<p>How should you go about compiling this and installing it in | |
a product server? Read on! | |
</p> | |
</subsection> | |
</section> | |
<section name="Compiling the Code"> | |
<p>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. | |
</p> | |
<p>First, let's go back to the <code>$PS_HOME</code> directory | |
we made earlier and make directories to hold both the source | |
code and classes that we'll compile from it: | |
</p> | |
<source>% <b>cd $PS_HOME</b> | |
% <b>mkdir classes src</b></source> | |
<p>Then, create <code>$PS_HOME/src/ConstantHandler.java</code> using | |
your favorite text editor (or by cutting and pasting the source | |
from this page, or whatever). Finally, compile the file as follows: | |
</p> | |
<source>% <b>javac -extdirs lib \ | |
-d classes src/ConstantHandler.java</b> | |
% ls -l classes | |
total 4 | |
-rw-r--r-- 1 kelly kelly 2524 25 Feb 15:46 ConstantHandler.class</source> | |
<p>The <code>javac</code> command is the Java compiler. The | |
<code>-extdirs lib</code> 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 <code>-d classes</code> tells where compiled classes | |
should go. | |
</p> | |
<p>Next, make a jar file that contains your compiled class:</p> | |
<source>% <b>jar -cf lib/my-handlers.jar \ | |
-C classes ConstantHandler.class</b> | |
% <b>jar -tf lib/my-handlers.jar</b> | |
META-INF/ | |
META-INF/MANIFEST.MF | |
ConstantHandler.class</source> | |
<p>We now have a new jar file of our own creation in the | |
<code>$PS_HOME/lib</code> 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. | |
</p> | |
</section> | |
<section name='Specfying the Query Handler'> | |
<p>Query handlers aren't really <em>installed</em> 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. | |
</p> | |
<p>To tell a product server what query handlers to instantiate, | |
you specify a system property called <code>handlers</code>. | |
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 <code>ConstantHandler</code>. | |
</p> | |
<p>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 | |
<code>$PS_HOME/bin/ps</code> file so it reads as follows: | |
</p> | |
<source>#!/bin/sh | |
exec java -Djava.ext.dirs=$PS_HOME/lib \ | |
-Dhandlers=ConstantHandler \ | |
jpl.eda.ExecServer \ | |
jpl.eda.product.rmi.ProductServiceImpl \ | |
urn:eda:rmi:MyProductService</source> | |
<p>We specified a system property on the command line using | |
Java's <code>-D</code> option. This defines the system | |
property <code>handlers</code> as having the value | |
<code>ConstantHandler</code>. Finally, start the product | |
server again by running <code>$PS_HOME/bin/ps</code>. | |
</p> | |
</section> | |
<section name='Querying for Constants'> | |
<p>Once again, edit the <code>$PS_HOME/bin/pc</code> script and | |
change <code>-xml</code> back to <code>-out</code> so that | |
instead of the XML output we'll see the raw product data. | |
Then run it and see what happens: | |
</p> | |
<source>% <b>$PS_HOME/bin/pc 'constant = pi'</b> | |
3.14159265...% </source> | |
<p>Because the raw product data was the string | |
<code>3.14159265...</code> 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 <code>more</code> or <code>less</code> to | |
avoid this. | |
</p> | |
<p>Here's what happened when we ran this command:</p> | |
<ol> | |
<li>The product client created an <code>XMLQuery</code> object | |
out of the string query <code>constant = pi</code>. | |
</li> | |
<li>It asked the RMI Registry to tell it where (network | |
address) it could find the product service named | |
<code>MyProductService</code>. | |
</li> | |
<li>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. | |
</li> | |
<li>The product service had only one query handler, the | |
<code>ConstantHandler</code>, to which to delegate, so it | |
passed the XMLQuery to it. | |
</li> | |
<li>The <code>ConstantHandler</code>'s <code>query</code> | |
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 <code>Result</code> into | |
the <code>XMLQuery</code>, and returned the modified query. | |
</li> | |
<li>The product service, seeing it had no other handlers to | |
try, returned the modified <code>XMLQuery</code> to the | |
product client over the network connection. | |
</li> | |
<li>The product client took the first <code>Result</code> out | |
of the <code>XMLQuery</code>, called | |
<code>Result.getInputStream</code>, and copied each byte of | |
the result to the standard output. This wrote | |
<code>3.14159265...</code> to your window. | |
</li> | |
</ol> | |
<p>If you change the <code>$PS_HOME/bin/pc</code> script again | |
so that instead of <code>-out</code> it's <code>-xml</code>, | |
you'll again see the XMLQuery as an XML document. The interesting part is the <code><queryResultSet></code>: | |
</p> | |
<source><![CDATA[<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>]]></source> | |
<p>I'll let you figure out how this maps to the | |
<code>Result</code> object we created in the code. OK, so is | |
this really interesting? Not really, except to note that the | |
actual result data, <code>3.1415265...</code>, 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 <code>text/plain</code> products | |
appear just as text. Binary data like <code>image/jpeg</code> | |
and <code>application/octet-stream</code> get base-64 encoded | |
into text. | |
</p> | |
<p>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. | |
</p> | |
</section> | |
<section name='Conclusion'> | |
<p>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. | |
</p> | |
<p>Don't toss out this product server yet, though. Another | |
tutorial will use it and cover the infamous | |
<code>LargeProductQueryHandler</code>. | |
</p> | |
</section> | |
</body> | |
</document> |