...
This section explains how to write the Spin plug-in used to implement a query and provides client code that can be used to perform this query remotely.
Resources
- Spin sources: http://scm.chip.orgopen.med.harvard.edu/svn/repos/spin/base/trunk/
- Example Spin extension in Java and Scala: http://scm.chip.org/svn/repos/spin/base/trunk/examples
System Requirements
- Sun/Oracle Java Development Kit (JDK) 1.6.0_04 or later. Other JDKs, including OpenJDK, are not supported but will likely work.
- Apache Maven 2.2.1. Maven 3.x is not supported, but will likely work. (Required to build SPIIN and the example module)
...
Code Block |
---|
public interface QueryAction<Criteria> { String perform(final QueryContext context, final Criteria criteria) throws QueryException; Criteria unmarshal(final String serializedCriteria) throws SerializationException; boolean isReady(); void destroy(); } |
...
Code Block |
---|
final class EchoQueryAction extends QueryAction[String] { override def unmarshal(serialized: String) = serialized override def perform(context: QueryContext, input: String) = input override def isReady = true override def destroy { } } |
This could be simplified by extending AbstractQueryAction, which provides default implementations for isReady() and destroy() which are suitable for a stateless QueryAction like EchoQueryAction:
Code Block |
---|
final class EchoQueryAction extends AbstractQueryAction[String] { override def unmarshal(serialized: String) = serialized override def perform(context: QueryContext, input: String) = input } |
QueryAction's unmarshal method defines how the input criteria is unmarshalled into a Java object. In this case, we just echoing our input, so we don't need to deserialize.
Consider a slightly more complicated QueryAction that receives as input a list of integers and returns the sum. Here the serialization format is XML, so we can extend JAXBQueryAction, which supplies an implementation of unmarshal() that uses JAXB to turn raw XML into a object in the JVM:
No Formatcode |
---|
final class AddQueryAction extends JAXBQueryAction(classOf[AddInput]) { //Take an AddInput, and return an XML-serialized AddResult override def perform(context: QueryContext, input: AddInput): String = { val result = new AddResult(input.toAdd.sum) JAXBUtils.marshalToString(result) } } |
This class takes input like:
Code Block |
---|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <ns2:AddInput xmlns:ns2="http://spin.org/xml/res/"> <number xsi:type="xs:int" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">1</number> <number xsi:type="xs:int" xmlns:xs="http://www.w3.org/2001/XMLSchemaXMLScema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2</number> <number xsi:type="xs:int" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">3</number> </ns2:AddInput> which gets unmarshalled into a class like import java.util.{List => JList} @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "AddInput", namespace = "http://spin.org/xml/res/") @XmlRootElement(name = "AddInput", namespace = "http://spin.org/xml/res/") //The only field is a Java List, which works with JAXB final case class AddInput(private val number: JList[Int]) { { //JAXB requires a no-arg constructor; can be private *def* *this* def this() = *this*(*new* JArrayList\[Int\]) //For nicer Scala interop *def* *this* def this(toAdd: Seq\[Int\]) = *this*(*new* JArrayList(asJavaList(toAdd))) *def* def toAdd: Seq\[Int\] = number.toSeq } |
Returning Results
QueryActions return results as serialized Strings. Spin is agnostic about the contents of the returned String and, as with input, the serialization format used. In this case, if you define a class that's serializable by JAXB, like:
Code Block |
---|
@XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "AddResult", namespace = "http://spin.org/xml/res/") @XmlRootElement(name = "AddResult", namespace = "http://spin.org/xml/res/") final case class AddResult(val sum: Int) { //JAXB requires a no-arg constructor; can be private def this() = this(0) } |
You can use SPIN's JAXBUtils class to serialize an instance into a String:
Code Block |
---|
final class AddQueryAction extends JAXBQueryAction(classOf[AddInput]) { //Take an AddInput, and return an XML-serialized AddResult override def perform(context: QueryContext, input: AddInput): String = {, input: AddInput): String = { val sum = input.toAdd.sum val result = new AddResult(input.toAdd.sum) JAXBUtils.marshalToString(result) } } |
Make a node that loads your query
...
Code Block |
---|
object Config { //The human-readable name of the node we will create; purely descriptive val nodeName = "AddNode" //Some dummy credentials for submitting queries with val credentials = new Credentials("CBMI", "some-user", "some-password") //Method that returns a SpinClientConfig with all necessary fields set to enable //querying the passed -in node def spinClientConfigFor(node: SpinNode) = { val nodeConnectorSource = registerAsLocalSource(nodeConnectorSourceFor(node)) val entryPoint = new EndpointConfig(EndpointType.Local, nodeName) SpinClientConfig.Default.withCredentials(credentials).withNodeConnectorSource(nodeConnectorSource).withEntryPoint(entryPoint) } //Nodes may participate in one or more overlay networks, called peer groups. By belonging to more //than one peer group, the same node can exist at different points in different logical network topologies. val peerGroupName = "AddPeerGroup" //An empty, dummy, routing table. Defines one peer group, 'AddPeerGroup' that only the node //we will create belongs to. val routingTableConfig = new RoutingTableConfig(new PeerGroupConfig(peerGroupName)) //The QueryType name that will map to the AddQueryAction class. Clients specify which query they would //like to perform by specifying a QueryType. val queryType = "Spin.Add" //Create a NodeConfig with default values for its fields, and a mapping between the QueryType 'Spin.Add' and //an instance of AddQueryAction val nodeConfig = NodeConfig.Default.withQuery(new QueryTypeConfig(queryType, classOf[AddQueryAction].getName)) //Needed to enable locating node instances when making in-JVM queries private def nodeConnectorSourceFor(node: SpinNode) = new NodeConnectorSource { override def getNodeConnector(endpoint: EndpointConfig, timeoutPeriod: Long): NodeConnector = NodeConnector.instance(node) } //Needed to enable locating node instances when making in-JVM queries private def registerAsLocalSource(source: NodeConnectorSource): NodeConnectorSource = { NodeOperationFactory.addMapping(EndpointType.Local, source) source } } |
Here is a higher-level wrapper that makes use of the information from Config to synchronously send a query to a node and get the results:
Code Block |
---|
final class AddClient(client: SpinClient) { //Init with the in-JVM node we're going to query def this(toBeQueried: SpinNode) = this(new SpinClient(Config.spinClientConfigFor(toBeQueried))) //Take a bunch of ints, return an Option of their sum, or None if there was an error. def query(intsToBeAdded: Seq[Int]): Option[Int] = { //method to extract the first (and in this case, only) Result from a ResultSet def firstAddResult(resultSet: ResultSet) = JAXBUtils.unmarshal(resultSet.getResults.head.getPayload.getData, classOf[AddResult]) try { //Actually make the query val resultSet = client.query(Config.peerGroupName, Config.queryType, new AddInput(intsToBeAdded)) log(resultSet) //Inspect the results, and extract the first (only) one if(resultSet.size > 0) Some(firstAddResult(resultSet).sum) else None } catch { case e: TimeoutException => { println("Timed out waiting for query to complete: " + e.getMessage) ; e.printStackTrace(System.err) ; None } case e: Exception => { println("Error making query: " + e.getMessage) ; e.printStackTrace(System.err) ; None} } } private def log(resultSet: ResultSet) { val expectedString = Option(resultSet.getTotalExpected).map(_.toString).getOrElse("?") val completeString = if(resultSet.isComplete) "complete" else "incomplete" println("(" + completeString + ": " + resultSet.size + "/" + expectedString + " results)") } } |
Here is a main method that ties it all together:
Code Block |
---|
object Main { def main(args: Array[String]) = { //Create a node to query; once instantiated, it is ready to be queried val node = new SpinNodeImpl(new CertID("123456789", "some-node"), //arbitrary ID Config.nodeConfig, //pre-made config, references AddQueryAction RoutingTableConfigSources.withConfigObject(Config.routingTableConfig)) //pre-made config, contains one peer group //Create a client pointing at that node val client = new AddClient(node) val prompt = "Add> " val in = new BufferedReader(new InputStreamReader(System.in)) println("Enter a list of numbers to be summed, separated by spaces or commas") println("Enter quit to exit") print(prompt) var line = in.readLine while(line != null) { //Shut down cleanly if requested if(line.equalsIgnoreCase("quit")) { println("Exiting...") node.destroy() System.exit(0) } if(!line.isEmpty) { //Split each line on non-numeric chars, and turn the chunks into ints val numbers = line.trim.split("[^\\d]+").map(Integer.parseInt) //Query the node we made earlier val queryResult = client.query(numbers) //Print the results println(queryResult.map(result => "Received: '" + result + "'").getOrElse("Query failed")) } print(prompt) line = in.readLine } } } |
Running this will create an in-JVM Spin node and configure it to respond to the 'Spin.Add' QueryType using an AddQueryAction instance; create a client that queries that node and performs queries in response to user input. These examples are available at:http://scm.chip.org/svn/repos/spin/base/trunk/examples
...
Code Block |
---|
val entrypoint = new EndpointConfig(EndpointType.SOAP, "http://localhost:8080/examples/node") val config = SpinClientConfig.Default.withEntryPoint(entrypoint) val client = new SpinClient(config) |
...
A SpinClient with the default configuration will attempt to sign all outgoing queries, which means that a certificate with a private key part must be available. (TODO: Spin certificate management)
Testing in a Servlet Container
...
Code Block |
---|
<definitions targetNamespace="http://org.spin.node/"> <types> <xsd:schema> <xsd:import namespace="http://www.w3.org/2000/09/xmldsig#" schemaLocation="http://localhost:8080/examples/node?xsd=1"/> </xsd:schema> <xsd:schema> <xsd:import namespace="http://spin.org/xml/res/endpoint" schemaLocation="http://localhost:8080/examples/node?xsd=2"/> </xsd:schema> <xsd:schema> <xsd:import namespace="http://org.spin.node/" schemaLocation="http://localhost:8080/examples/node?xsd=3"/> </xsd:schema> </types> |
From the examples-war module, run the class org.spin.examples.Test. It has a main method that will connect to a Spin node running in a servlet container that's configured to expose the AddQueryAction class from the examples-scala module.
...