/*************************************************************************** * Copyright (C) 2009 by Claudio Guidi * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU Library General Public License as * * published by the Free Software Foundation; either version 2 of the * * License, or (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this program; if not, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * * * For details about the authors of this software, see the AUTHORS file. * ***************************************************************************/ package jolie.net; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.net.URI; import java.util.Map.Entry; import javax.xml.transform.Result; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.stream.StreamResult; import jolie.Interpreter; import jolie.runtime.FaultException; import jolie.runtime.Value; import jolie.runtime.ValueVector; import jolie.runtime.VariablePath; import org.w3c.dom.Node; import org.xml.sax.SAXException; import java.io.ByteArrayInputStream; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; import jolie.net.http.HttpMessage; import jolie.net.http.HttpParser; import jolie.net.http.HttpUtils; import jolie.net.protocols.SequentialCommProtocol; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.InputSource; import java.io.ByteArrayOutputStream; import org.w3c.dom.NodeList; /** Implements the XML-RPC over HTTP protocol. * * @author Claudio Guidi * 2009 - Fabrizio Montesi: optimizations and refactoring to use the Element-based API * */ // TO DO: faults /* NOTE about this module: * The XML-RPC specification reference considered is http://www.xmlrpc.com/spec * I made different assumptions for input and output messages. * Output: * aliases for operation names are allowed because Jolie does not support operation names with dots (es. mickey.mouse) * aliases are expressed as protocol parameters as aliases.opname = "aliasName". * In output the request message must contain a subelement param (which could be an array). * Each element of teh subelement param is translated into a tag <param>. * in order to express an array within an element of param, you need to use the keyword array: * request.param.array[0]=....; request.param.array[1]=... * will be translated into <param><value><array><data><value>...</value><value>...</value>..</data></array></value></param> * Since boolean are not managed by Jolie, it is necessary to represent it with subelement boolean * * Output Faults: * At the present Jolie always generates zero code faults. * * Input: * All the array in an input XMLRPC message will be translated into Jolie by means of arrays of the keyword array. * */ public class XmlRpcProtocol extends SequentialCommProtocol { private String inputId = null; final private Transformer transformer; final private Interpreter interpreter; final private DocumentBuilderFactory docBuilderFactory; final private DocumentBuilder docBuilder; final private URI uri; private boolean received = false; final private static String CRLF = new String( new char[]{13, 10} ); public String name() { return "xmlrpc"; } public XmlRpcProtocol( VariablePath configurationPath, URI uri, Transformer transformer, DocumentBuilderFactory docBuilderFactory, DocumentBuilder docBuilder, Interpreter interpreter ) { super( configurationPath ); this.uri = uri; this.transformer = transformer; this.interpreter = interpreter; this.docBuilderFactory = docBuilderFactory; this.docBuilder = docBuilder; } private static Element getFirstElement( Element element, String name ) throws IOException { NodeList children = element.getElementsByTagName( name ); if ( children.getLength() < 1 ) { throw new IOException( "Could not find element " + name ); } return (Element)children.item( 0 ); } private void navigateValue( Value value, Element element ) throws IOException { NodeList contents = element.getChildNodes(); if ( contents.getLength() < 1 ) { throw new IOException( "empty value node" ); } Node contentNode = contents.item( 0 ); if ( contentNode.getNodeType() != Node.ELEMENT_NODE ) { throw new IOException( "a value node may contain only an sub-element" ); } Element content = (Element)contentNode; String name = content.getNodeName(); if ( name.equals( "array" ) ) { Value currentValue; ValueVector vec = value.getChildren( "array" ); NodeList dataChildren = getFirstElement( content, "data" ).getElementsByTagName( "value" ); for ( int i = 0; i < dataChildren.getLength(); i++ ) { currentValue = Value.create(); navigateValue( currentValue, (Element)dataChildren.item( i ) ); vec.add( currentValue ); } } else if ( name.equals( "struct" ) ) { NodeList members = content.getElementsByTagName( "member" ); Element member; for ( int i = 0; i < members.getLength(); i++ ) { member = (Element)members.item( i ); Element valueNode = getFirstElement( member, "value" ); navigateValue( value.getNewChild( getFirstElement( member, "name" ).getNodeValue() ), valueNode ); } } else if ( name.equals( "string" ) ) { value.setValue( content.getTextContent() ); } else if ( name.equals( "int" ) || name.equals( "i4" ) || name.equals( "boolean" ) ) { try { value.setValue( Integer.parseInt( content.getTextContent() ) ); } catch( NumberFormatException e ) { throw new IOException( e ); } } } private void documentToValue( Value value, Document document ) throws IOException { ValueVector paramsValueVector = value.getChildren( "param" ); NodeList params = getFirstElement( document.getDocumentElement(), "params" ).getElementsByTagName( "param" ); Value paramValue; for( int i = 0; i < params.getLength(); i++ ) { paramValue = Value.create(); navigateValue( paramValue, getFirstElement( ((Element)params.item( i )), "value" ) ); paramsValueVector.add( paramValue ); } } private void valueToDocument( Value value, Node node, Document doc ) { Element v = doc.createElement( "value" ); // node value creation in case the contents is a value if ( value.isInt() ) { Element i = doc.createElement( "int" ); i.appendChild( doc.createTextNode( value.strValue() ) ); v.appendChild( i ); node.appendChild( v ); } else if ( value.isString() ) { Element i = doc.createElement( "string" ); i.appendChild( doc.createTextNode( value.strValue() ) ); v.appendChild( i ); node.appendChild( v ); } else if ( value.isDouble() ) { Element i = doc.createElement( "double" ); i.appendChild( doc.createTextNode( value.strValue() ) ); v.appendChild( i ); node.appendChild( v ); } else if ( value.hasChildren( "array" ) ) { // array creation Element a = doc.createElement( "array" ); Element d = doc.createElement( "data" ); for ( int i = 0; i < value.getChildren( "array" ).size(); i++ ) { valueToDocument( value.getChildren( "array" ).get( i ), d, doc ); } a.appendChild( d ); v.appendChild( a ); node.appendChild( v ); } else if ( value.hasChildren( "boolean" ) ) { Element b = doc.createElement( "boolean" ); b.appendChild( doc.createTextNode( value.getFirstChild( "boolean" ).strValue() ) ); v.appendChild( b ); node.appendChild( v ); } else { for ( Entry<String, ValueVector> entry : value.children().entrySet() ) { if ( !entry.getKey().startsWith( "@" ) ) { Element st = doc.createElement( "struct" ); Element m = doc.createElement( "member" ); Element n = doc.createElement( "name" ); n.appendChild( doc.createTextNode( entry.getKey() ) ); m.appendChild( n ); for ( Value val : entry.getValue() ) { valueToDocument( val, m, doc ); } st.appendChild( m ); v.appendChild( st ); } } } } public void send( OutputStream ostream, CommMessage message, InputStream istream ) throws IOException { Document doc = docBuilder.newDocument(); // root element <methodCall> String rootName = "methodCall"; if ( received ) { // We're responding to a request rootName = "methodResponse"; } Element root = doc.createElement( rootName ); doc.appendChild( root ); if ( !received ) { // element <methodName> Element methodName; methodName = doc.createElement( "methodName" ); Value aliases = getParameterFirstValue( "aliases" ); String alias; if ( aliases.hasChildren( message.operationName() ) ) { alias = aliases.getFirstChild( message.operationName() ).strValue(); } else { alias = message.operationName(); } methodName.appendChild( doc.createTextNode( alias ) ); inputId = message.operationName(); root.appendChild( methodName ); } if ( message.isFault() ) { FaultException f = message.fault(); Element fault = doc.createElement( "fault" ); Element v = doc.createElement( "value" ); Element s = doc.createElement( "struct" ); Element m1 = doc.createElement( "member" ); Element n1 = doc.createElement( "name" ); Element v1 = doc.createElement( "value" ); Element i1 = doc.createElement( "i4" ); Element m2 = doc.createElement( "member" ); Element n2 = doc.createElement( "name" ); Element v2 = doc.createElement( "value" ); Element i2 = doc.createElement( "string" ); i1.setTextContent( "0" ); // Jolie generates always zero code faults i2.setTextContent( f.faultName() ); // fault name is insertied into faultString tag of XMLRPC message v1.appendChild( i1 ); v2.appendChild( i2 ); n1.setTextContent( "faultCode" ); m1.appendChild( v1 ); m1.appendChild( n1 ); m2.appendChild( v2 ); m2.appendChild( n2 ); s.appendChild( m1 ); s.appendChild( m2 ); v.appendChild( s ); fault.appendChild( v ); root.appendChild( fault ); } else if ( message.value().hasChildren( "param" ) == true ) { // params exist Element params = doc.createElement( "params" ); for ( int i = 0; i < message.value().getChildren( "param" ).size(); i++ ) { Element p = doc.createElement( "param" ); Element v = doc.createElement( "value" ); if ( message.value().getChildren( "param" ).get( i ).isInt() ) { Element in = doc.createElement( "int" ); in.appendChild( doc.createTextNode( message.value().getChildren( "param" ).get( i ).strValue() ) ); v.appendChild( in ); } else if ( message.value().getChildren( "param" ).get( i ).isString() ) { Element s = doc.createElement( "string" ); s.appendChild( doc.createTextNode( message.value().getChildren( "param" ).get( i ).strValue() ) ); v.appendChild( s ); } else if ( message.value().getChildren( "param" ).get( i ).isDouble() ) { Element d = doc.createElement( "double" ); d.appendChild( doc.createTextNode( message.value().getChildren( "param" ).get( i ).strValue() ) ); v.appendChild( d ); } else if ( message.value().getChildren( "param" ).get( i ).hasChildren( "array" ) ) { // array creation Element a = doc.createElement( "array" ); Element d = doc.createElement( "data" ); for ( int x = 0; x < message.value().getChildren( "param" ).get( i ).getChildren( "array" ).size(); x++ ) { valueToDocument( message.value().getChildren( "param" ).get( i ).getChildren( "array" ).get( x ), d, doc ); } a.appendChild( d ); v.appendChild( a ); } else if ( message.value().getChildren( "param" ).get( i ).hasChildren( "boolean" ) ) { Element b = doc.createElement( "boolean" ); b.appendChild( doc.createTextNode( message.value().getChildren( "param" ).get( i ).getFirstChild( "boolean" ).strValue() ) ); v.appendChild( b ); } else if ( message.value().getChildren( "param" ).get( i ).hasChildren() ) { for ( Entry<String, ValueVector> entry : message.value().getChildren( "param" ).get( i ).children().entrySet() ) { if ( !entry.getKey().startsWith( "@" ) ) { Element st = doc.createElement( "struct" ); Element m = doc.createElement( "member" ); Element n = doc.createElement( "name" ); n.appendChild( doc.createTextNode( entry.getKey() ) ); m.appendChild( n ); for ( Value val : entry.getValue() ) { valueToDocument( val, m, doc ); } st.appendChild( m ); v.appendChild( st ); } } } p.appendChild( v ); params.appendChild( p ); } root.appendChild( params ); } inputId = message.operationName(); Source src = new DOMSource( doc ); ByteArrayOutputStream tmpStream = new ByteArrayOutputStream(); Result dest = new StreamResult( tmpStream ); try { transformer.transform( src, dest ); } catch ( TransformerException e ) { throw new IOException( e ); } String xmlrpcString = CRLF + "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + new String( tmpStream.toByteArray() ); String messageString = ""; if ( received ) { // We're responding to a request messageString += "HTTP/1.1 200 OK" + CRLF; received = false; } else { // We're sending a notification or a solicit String path = uri.getPath(); if ( path == null || path.length() == 0 ) { path = "*"; } messageString += "POST " + path + " HTTP/1.1" + CRLF; messageString += "User-Agent: Jolie" + CRLF; messageString += "Host: " + uri.getHost() + CRLF; } if ( getParameterVector( "keepAlive" ).first().intValue() != 1 ) { channel().setToBeClosed( true ); messageString += "Connection: close" + CRLF; } //messageString += "Content-Type: application/soap+xml; charset=\"utf-8\"\n"; messageString += "Content-Type: text/xml; charset=\"utf-8\"" + CRLF; messageString += "Content-Length: " + xmlrpcString.length() + CRLF; messageString += xmlrpcString + CRLF; if ( getParameterVector( "debug" ).first().intValue() > 0 ) { interpreter.logInfo( "[XMLRPC debug] Sending:\n" + messageString ); } Writer writer = new OutputStreamWriter( ostream ); writer.write( messageString ); writer.flush(); } public CommMessage recv( InputStream istream, OutputStream ostream ) throws IOException { HttpParser parser = new HttpParser( istream ); HttpMessage message = parser.parse(); HttpUtils.recv_checkForChannelClosing( message, channel() ); CommMessage retVal = null; FaultException fault = null; Value value = Value.create(); Document doc = null; if ( getParameterVector( "debug" ).first().intValue() > 0 ) { interpreter.logInfo( "[XMLRPC debug] Receiving:\n" + new String( message.content() ) ); } if ( message.content() != null ) { try { if ( message.size() > 0 ) { DocumentBuilder builder = docBuilderFactory.newDocumentBuilder(); InputSource src = new InputSource( new ByteArrayInputStream( message.content() ) ); doc = builder.parse( src ); if ( message.isResponse() ) { // test if the message contains a fault try { Element faultElement = getFirstElement( doc.getDocumentElement(), "fault" ); Element struct = getFirstElement( getFirstElement( faultElement, "value" ), "struct" ); NodeList members = struct.getChildNodes(); if ( members.getLength() != 2 || members.item( 1 ).getNodeType() != Node.ELEMENT_NODE ) { throw new IOException( "Malformed fault data" ); } Element faultMember = (Element)members.item( 1 ); //Element valueElement = getFirstElement( getFirstElement( struct, "value" ), "string" ); String faultName = getFirstElement( faultMember, "name" ).getTextContent(); String faultString = getFirstElement( getFirstElement( faultMember, "value" ), "string" ).getTextContent(); fault = new FaultException( faultName, Value.create( faultString ) ); } catch( IOException e ) { documentToValue( value, doc ); } } else { documentToValue( value, doc ); } } } catch ( ParserConfigurationException pce ) { throw new IOException( pce ); } catch ( SAXException saxe ) { throw new IOException( saxe ); } if ( message.isResponse() ) { //fault = new FaultException( "InternalServerError", "" ); //TODO support resourcePath retVal = new CommMessage( CommMessage.GENERIC_ID, inputId, "/", value, fault ); } else if ( !message.isError() ) { //TODO support resourcePath String opname = doc.getDocumentElement().getFirstChild().getTextContent(); retVal = new CommMessage( CommMessage.GENERIC_ID, opname, "/", value, fault ); } } received = true; return retVal; } }