The National Virtual Observatory
2006 Summer School

VO Client side Integration: Lessons Learned (C. Miller 09/11/06)

In this lesson, we discuss how astronomers can integrate VO services into their day-to-day research activities using IDL.
NOTE: GDL (an open-source version of IDL) does not support many of the features discussed here.

Contents:

VOTable Readers
    Common Types of XML Parsers
    readvot.pro
    Effective Translation of VOTable Data to other Data types
    Other Techniques for Working with VOTables

Utilizing REST-based Services
    Cone Searches and Catalogs
    SIAP Queries and Images

Utilizing SOAP-based Services
    Registry Calls
    Sesame Name Resolver
    SkyPortal Calls


Reading VOTables

The most common type of data output you will find in VO services is the VOTable format. It is a requirement of Cone Search and SIAP query returns. Similarly, WESIX returns only a VOTable and SkyPortal can return x,y,z.  Thus, from whatever research environment you use, the ability to read VOTables is very important. In most cases, XML/VOTables can be re-formatted into a more "comfortable" format (e.g., ascii, FITs, etc). But the importance of having a near seamless connection between VOTable outputs and variables within your research environment cannot be over-emphasized. A fairly inefficient way to do one's research is to be continually exiting a research environment (e.g., Python, IDL, SM) in order to convert VOTables. A much preferred technique is to work with a converter or parser within your environment.

Common Types of XML Parsers

See Simon Krughoff's Lesson.

SAX (Simple API for XML)
Reads XML sequentially reporting events. Good for reading large data files.

DOM
Tree-based parser. Allows XML to be modified and written.

IDL supports both DOM and SAX parsers. We give an example of the use of SAX and DOM parsers on a simple XML file.
Using the IDL native SAX library: An Event-based parser. See rsi/idl_6.1/examples/data_access
First, create your parser (xml_to_array__define.pro), then read the XML file:


num_array.xml


<?xml version="1.0"?>
<!--
This file is used in the example of the xmlnumber parser object
class, described in the "Using the XML Parser Object Class"
chapter of _Building IDL Applications_.
-->

<!DOCTYPE array [
<!ELEMENT array (number+)>
<!ELEMENT number (#PCDATA)>
]>

<array>
<number>0</number>
<number>1</number>
<number>2</number>
</array>

xml_to_array__define.pro


; Called when the xml_to_array object is created.
FUNCTION xml_to_array::Init
self.pArray = PTR_NEW(/ALLOCATE_HEAP)
RETURN, self->IDLffXMLSAX::Init()
END
; Called when the xml_to_array object is destroyed.
PRO xml_to_array::Cleanup
; Release pointer
IF (PTR_VALID(self.pArray)) THEN PTR_FREE, self.pArray
END
; Called when parsing of the document data begins.
; If the array pointed at by pArray contains data, reinitialize it.
PRO xml_to_array::StartDocument
IF (N_ELEMENTS(*self.pArray) GT 0) THEN $
void = TEMPORARY(*self.pArray)
END
; Called when parsing character data within an element.
; Adds data to the charBuffer field.
PRO xml_to_array::characters, data
self.charBuffer = self.charBuffer + data
END
; Called when the parser encounters the start of an element.
PRO xml_to_array::startElement, URI, local, strName, attr, value
CASE strName OF
; If the array pointed at by pArray contains data,
; reinitialize it.
"array": BEGIN
IF (N_ELEMENTS(*self.pArray) GT 0) THEN $
void = TEMPORARY(*self.pArray); clear out memory
END
; Reinitialize the charBuffer field.
"number" : BEGIN
self.charBuffer = ''
END
ENDCASE
END
; Called when the parser encounters the end of an element.
PRO xml_to_array::EndElement, URI, Local, strName
CASE strName OF
"array": ; Do nothing.
"number": BEGIN
; Convert string data to an integer.
idata = FIX(self.charBuffer);
; If the array pointed at by pArray has no elements,
; set it equal to the current data.
IF (N_ELEMENTS(*self.pArray) EQ 0) THEN $
*self.pArray = iData $
; If the array pointed at by pArray contains data
; already, extend the array.
ELSE $
*self.pArray = [*self.pArray,iData]
END
ENDCASE
END
; Returns the current array stored internally. If
; no data is available, returns -1.
FUNCTION xml_to_array::GetArray
IF (N_ELEMENTS(*self.pArray) GT 0) THEN $
RETURN, *self.pArray $
ELSE RETURN , -1
END
; Object class definition method.
PRO xml_to_array__define
void = {xml_to_array, $
INHERITS IDLffXMLSAX, $
charBuffer : '', $
pArray : PTR_NEW() }
END

Let's try it out:

IDL> xmlObj = OBJ_NEW('xml_to_array')
IDL> xmlFile = 'num_array.xml'
IDL> xmlObj->ParseFile, xmlFile
IDL> myArray = xmlObj->GetArray()
IDL> PRINT, myArray

Using the IDL native DOM library: Consider this sample.xml file and the code sample:

sample.xml


<?xml version="1.0" encoding="UTF-8"?>
<plugin type="tab-iframe">
   <name>Weather.com Radar Image [DEN]</name>
   <description>600 mile Doppler radar image for DEN</description>
   <version>1.0</version>
   <tab>
      <icon>weather.gif</icon>
      <tooltip>DEN Doppler radar image</tooltip>
   </tab>
</plugin>

sample.pro

PRO sample_recurse, oNode, indent 

   ; "Visit" the node by printing its name and value
   PRINT, indent GT 0 ? STRJOIN(REPLICATE(' ', indent)) : '', $
      oNode->GetNodeName(), ':', oNode->GetNodeValue()

   ; Visit children
   oSibling = oNode->GetFirstChild()
   WHILE OBJ_VALID(oSibling) DO BEGIN
      SAMPLE_RECURSE, oSibling, indent+3
      oSibling = oSibling->GetNextSibling()
   ENDWHILE
END

PRO sample
   oDoc = OBJ_NEW('IDLffXMLDOMDocument')
   oDoc->Load, FILENAME="sample.xml"
   SAMPLE_RECURSE, oDoc, 0
   OBJ_DESTROY, oDoc
END
Let's try it out:

IDL> sample
% Compiled module: SAMPLE.
% Loaded DLM: XML.
#document:
plugin:
#text:

name:
#text:Weather.com Radar Image [DEN]
#text:

description:
#text:600 mile Doppler radar image for DEN
#text:

version:
#text:1.0
#text:

tab:
#text:

icon:
#text:weather.gif
#text:

tooltip:
#text:DEN Doppler radar image
#text:

#text:


VOlib_0.2 contains readvot.pro, a DOM-based VOTable reader:  READVOT.PRO

How might you call it?  Click Here

How does it work?  It's a DOM-based recursive procedure to parse the XML tree and build an IDL structure.

Keep track of the nuumber of ROWs, COLUMNS, RESOURCES (tables) and which RESOURCE is placed in the structure.

SAMPLE_RECURSE2, oDoc, 0,str, J,K, resource_num, resource_read

Each call to SAMPLE_RECURSE2 builds a new line (a single row-structure). This newline is concatenated to the old structure.
 concat_structs, old_str, str, new_str
str=new_str

Lessons Learned:

Effective Translation of VO Data to other data types

VOTables contain information (FIELDS) which can completely describe the data. The data type and the [array]size are vital. Names are nice, but not necessary, likewise for UCDs and units. Challenges occur when 'arraysize="*"' or the data type is not specified (which is fairly often) in the VO. Likewise, incorrect FIELD information (i.e., the wrong data type or arraysize) cause similar difficulties. To account for either of these, I find it easiest to read everything in as a string (or something non-type specific) and then  typically parse. However, parsing alone can be a challenge (on strings).

First: build the structure (needs structure size, tags-names, data-types, and array sizes).
READVOT looks for the following field-types: NAME, ID, UCD, ARRAYSIZE, DATATYPE
READVOT recognizes the following types: unsignedByte, long, int short, double, float, char. These are translated to IDL datatypes.
NAMEs are parsed for unallowable characters (e.g., $, ', ", ?, etc).
The structure is defined (as a single row). The structure tags (columns) are built up as the tree is transversed.

As the metadata information is read in, one hopes to have the correct datatypes and array definitions. But this is rare.
Instead, traverse the tree and try to determine the datatype and the array-size from the XML alone.

The initial structure is built from the XML table metadata. But the first time data is read, each TD element is parsed, typed, and sized to determine if it comes close to what is in the structure already. The challenge with this is datatyping: STRING arrays must be treated differently from INT/FLOAT (numeric) arrays. I parse on "," in STRINGs and "," and " " in numbers.

By the end, one hopes to have a correctly build structure.

Give it a try:


IDL> readvot, 'nvoss200/idl/VOlib_0.2/data/xmm1_votable.xml',str
3 tables found. Table number 1 was read in. You can specify another with table = x.
IDL> help, /str,str
** Structure <8347804>, 15 tags, length=92, data length=92, refs=1:
CREATED STRING '2005-09-02'
UNIQUE_ID LONG 38426
NAME STRING Array[1]
RA DOUBLE 34.252984
DEC DOUBLE -5.0352980
TIME DOUBLE 51756.944
EP_FLUX FLOAT 1.54315e-14
EP_FLUX_ERROR FLOAT 4.09244e-15
PN_FLUX FLOAT 1.54315e-14
PN_FLUX_ERROR FLOAT 4.09244e-15
M1_FLUX FLOAT -999.900
M1_FLUX_ERROR FLOAT -999.900
M2_FLUX FLOAT -999.900
M2_FLUX_ERROR FLOAT -999.900
SEARCH_OFFSET DOUBLE 2.1590000
IDL> readvot, 'xmm1_votable.xml',str, table=2
IDL> help, /str,str
** Structure <82adc3c>, 12 tags, length=108, data length=108, refs=1:
CREATED STRING '2005-09-02'
UNIQUE_ID LONG 191
NAME STRING Array[1]
RA DOUBLE 34.100000
DEC DOUBLE -5.0000000
PROPOSAL_TYPE STRING Array[1]
PRIORITY STRING Array[1]
PNO LONG 11237
EXPOSURE LONG 50000
PI_LNAME STRING Array[1]
PI_FNAME STRING Array[1]
SEARCH_OFFSET DOUBLE 9.5630000

Other Techniques for Working with VOTables

Java Libraries (as opposed to native XML parsers)

XML-tag removal



Using REST-based Webservices

Cone Searches and SIAP queries are REST web-services (i.e., a POST) and their results are simple XML files. These are easily obtained through spawning a "wget" or via port streaming (?).  For both of these web-services, the required inputs are POSITION, SIZE and URL of the cone search or SIAP query.

Points to note: spawning a wget require write privileges in some working directory to save the XML file and then a VOTable parser to read it in. Connecting to a port requires:

Cone Searches: CONECALL.PRO

How might you call it?  Click Here

How does it work?  It "spawns" a WGET call and downloads the CONE SEARCH XML file. The XML VOTable is read in by the IDL VOTable to create a structure.  It requires a position on the sky, and search radius, and a URL (obtained from a Registry call--see below).
We parse for all "?" marks to place the RA=&DEC=&SR=1

Lessons Learned
Some don't work:
http://irsa.ipac.caltech.edu/cgi-bin/Oasis/CatSearch/nph-catsearch?server=@rmt_grit&CAT=fp_2mass:fp_psc

Some have "?", some have "&" and "?". Some have neither.
http://simbad.u-strasbg.fr/simbad-conesearch.pl
http://www2.keck.hawaii.edu/software/vos/tkrsConeSearch.php?
http://heasarc.gsfc.nasa.gov/cgi-bin/vo/cone/coneGet.pl?table=ascalss&

How do you meaningfully name the file (VOTable) if you store.

Fill a structure with the data returned from some Cone Search call:

IDL> conecall, 202.8, -1.72, 0.1, str=str, $
url = "http://heasarc.gsfc.nasa.gov/cgi-bin/vo/cone/coneGet.pl?table=xmmssc&"
IDL> help, /str,str
** Structure <82c00e4>, 15 tags, length=92, data length=92, refs=1:
CREATED STRING '2005-09-01'
UNIQUE_ID LONG 37423
NAME STRING Array[1]
RA DOUBLE 202.80856
DEC DOUBLE -1.7227970
TIME DOUBLE 52119.536
EP_FLUX FLOAT -999.900
EP_FLUX_ERROR FLOAT -999.900
PN_FLUX FLOAT 1.59372e-14
PN_FLUX_ERROR FLOAT 3.78526e-15
M1_FLUX FLOAT -999.900
M1_FLUX_ERROR FLOAT -999.900
M2_FLUX FLOAT -999.900
M2_FLUX_ERROR FLOAT -999.900
SEARCH_OFFSET DOUBLE 0.54000000

SIAP Queries

Fill a structure with the VOTable Data returned from a SIAP query.
Download the XML file from within your research environment.

How might you call it?  Click Here

How does it work?  It "spawns" a WGET call and downloads the SIAP query XML file. The XML VOTable is read in by the IDL VOTable to create a structure.  It requires a position on the sky, and search radius, and a URL (obtained from a Registry call--see below).
We parse for all "?" marks to place the POS=&SIZE=

Once the SIAP call is made, the URL is provided. the procedure SIAPCALL will automatically download the image for you (wheter a fits, gzipped fits, jpg, or gif). WEBGET is part of the ASTROLIB libraries and works only on FITs images (or text files), but not XML.
NOTE: MacOSX comes standard with WGET v1.6. SIAPCALL needs version WGET 1.8+ to work properly.

IDL> siapcall,180,1, 0.1, /metadata,str=str, $
url="http://skyview.gsfc.nasa.gov/cgi-bin/vo/sia.pl?survey=rass-cnt&"
IDL> help, /str,str
** Structure <82b9e84>, 12 tags, length=128, data length=128, refs=1:
CREATED STRING '2005-09-01'
SURVEY STRING Array[1]
RA DOUBLE 180.00000
DEC DOUBLE 1.0000000
DIM LONG 2
SIZE LONG64 Array[2]
SCALE DOUBLE Array[2]
FORMAT STRING Array[1]
PIXFLAGS STRING Array[1]
URL STRING Array[1]
NBYTES LONG 362880
LOGICALNAME STRING Array[1]
IDL> a = webget(str[2].url)
IDL> print, a.imageheader
IDL> tv, a.image
or
IDL> siapcall,180,1, 0.1, url="http://skyview.gsfc.nasa.gov/cgi-bin/vo/sia.pl?survey=rass-cnt&"
IDL> image = readfits('image0.fits',hdr)
IDL> hprint, hdr
IDL> tv, image

Lessons Learned:

XMM: Non-compliant. JPGs, GIFs, FITs, GZIPPED. Same issues as Cone Searchers.

Of course, since the image location (URL) is specified in the SIAP XML file, spawning a wget also allows for the immediate download of the image file to some local working directory. In this case, there should be some meaningful root name specified by the user.  Be wary of non-compliant SIAP servers (e.g., XMM) in which the images do not really exist at the specified URL, but the location in fact starts a script to retrieve the image. GUNZIPping on the fly.


Using  SOAP-based Webservices

Examples of current SOAP-based VO services include the JHU NVO Registry, SkyPortal, and WESIX. For legacy software that has a Java-bridge (i.e., the ability to make calls to external Java classes), the easiest way to go is to use the Service WSDL to generate the Java stubs (wsdl2java) and classes (javac) locally, and then call those classes from within your legacy software. Different legacy software have different ways of "importing" external classes. It is extremely helpful to have a minimally useful Service provided client. Additionally, some thought into how the results will be useful to the user is required. In most cases, I have found that filling a structure with all of the VOTable (XML returned file) does the trick.

Registry Calls

So how do I find the Cone Search servers and SIAP servers (etc) to run CONECALL and SIAPCALL? Use CALL_REGISTRY.
SIAP queries, Cone Searches, Open Sky Query calls, all require service (or resource) discovery. This is typically done through a Registry call.

How might you call it?  Click Here

How does it work?  Use the JAVA classes RegistryLocator.class and the regService methods (getResgitrySoap, queryRegistry, etc).
The QUERY is made to be as general as possible.

regService = OBJ_NEW('IDLJavaObject$ORG_US_VO_WWW_REGISTRY_LOCATOR', 'org.us_vo.www.RegistryLocator')
regInterface = regService->getRegistrySoap()
query = "Title like '%" + keyword + "%' or " + $
"ShortName like '%" + keyword + "%' or " + $
"Subject like '%" + keyword + "%' or " + $
"Type like '%" + keyword + "%' or " + $
"Description like '%" + keyword + "%' or " + $
"ServiceType like '%" + keyword + "%' or " + $
"Identifier like '%" + keyword + "%'"

CONEs, SIAPs, and SKYNODEs are treated "special".

IF keyword_set(cone) THEN query = "ServiceType like '%CONE%' and (" + query + ")"
IF keyword_set(skynode) THEN query = "ServiceType like '%SKYNODE%' and (" + query + ")"
IF keyword_set(siap) THEN query = "(ServiceType like '%SIAP%' or ServiceType like '%SIAP;%ARCHIVE%') and (" + query + ")"
callit = regInterface->queryRegistry(query)
results = callit->getSimpleResource()

Data is in the object "results", which is a SimpleResource. Create a structure and fill it:

str = create_struct('Title', ' ', 'URL', ' ', 'Type', ' ', 'ShortName', ' ', $
'ID', ' ', 'Desc', ' ', 'ServiceType', ' ', 'Coverage', ' ', $
'Subjects', ' ')
str = replicate(str, n_elements(results))
FOR I = 0, n_elements(results) -1 DO BEGIN
str[I].Title = results[I]->getTitle()
str[I].URL = results[I]->getServiceURL()
str[I].Type = results[I]->getType()
str[I].ShortName = results[I]->getShortName()
str[I].ID = results[I]->getIdentifier()
str[I].Desc = results[I]->getDescription()
str[I].ServiceType = results[I]->getServiceType()
str[I].Coverage = results[I]->getCoverageSpatial()


Let's give it a try:

IDL> call_registry, 'ROSAT',/SIAP, str=str
IDL> help, /str,str
** Structure <14a5116c>, 9 tags, length=108, data length=108, refs=1:
TITLE STRING '
ROSAT All Sky Survey
'
URL STRING '
http://skyview.gsfc.nasa.gov/cgi-bin/vo/sia.pl?survey=rass-cnt&
'
TYPE STRING ' Archive '
SHORTNAME STRING '
ROSAT/RASS
'
ID STRING 'ivo://nasa.heasarc/skyview/rass'
DESC STRING '
The ROSAT All-Sky X-ray Survey was obtained during 1990/1991 using the ROSAT Position Sens'...
SERVICETYPE STRING 'SIAP'
COVERAGE STRING '<region xsi:type="AllSky" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.ivo'...
SUBJECTS STRING 'surveys'

IDL> call_registry, 'XMM-Newton',/CONE, str=str
IDL> help, /str, str
IDL> help, /str,str[2]
** Structure <14a3666c>, 9 tags, length=108, data length=108, refs=2:
TITLE STRING 'XMM-Newton Serendipitous Source Catalog'
URL STRING '
http://heasarc.gsfc.nasa.gov/cgi-bin/vo/cone/coneGet.pl?table=xmmssc&
'
TYPE STRING 'Catalog'
SHORTNAME STRING 'XMM/SSC'
ID STRING 'ivo://nasa.heasarc/xmmssc'
DESC STRING '
The XMM-Newton Serendipitous Source Catalog (1XMM) is the first comprehensive catalog of serendipito'...
SERVICETYPE STRING 'CONE'
COVERAGE STRING '<region xsi:type="CircleRegion" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://w'...
SUBJECTS STRING 'Serendipitous sources'

IDL> print, str[2].url

http://heasarc.gsfc.nasa.gov/cgi-bin/vo/cone/coneGet.pl?table=xmmssc&

Lessons Learned:

White space can be a nuisance (but it is manageable).  Hidden characters (like line breaks) also cause headaches.
A Registry-calling Java client was extremely useful for a non-Java expert to utilize the Java-bridge in IDL.
Compare the Java client below to the IDL client-code above.
From nvoss2005/java/dev/coneclient/FindConeSearch.java

    {
// get a registry service object
Registry regService = new RegistryLocator();

// get an interface object that can accept our query.
RegistrySoap regInterface = regService.getRegistrySoap();

// Combine our query with a constraint to return only Cone Searches.
// The resulting query will look something like this:
//
// ServiceType like '%CONE%' and (Title like '%parallax%')
//
query = "ResourceType like '%CONE%' and (" + query + ")";
// Now submit the query
ArrayOfSimpleResource results = regInterface.queryRegistry(query);
return results.getSimpleResource();
}


Sesame Name Resolver

Sesame is a name resolver for astronomy. The user specifies the name of an object, and Sesame responds with coordinates.
Like the Registry Client, the name resolver needs an object which is the service locator:

loc = OBJ_NEW('IDLJavaObject$SESAME_PKG_SESAMESERVICELOCATOR','Sesame_pkg.SesameServiceLocator')
addr =loc -> getSesame()

The call to the service is straightforward:

result = addr ->sesame(name, "pi", 1, "N")

In the above call, name specifies the object name being queried (e.g., "m31"), while "pi" tells the service to respond in plain text.
1 signifies to return all ALIASes and "N" meands search NED. These options are futher explained at the Sesame website.

An example call looks like this:


IDL> resolve_name, "m31", str=str
% Compiled module: RESOLVE_NAME.
% Compiled module: STRSPLIT.
% Compiled module: APPEND_ARR.
% Compiled module: CREATE_STRUCT.
% Compiled module: REPCHR.
% Compiled module: TEMP_M31X.
IDL> help, /str, str
** Structure M31, 41 tags, length=476, data length=476:
TYPE STRING 'G'
RA DOUBLE 10.684685
DEC DOUBLE 41.269037
RA_ERR DOUBLE 375.00000
DEC_ERR DOUBLE 375.00000
ALIAS0 STRING '0 MESSIER 031 =[G]'
ALIAS1 STRING 'NGC 0224 =[G]'
ALIAS2 STRING 'Andromeda Galaxy =[G]'
ALIAS3 STRING 'UGC 00454 =[G]'
ALIAS4 STRING 'CGCG 535-017 =[G]'
ALIAS5 STRING 'CGCG 0040.0+4100 =[G]'
ALIAS6 STRING 'MCG +07-02-016 =[G]'
ALIAS7 STRING 'GIN 801 =[G]'
ALIAS8 STRING 'B3 0040+409 =[RadioS]'
ALIAS9 STRING '2MASX J00424433+4116074 =[IrS]'
ALIAS10 STRING 'IRAS 00400+4059 =[IrS]'
ALIAS11 STRING 'IRAS F00400+4059 =[IrS]'
ALIAS12 STRING 'KTG 01C =[G]'
ALIAS13 STRING 'HOLM 017A =[G]'
ALIAS14 STRING 'PGC 002557 =[G]'
ALIAS15 STRING 'UZC J004244.3+411608 =[G]'
ALIAS16 STRING '87GB 004002.2+405940 =[RadioS]'
ALIAS17 STRING '87GB[BWE91] 0040+4059 =[RadioS]'
ALIAS18 STRING '6C B004001.6+410004 =[RadioS]'
ALIAS19 STRING 'MY 0040+409A =[RadioS]'
ALIAS20 STRING 'CXOM31 J004244.3+411608 =[XrayS]'
ALIAS21 STRING 'RX J0042.6+4115 =[XrayS]'
ALIAS22 STRING '1RXS J004241.8+411535 =[XrayS]'
ALIAS23 STRING 'EXSS 0039.9+4059 =[XrayS]'
ALIAS24 STRING '1H 0039+408 =[XrayS]'
ALIAS25 STRING '1ES 0039+409 =[XrayS]'
ALIAS26 STRING 'XSS J00425+4102 =[XrayS]'
ALIAS27 STRING 'LGG 011:[G93] 001 =[G]'
ALIAS28 STRING '[PFJ93] 44 =[XrayS]'
ALIAS29 STRING '[MHH96] J004241+411531 =[XrayS]'
ALIAS30 STRING '[VCV2001] J004244.3+411610 =[G]'
ALIAS31 STRING 'MESSIER 031:[KGP2002] r1-010 =[V*]'
ALIAS32 STRING 'MESSIER 031:[PFH2005] 321 =[XrayS]'
ALIAS33 STRING '0039+408 =[Other]'
ALIAS34 STRING '0040+4059 =[Other]'
ALIAS35 STRING 'LEDA 002557 =[G]'


SkyPortal Calls

The Open Sky Query (OSQ--via the SkyPortal) allows for SQL-like queries of more than 20 astronomical catalogs. OSQ also will cross-match multiple catalogs for you.


How might you call it?  Click Here

How does it work?  Use the JAVA classes SkyPortalLocator, XSD_VOTable, among others.
SKYPORTAL client needs a position, a search radius (in arcminutes) and possibly a SQL query:

qry = " SELECT o.objId, o.ra,o.dec, o.type,t.objId,t.j_m,o.z " + $
" FROM SDSSDR2:PhotoPrimary o, " + $
" TWOMASS:PhotoPrimary t WHERE XMATCH(o,t)<" + strtrim(string(chisqArr),2) + " " + " AND o.type = 3 " + $
" AND Region('Circle J2000 " + strtrim(string(raArr,format='(f10.3)'),2) + " " + strtrim(string(decArr, format='(f10.3)'),2) + $
" " + strtrim(string(srArr, format='(i2)'),2) + "') "


The external procedure make_adql.pro creates a SQL query for you. The SKYCLIENT code uses the JAVA_BRIDGE (like the CALL_REGISTRY procedure):

resource = OBJ_NEW('IDLJavaObject$Static$FR_U_STRASBG_VIZIER_XML_VOTABLE_1_1_XSD_RESOURCE', 'fr.u_strasbg.vizier.xml.VOTable_1_1_xsd/RESOURCE')
vot = OBJ_NEW('IDLJavaObject$Static$FR_U_STRASBG_VIZIER_XML_VOTABLE_1_1_XSD_VOTABLE', 'fr.u_strasbg.vizier.xml.VOTable_1_1_xsd/VOTABLE')
loc = OBJ_NEW('IDLJavaObject$NET_IVOA_SKYPORTAL_SKYPORTALLOCATOR','net.ivoa.SkyPortal.SkyPortalLocator')
loc ->setSkyPortalSoapEndpointAddress,'http://openskyquery.net/Sky/SkyPortal/SkyPortal.asmx'
addr =loc -> getSkyPortalSoapAddress()
stub = loc -> getSKyPortalSoap()

;Make the query call, return a VOTable
vod = stub -> submitDistributedQuery(qry,"VOTABLE")
vot = vod ->getVOTABLE()
The SKYCLIENT then parses the VOTable internal and places the data into a structure.

IDL> skyclient, 180,0,1,str = str; Defaults to whatever is uncommented in the MAKE_ADQL.PRO
% Compiled module: MAKE_ADQL.
SELECT o.objId, o.ra,o.dec, o.type,t.objId,t.j_m,o.z FROM SDSSDR2:PhotoPrimary o,
TWOMASS:PhotoPrimary t WHERE XMATCH(o,t)<2.50000 AND o.type = 3 AND Region('Circle J2000 180.000 0.000 1')
IDL> help, /str, str
** Structure <520790>, 8 tags, length=48, data length=48, refs=1:
SDSSDR2_OBJID LONG64 588848899912695818
SDSSDR2_RA DOUBLE 179.98346
SDSSDR2_DEC DOUBLE -0.0016030098
SDSSDR2_TYPE LONG 3
TWOMASS_OBJID LONG 1016752918
TWOMASS_J_M FLOAT 16.7000
SDSSDR2_Z FLOAT 17.6322
CHISQ DOUBLE 0.088211060


Lessons Learned:
I needed some pre-built Java classes (e.g., from nvoss2006/java/src/skyportalclient) to make this work. If the Java classes needed to call a service (by some client) do not already exist, it is given as a responsibility to the user (wsdl2java, javac, etc).