During the development of QA systems which are based on the QALL-ME Framework, it often helps to invoke single WSs from a standard Java application and independent from the rest of the QA system. What’s more, it is sometimes desirable to reuse existing WS component implementations from the QALL-ME Framework in another application context. In this section we will make up an example for such an application and we will show how to integrate existing WSs. If you don’t know, yet, how WSs can be invoked from Java, then you may find this out in the following example, too.
In order to be able to follow this tutorial section, you will have to
install the QALL-ME Framework SDK package
along with the DemoKit that you probably already have. Make sure to have the
main contents of both packages in the same directory, i.e., to have the
deploy/
, lib/
, src/
, etc. subdirectories of both packages all in the same
directory. Some files may be contained in both packages, however, it should be
safe to replace files from one package with files from the other. Additionally
make sure to have both the libraries provided by your JAX-WS
package (cf. section 3.1.4) as well as the Java EE 5
API (cf. section 3.1.5) in the classpath.
The sample application that we would like to develop is a temporal expression identifier for multiple languages. The user can pass in some text in any of the supported languages and will get back all temporal expressions that were found in the given text as normalized TIMEX2 annotations. The normalization will be performed relative to the time and date at which the temporal expression annotation was started. Here is the simple skeleton of our tutorial application. WS usage initialization and WS invocation parts are still missing (cf. “TODO” comments):
package com.example; import java.io.Serializable; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.xml.bind.JAXBElement; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import net.sf.qallme.WebServiceTools; import net.sf.qallme.gen.ws.AnnotatedSentence; import net.sf.qallme.gen.ws.InternalServiceFault; import net.sf.qallme.gen.ws.AnnotatedSentence.Annotation; import net.sf.qallme.gen.ws.languageidentification.LanguageIdentifier; import net.sf.qallme.gen.ws.languageidentification.LanguageIdentifierWS; import net.sf.qallme.gen.ws.timeannotation.AnnotationRequest; import net.sf.qallme.gen.ws.timeannotation.ObjectFactory; import net.sf.qallme.gen.ws.timeannotation.TimeAnnotator; import net.sf.qallme.gen.ws.timeannotation.TimeAnnotatorWS; /** * Finds temporal expressions in texts and normalizes them. */ public class TemporalExpressionFinder { /** the WSDL location of the {@link LanguageIdentifier} to use */ private static final URL LANGUAGE_IDENTIFIER_LOC; static { try { LANGUAGE_IDENTIFIER_LOC = new URL("http://localhost:8080/qmfdemo/" + "languageidentification/LanguageIdentifierWS?wsdl"); } catch (MalformedURLException e) { // should never happen throw new IllegalStateException(e); } } /** the WSDL locations of the available {@link TimeAnnotator}s */ private static Map<String, URL> ANNOTATOR_LOCS; static { ANNOTATOR_LOCS = new HashMap<String, URL>(3); try { ANNOTATOR_LOCS.put("de", new URL("http://localhost:8080/qmfdemo/" + "de-timeannotation/TimeAnnotatorWS?wsdl")); ANNOTATOR_LOCS.put("es", new URL("http://localhost:8080/qmfdemo/" + "es-timeannotation/TimeAnnotatorWS?wsdl")); ANNOTATOR_LOCS.put("en", new URL("http://localhost:8080/qmfdemo/" + "en-timeannotation/TimeAnnotatorWS?wsdl")); } catch (MalformedURLException e) { // should never happen throw new IllegalStateException(e); } } /** an object factory for creating requests to the {@link TimeAnnotator} WS */ private static final ObjectFactory OBJ_FACTORY = new ObjectFactory(); /** * an object factory for creating {@link AnnotatedSentence}s in requests to * the {@link TimeAnnotator} WS */ private static final net.sf.qallme.gen.ws.ObjectFactory OBJ_FACTORY2 = new net.sf.qallme.gen.ws.ObjectFactory(); /** the internally used {@link LanguageIdentifier} */ private final LanguageIdentifier langIdentifier; /** the internally used {@link TimeAnnotator}s */ private final Map<String, TimeAnnotator> timeAnnotators; /** the temporal context to normalize temporal expressions against */ private final XMLGregorianCalendar calendar; /** * Constructs and initializes a new {@link TemporalExpressionFinder} with * the temporal context being the current date and time. * * @throws RuntimeException * if there is a problem creating any of the web service * connections */ public TemporalExpressionFinder() throws RuntimeException { // set the temporal context to the current date and time try { this.calendar = DatatypeFactory .newInstance() .newXMLGregorianCalendar( (GregorianCalendar) GregorianCalendar.getInstance()); } catch (DatatypeConfigurationException e) { // should not happen throw new IllegalStateException(e); } // TODO: correctly initialize the web services here: this.langIdentifier = null; this.timeAnnotators = new HashMap<String, TimeAnnotator>(ANNOTATOR_LOCS .size()); } /** * Finds temporal expressions in texts and normalizes them. * * @param args * the text strings from which to extract and normalize temporal * expressions * @throws IllegalArgumentException * if any of the given texts is of an unsupported language * @throws InternalServiceFault * if there is some problem with web service invocation */ public static void main(String[] args) throws IllegalArgumentException, InternalServiceFault { TemporalExpressionFinder finder = new TemporalExpressionFinder(); // find and normalize all temporal expressions in the given texts: for (String text : args) { System.out.println("Normalized temporal expressions of: " + text); for (String timex : finder.find(text)) { System.out.print("\t– "); System.out.println(timex); } System.out.println(); } } /** * Finds temporal expressions in the given text and returns them as * normalized TIMEX2 annotations. * * @param text * the text in which to find temporal expressions * @return normalized TIMEX2 annotations * @throws IllegalArgumentException * if the the given text is of an unsupported language * @throws InternalServiceFault * if there is some problem with web service invocation */ private List<String> find(String text) throws IllegalArgumentException, InternalServiceFault { // identify the language of the given text and make sure that we have a // temporal expression annotator for this language String lang = this.langIdentifier.getLanguage(text); TimeAnnotator annotator = this.timeAnnotators.get(lang); if (annotator == null) throw new IllegalArgumentException("Language not supported: " + lang); return findAndNormalize(annotator, text); } /** * Finds temporal expressions in the given text and returns them as * normalized TIMEX2 annotations. The given annotator is used for finding * and normalizing. * * @param annotator * the annotator to use for finding and normalizing * @param text * the text in which to find temporal expressions * @return normalized TIMEX2 annotations * @throws InternalServiceFault * if there is some problem with web service invocation */ private List<String> findAndNormalize(TimeAnnotator annotator, String text) throws InternalServiceFault { List<String> result = new ArrayList<String>(); // TODO use the selected time annotator to create the result return result; } }
The code should be self-explanatory. The WSs to use still
have to be initialized in the constructor. The find
method contains the actual functionality of the TemporalExpressionFinder
: it first uses an instance of the
LanguageIdentifier
WS to find the
language of the given text and then uses an appropriate TimeAnnotator
for the extraction of normalized temporal
expressions. The latter is done in the findAndNormalize
method which we will write in the following.
First – before actually using the WSs – we will have to
initialize the connections to the WSs so that we can easily use
them like normal Java objects. The URL of the WSDL description of every WS to use suffices for the
initialization of these connections. In the code skeleton above we have already
statically defined (or hard coded) all relevant URLs. We are
now going to use these URLs in the constructor of the sample
class. The QALL-ME Framework offers the WebServiceTools
class (in package net.sf.qallme)
of which the instantiateServiceClientInterface
method
can be used to establish a WS connection.[6] You just have to pass this method the
WSDL URL and the Java class of the
corresponding JAX-WS generated WS client
interface (carrying a WebServiceClient
annotation), e.g.,
LanguageIdentifierWS
in the package net.sf.qallme.gen.ws.languageidentification. The initialization of
the WS connections for our class in the constructor then looks
like this:
public TemporalExpressionFinder() throws RuntimeException { // initialize connection to the LanguageIdentifier WS this.langIdentifier = WebServiceTools .instantiateServiceClientInterface(LanguageIdentifierWS.class, LANGUAGE_IDENTIFIER_LOC).getLanguageIdentifierPort(); // initialize connections to the different TimeAnnotator WSs this.timeAnnotators = new HashMap<String, TimeAnnotator>(ANNOTATOR_LOCS .size()); for (Entry<String, URL> annotLoc : ANNOTATOR_LOCS.entrySet()) this.timeAnnotators.put(annotLoc.getKey(), WebServiceTools .instantiateServiceClientInterface(TimeAnnotatorWS.class, annotLoc.getValue()).getTimeAnnotatorPort()); }
LanguageIdentifier
in the package net.sf.qallme.gen.ws.languageidentification.
We can now turn to the last step, using the WSs in our
code. In the above skeleton the LanguageIdentifier
is
already used via its Java interface; this should not need any further
explanations. The usage of the TimeAnnotator
WSs is a little more complicated: we first have to create a
dedicated AnnotationRequest
object consisting of an
AnnotatedSentence
with no annotations at all and the
temporal context of the given text; by our definition this context is the
date and time of the creation of our example object. The request object is then
simply passed to the annotator we have retrieved for the language of the given
text and a new AnnotatedSentence
object is returned. From
this annotated sentence we can now easily extract all annotations and return
their canonical forms as the result. Here is the missing code:
@SuppressWarnings("unchecked") private List<String> findAndNormalize(TimeAnnotator annotator, String text) throws InternalServiceFault { List<String> result = new ArrayList<String>(); // create a request to the TimeAnnotator: AnnotatedSentence annotSent = OBJ_FACTORY2.createAnnotatedSentence(); annotSent.getContent().add(text); AnnotationRequest request = OBJ_FACTORY.createAnnotationRequest(); request.setAnnotatedSentence(annotSent); request.setTemporalContext(this.calendar); // run the TimeAnnotator WS annotSent = annotator.annotateTime(request); // extract the annotations from the TimeAnnotator’s response and return // them for (Serializable s : annotSent.getContent()) if (s instanceof JAXBElement) result.add(((JAXBElement<Annotation>) s).getValue() .getCanonicalForm()); return result; }
And we are done. With just about 200 lines of code (including comments) you have just created a temporal expression finder and normalizer for multiple languages. Test your class for example with an argument string like “Franz geht heute Abend ins Kino. Maria geht bereits um 16 Uhr mit Eva.” – or try your own natural language texts.
When testing your implementation on the command line, make sure to
really pass each text as a single command line argument. In
most shells you will have to entirely quote each text for this. Otherwise the
TemporalExpressionFinder
will treat every single word as
an extra text which is probably not desired.
[6] The mechanism used here and in the following relies on the availability of a JAX-WS API implementation. See section 3.1.4: “Java Web Service API And Tools” for information on how to get this API.