From 41cee0522d6586d8b1810f8ab47eacd1e2f80873 Mon Sep 17 00:00:00 2001 From: phasefx Date: Fri, 4 Feb 2005 22:08:15 +0000 Subject: [PATCH] Initial revision git-svn-id: svn://svn.open-ils.org/OpenSRF/trunk@2 9efc2488-bf62-4759-914b-345cdb29e865 --- bin/jabber_users_create | 50 + bin/opensrf_ctl | 44 + doc/OpenSRF-Messaging-Protocol.html | 318 ++++++ examples/math_bench.pl | 110 ++ examples/math_shell.pl | 139 +++ examples/math_simple.pl | 86 ++ include/opensrf/generic_utils.h | 80 ++ include/opensrf/transport_client.h | 87 ++ include/opensrf/transport_message.h | 94 ++ include/opensrf/transport_session.h | 217 ++++ include/opensrf/transport_socket.h | 68 ++ src/javascript/JSON.js | 89 ++ src/javascript/md5.js | 256 +++++ src/javascript/opensrf_app_session.js | 509 +++++++++ src/javascript/opensrf_config.js | 281 +++++ src/javascript/opensrf_dom_element.js | 263 +++++ src/javascript/opensrf_domain_object.js | 609 +++++++++++ src/javascript/opensrf_jabber_transport.js | 418 +++++++ src/javascript/opensrf_msg_stack.js | 203 ++++ src/javascript/opensrf_transport.js | 64 ++ src/javascript/opensrf_utils.js | 117 ++ src/libtransport/Makefile | 31 + src/libtransport/basic_client.c | 78 ++ src/libtransport/generic_utils.c | 392 +++++++ src/libtransport/transport_client.c | 212 ++++ src/libtransport/transport_message.c | 230 ++++ src/libtransport/transport_session.c | 507 +++++++++ src/libtransport/transport_socket.c | 289 +++++ src/patch/README | 8 + src/patch/mod_offline.c | 266 +++++ src/patch/nad.c | 1155 ++++++++++++++++++++ src/perlmods/JSON.pm | 235 ++++ src/perlmods/OpenSRF.pm | 75 ++ src/perlmods/OpenSRF/AppSession.pm | 827 ++++++++++++++ src/perlmods/OpenSRF/Application.pm | 222 ++++ src/perlmods/OpenSRF/Application/Client.pm | 6 + src/perlmods/OpenSRF/Application/Demo/Math.pm | 125 +++ src/perlmods/OpenSRF/Application/Demo/MathDB.pm | 94 ++ src/perlmods/OpenSRF/DOM.pm | 289 +++++ src/perlmods/OpenSRF/DOM/Element/domainObject.pm | 114 ++ .../OpenSRF/DOM/Element/domainObjectAttr.pm | 15 + .../OpenSRF/DOM/Element/domainObjectCollection.pm | 116 ++ src/perlmods/OpenSRF/DOM/Element/param.pm | 4 + src/perlmods/OpenSRF/DOM/Element/params.pm | 4 + src/perlmods/OpenSRF/DOM/Element/searchCriteria.pm | 48 + .../OpenSRF/DOM/Element/searchCriterium.pm | 116 ++ .../OpenSRF/DOM/Element/searchTargetValue.pm | 23 + src/perlmods/OpenSRF/DOM/Element/userAuth.pm | 178 +++ src/perlmods/OpenSRF/DomainObject.pm | 90 ++ src/perlmods/OpenSRF/DomainObject/oilsMessage.pm | 344 ++++++ src/perlmods/OpenSRF/DomainObject/oilsMethod.pm | 100 ++ .../OpenSRF/DomainObject/oilsMultiSearch.pm | 186 ++++ src/perlmods/OpenSRF/DomainObject/oilsPrimitive.pm | 623 +++++++++++ src/perlmods/OpenSRF/DomainObject/oilsResponse.pm | 406 +++++++ src/perlmods/OpenSRF/DomainObject/oilsSearch.pm | 106 ++ src/perlmods/OpenSRF/DomainObjectCollection.pm | 35 + src/perlmods/OpenSRF/EX.pm | 251 +++++ src/perlmods/OpenSRF/System.pm | 364 ++++++ src/perlmods/OpenSRF/Transport.pm | 189 ++++ src/perlmods/OpenSRF/Transport/Jabber.pm | 11 + src/perlmods/OpenSRF/Transport/Jabber/JInbound.pm | 101 ++ .../OpenSRF/Transport/Jabber/JMessageWrapper.pm | 91 ++ .../OpenSRF/Transport/Jabber/JPeerConnection.pm | 80 ++ .../OpenSRF/Transport/Jabber/JabberClient.pm | 277 +++++ src/perlmods/OpenSRF/Transport/Listener.pm | 43 + src/perlmods/OpenSRF/Transport/PeerHandle.pm | 40 + src/perlmods/OpenSRF/Transport/SlimJabber.pm | 18 + .../OpenSRF/Transport/SlimJabber/Client.pm | 541 +++++++++ .../OpenSRF/Transport/SlimJabber/Inbound.pm | 94 ++ .../OpenSRF/Transport/SlimJabber/MessageWrapper.pm | 99 ++ .../OpenSRF/Transport/SlimJabber/PeerConnection.pm | 99 ++ src/perlmods/OpenSRF/UnixServer.pm | 160 +++ src/perlmods/OpenSRF/Utils.pm | 390 +++++++ src/perlmods/OpenSRF/Utils/Cache.pm | 63 ++ src/perlmods/OpenSRF/Utils/Config.pm | 434 ++++++++ src/perlmods/OpenSRF/Utils/LogServer.pm | 149 +++ src/perlmods/OpenSRF/Utils/Logger.pm | 355 ++++++ src/router/Makefile | 31 + src/router/router.c | 729 ++++++++++++ src/router/router.h | 234 ++++ 80 files changed, 16494 insertions(+) create mode 100755 bin/jabber_users_create create mode 100755 bin/opensrf_ctl create mode 100644 doc/OpenSRF-Messaging-Protocol.html create mode 100755 examples/math_bench.pl create mode 100755 examples/math_shell.pl create mode 100755 examples/math_simple.pl create mode 100644 include/opensrf/generic_utils.h create mode 100644 include/opensrf/transport_client.h create mode 100644 include/opensrf/transport_message.h create mode 100644 include/opensrf/transport_session.h create mode 100644 include/opensrf/transport_socket.h create mode 100644 src/javascript/JSON.js create mode 100644 src/javascript/md5.js create mode 100644 src/javascript/opensrf_app_session.js create mode 100644 src/javascript/opensrf_config.js create mode 100644 src/javascript/opensrf_dom_element.js create mode 100644 src/javascript/opensrf_domain_object.js create mode 100644 src/javascript/opensrf_jabber_transport.js create mode 100644 src/javascript/opensrf_msg_stack.js create mode 100644 src/javascript/opensrf_transport.js create mode 100644 src/javascript/opensrf_utils.js create mode 100644 src/libtransport/Makefile create mode 100644 src/libtransport/basic_client.c create mode 100644 src/libtransport/generic_utils.c create mode 100644 src/libtransport/transport_client.c create mode 100644 src/libtransport/transport_message.c create mode 100644 src/libtransport/transport_session.c create mode 100644 src/libtransport/transport_socket.c create mode 100644 src/patch/README create mode 100644 src/patch/mod_offline.c create mode 100644 src/patch/nad.c create mode 100644 src/perlmods/JSON.pm create mode 100644 src/perlmods/OpenSRF.pm create mode 100644 src/perlmods/OpenSRF/AppSession.pm create mode 100644 src/perlmods/OpenSRF/Application.pm create mode 100644 src/perlmods/OpenSRF/Application/Client.pm create mode 100644 src/perlmods/OpenSRF/Application/Demo/Math.pm create mode 100644 src/perlmods/OpenSRF/Application/Demo/MathDB.pm create mode 100644 src/perlmods/OpenSRF/DOM.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/domainObject.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/domainObjectAttr.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/domainObjectCollection.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/param.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/params.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/searchCriteria.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/searchCriterium.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/searchTargetValue.pm create mode 100644 src/perlmods/OpenSRF/DOM/Element/userAuth.pm create mode 100644 src/perlmods/OpenSRF/DomainObject.pm create mode 100644 src/perlmods/OpenSRF/DomainObject/oilsMessage.pm create mode 100644 src/perlmods/OpenSRF/DomainObject/oilsMethod.pm create mode 100644 src/perlmods/OpenSRF/DomainObject/oilsMultiSearch.pm create mode 100644 src/perlmods/OpenSRF/DomainObject/oilsPrimitive.pm create mode 100644 src/perlmods/OpenSRF/DomainObject/oilsResponse.pm create mode 100644 src/perlmods/OpenSRF/DomainObject/oilsSearch.pm create mode 100644 src/perlmods/OpenSRF/DomainObjectCollection.pm create mode 100644 src/perlmods/OpenSRF/EX.pm create mode 100644 src/perlmods/OpenSRF/System.pm create mode 100644 src/perlmods/OpenSRF/Transport.pm create mode 100644 src/perlmods/OpenSRF/Transport/Jabber.pm create mode 100644 src/perlmods/OpenSRF/Transport/Jabber/JInbound.pm create mode 100644 src/perlmods/OpenSRF/Transport/Jabber/JMessageWrapper.pm create mode 100644 src/perlmods/OpenSRF/Transport/Jabber/JPeerConnection.pm create mode 100644 src/perlmods/OpenSRF/Transport/Jabber/JabberClient.pm create mode 100644 src/perlmods/OpenSRF/Transport/Listener.pm create mode 100644 src/perlmods/OpenSRF/Transport/PeerHandle.pm create mode 100644 src/perlmods/OpenSRF/Transport/SlimJabber.pm create mode 100644 src/perlmods/OpenSRF/Transport/SlimJabber/Client.pm create mode 100644 src/perlmods/OpenSRF/Transport/SlimJabber/Inbound.pm create mode 100644 src/perlmods/OpenSRF/Transport/SlimJabber/MessageWrapper.pm create mode 100644 src/perlmods/OpenSRF/Transport/SlimJabber/PeerConnection.pm create mode 100644 src/perlmods/OpenSRF/UnixServer.pm create mode 100644 src/perlmods/OpenSRF/Utils.pm create mode 100644 src/perlmods/OpenSRF/Utils/Cache.pm create mode 100755 src/perlmods/OpenSRF/Utils/Config.pm create mode 100644 src/perlmods/OpenSRF/Utils/LogServer.pm create mode 100644 src/perlmods/OpenSRF/Utils/Logger.pm create mode 100644 src/router/Makefile create mode 100644 src/router/router.c create mode 100644 src/router/router.h diff --git a/bin/jabber_users_create b/bin/jabber_users_create new file mode 100755 index 0000000..16051a2 --- /dev/null +++ b/bin/jabber_users_create @@ -0,0 +1,50 @@ +#!/usr/bin/perl -w + +# Pulls the jabber users from the oils/jabber config files +# and populates the mysql db for the jabber server with the users + +use DBI; +use strict; +use OpenILS::Utils::Config qw( /pines/conf/oils.conf ); +my $config = OpenILS::Utils::Config->current; + +if( @ARGV < 2 ) { + print "usage: perl jcreate.pl dbhost dbuser dbpass\n"; + exit; +} + + +my $host = $ARGV[0]; +my $user = $ARGV[1]; +my $pass = $ARGV[2]; + +my $connection = DBI->connect( "DBI:mysql:jabberd2:$host", $user, $pass ) + or die "Cannot connect to db: $! \n"; + +my $jpass = $config->transport->auth->password; +my $realm = $config->transport->server->primary; + +# Delete all users +my $clean = "delete from authreg;"; +my $sth = $connection->prepare( $clean ); +$sth->execute(); + +my @users = keys %{$config->transport->users}; + +# Grab each user from the config and push them into mysql +for my $user (@users) { + if( ! $user or $user eq "__id" or $user eq "__sub") { next; } + print "Inserting $user: "; + + my $sql = "insert into authreg (username, realm, password) values " . + "('$user', '$realm', '$jpass');"; + + print "[$sql]\n"; + + $sth = $connection->prepare( $sql ); + $sth->execute(); + +} + +$sth->finish(); + diff --git a/bin/opensrf_ctl b/bin/opensrf_ctl new file mode 100755 index 0000000..c8469e0 --- /dev/null +++ b/bin/opensrf_ctl @@ -0,0 +1,44 @@ +#!/bin/bash + +# +# Simple rc script for controlling the system +# Only works on linux because of 'ps' syntax +# + + +case $1 in + "start") + perl -MOpenILS::System -e 'OpenILS::System->bootstrap()' & + sleep 2; + $0 status; + echo; + ;; + "stop") + PID=$(ps ax | grep "[0-9] System$" | awk '{print $1}'); + if [ -z $PID ]; then + echo "OpenILS System is not running"; + exit; + fi + echo "Killing System...$PID"; + kill -s INT $PID; + echo "Done"; + ;; + "status") + PID=$(ps ax | grep "[0-9] System$" | awk '{print $1}'); + if [ -z $PID ]; then + echo "OpenILS System is not running"; + exit 0; + fi + echo "OpenILS System is running"; + exit 1; + ;; + "restart") + $0 stop; + $0 start; + ;; + *) + echo "usage: system.sh [start|stop|restart|status]"; + ;; +esac + + diff --git a/doc/OpenSRF-Messaging-Protocol.html b/doc/OpenSRF-Messaging-Protocol.html new file mode 100644 index 0000000..4cb8f4e --- /dev/null +++ b/doc/OpenSRF-Messaging-Protocol.html @@ -0,0 +1,318 @@ + + + + + OILS Messaging + + + + + + +

Abstract

+ +

+ + The OILS messaging system works on two different primary layers: the transport layer and the + application layer. The transport layer manages virtual connections between client and server, + while the application layer manages user/application level messages. + + All messages must declare which protocol version they are requesting. The current protocol level + is 1. + +

Transport Layer

+ +

+ There are currently three types of messages in the transport layer: CONNECT, STATUS, and + DISCONNECT. + +

+ STATUS message provide general information to the transport layer are used in different + ways throughout the system. They are sent primarily by the server in response to client requests. + Each message comes with + a status and statusCode. The actual status part of the STATUS message is just a helpful message (mostly for debugging). The + statusCode is an integer representing the exact status this message represents. The status codes + are modeled after HTTP status codes. Currently used codes consist of the following: + +

+		100	STATUS_CONTINUE
+		200	STATUS_OK	
+		205	STATUS_COMPLETE
+		307	STATUS_REDIRECTED
+		400	STATUS_BADREQUEST
+		403	STATUS_FORBIDDEN
+		404	STATUS_NOTFOUND
+		408	STATUS_TIMEOUT
+		417	STATUS_EXPFAILED
+		
+ +

+ This list is likely to change at least a little. + + +

+ The CONNECT message initiates the virtual connection for a client and expects a STATUS + in return. If the connection is successful, the statusCode for the STATUS message shall be + STATUS_OK. If the authentication fails or if there is not actual authentication information + within the message, the statusCode for the returned message shall be STATUS_FORBIDDEN. + +

+ If at any point the client sends a non-connect message to the server when the client is not connected or the + connection has timed out, the STATUS that is returned shall have statusCode STATUS_EXPFAILED. + +

+ The DISCONNECT message is sent by the client to the server to end the virtual session. The server + shall not respond to any disconnect messages. + + +

Message Layer

+ +

+ There are currently two types of message layer messages: REQUEST and RESULT. REQUEST + messages represent application layer requests made by a client and RESULT messages are the servers + response to such REQUEST's. + +

+ By design, all CONNECT and REQUEST messages sent by a client will be acknowledged by one or + more responses from the server. This is much like the SYN-ACK philosophy of TCP, however different in many + ways. The only guarantees made by the server are 1. you will know that we received your request and 2. you + will know the final outcome of your request. It is the responsibility of the actual application to send + the requested application data (e.g. RESULT messages, intermediate STATUS messages). + + +

+ The server responses are matched to client requests by a threadTrace. A threadTrace is simply a + number and all application layer messages and STATUS messages are required to have one. (Note that the + threadTrace contained within a STATUS message sent in response to a CONNECT will be ignored). Currently, + there is no restriction on the number other than it shall be unique within a given virtual connection. + When the server receives a REQUEST message, it extracts the threadTrace from the message + and all responses to that request will contain the same threadTrace. + +

+ As mentioned above, every CONNECT message will be acknowledged by a single + STATUS message. REQUEST's are a little more complex, however. A REQUEST + will receive one or more RESULT's if the REQUEST warrants such a response. A REQUEST + may even receive one or more intermediate STATUS's (e.g. STATUS_CONTINUE). (Consult the + documentation on the application request the client is requesting for more information on the number and + type of responses to that request). All REQUEST's, however, regardless of other response types, + shall receieve as the last response a STATUS message with statusCode STATUS_COMPLETE. This + allows the client to wait for REQUEST "completeness" as opposed to waiting on or calculating individual + responses. + + +

Client Pseudocode

+ +
+
+send CONNECT
+
+msg = recv()
+
+if ( msg.statusCode == STATUS_OK ) 
+
+	OK. continue
+
+if ( msg.statusCode == STATUS_FORBIDDEN ) 
+
+	handle authentication failure and attempt another connect if requested
+
+while ( more requests ) {
+
+	/* you may send multiple requests before processing any responses.  For the sake
+		of this example, we will only walk through a single client request */
+
+	send REQUEST with threadTrace X 
+
+	while ( response = recv ) { 
+
+		if (  response.threadTrace != X ) 
+
+			continue/ignore
+
+		if ( response.type == STATUS )
+		
+			if (  response.statusCode == STATUS_TIMEOUT		or
+					response.statusCode == STATUS_REDIRECTED	or
+					response.statusCode == STATUS_EXPFAILED)
+
+				resend the the request with threadTrace X because it was not honored.
+
+			if ( response.statusCode == STATUS_COMPLETE ) 
+
+				the request is now complete, nothing more to be done with this request
+				break out of loop
+	
+		if ( response.typ == RESULT )
+
+			pass result to the application layer for processing
+
+	} // receiving
+
+} // sending
+
+
+		
+ +
+

Server Pseudocode

+ +
+
+while( message = recv() ) {
+
+	if( message.type != CONNECT )
+
+		return a STATUS with statusCode STATUS_EXPFAILED
+		start loop over
+
+	if ( message.type == CONNECT and server is unable to authenticate the client )
+
+		return a STATUS with statusCode STATUS_FORBIDDEN
+		start loop over
+
+	if ( message.type == CONNECT and server is able to authenticate user )
+
+		return STATUS with statusCode STATUS_OK and continue
+
+	while ( msg = recv() and virtual session is active ) {
+
+
+		if ( msg.type == REQUEST )
+
+			Record the threadTrace.  Pass the REQUEST to the application layer for processing.
+			When the application layer has completed processing, respond to the client
+			with a STATUS message with statusCode STATUS_COMPLETE and threadTrace matching
+			the threadTrace of the REQUEST.  Once the final STATUS_COMPLETE message is sent,
+			the session is over.  Return to outer server loop. 
+
+			/* Note: during REQUEST processing by the application layer, the application may 
+				opt to send RESULT and/or STATUS messages to the client.  The server side
+				transport mechanism is not concerned with these messages.  The server only 
+				needs to be notified when the REQUEST has been sucessfully completed. */
+
+		if( message.type == DISCONNECT )
+
+			Virtual session has ended. Return to outer loop.
+
+
+	} // Sessin loop
+
+} // Main server loop
+
+
+
+		
+ + +
+

XML Examples

+
+ + +

Protocol Element

+ +
+
+<oils:domainObjectAttr value="1" name="protocol"/>
+
+		
+ +

threadTrace Element

+ +
+
+<oils:domainObjectAttr value="1" name="threadTrace"/>
+
+		
+ +

Type element

+ +
+
+<oils:userAuth hashseed="237" secret="89dd8c65300d4af126cf467779ff1820" username="bill"/>
+
+		
+ +

CONNECT Message

+ +
+
+<oils:domainObjectAttr value="CONNECT" name="type"/>
+	<oils:userAuth hashseed="237" secret="89dd8c65300d4af126cf467779ff1820" username="bill"/>
+	<oils:domainObjectAttr value="1" name="threadTrace"/>
+	<oils:domainObjectAttr value="1" name="protocol"/>
+</oils:domainObject>
+
+		
+ + +

DISCONNECT Message

+ +
+
+<oils:domainObject name="oilsMessage">
+	<oils:domainObjectAttr value="DISCONNECT" name="type"/>
+	<oils:domainObjectAttr value="0" name="threadTrace"/>
+	<oils:domainObjectAttr value="1" name="protocol"/>
+</oils:domainObject>
+
+		
+ +

STATUS Message

+ +
+
+<oils:domainObject name="oilsMessage">
+	<oils:domainObjectAttr value="STATUS" name="type"/>
+	<oils:domainObjectAttr value="0" name="threadTrace"/>
+	<oils:domainObjectAttr value="1" name="protocol"/>
+	<oils:domainObject name="oilsConnectStatus">
+		<oils:domainObjectAttr value="Connection Successful" name="status"/>
+		<oils:domainObjectAttr value="200" name="statusCode"/>
+	</oils:domainObject>
+</oils:domainObject>
+
+		
+ +

REQUEST Message

+ +
+
+<oils:domainObject name="oilsMessage">
+	<oils:domainObjectAttr value="REQUEST" name="type"/>
+	<oils:domainObjectAttr value="4" name="threadTrace"/>
+	<oils:domainObjectAttr value="1" name="protocol"/>
+	<oils:domainObject name="oilsMethod">
+		<oils:domainObjectAttr value="mult" name="method"/>
+		<oils:params>
+			<oils:param>1
+			<oils:param>2
+		</oils:params>
+	</oils:domainObject>
+</oils:domainObject>
+
+		
+ +

RESULT Message

+ +
+
+<oils:domainObject name="oilsMessage">
+	<oils:domainObjectAttr value="RESULT" name="type"/>
+	<oils:domainObjectAttr value="4" name="threadTrace"/>
+	<oils:domainObjectAttr value="1" name="protocol"/>
+	<oils:domainObject name="oilsResult">
+		<oils:domainObjectAttr value="OK" name="status"/>
+		<oils:domainObjectAttr value="200" name="statusCode"/>
+		<oils:domainObject name="oilsScalar">2</oils:domainObject>
+	</oils:domainObject>
+</oils:domainObject>
+
+		
+ + + + + + + diff --git a/examples/math_bench.pl b/examples/math_bench.pl new file mode 100755 index 0000000..30f81af --- /dev/null +++ b/examples/math_bench.pl @@ -0,0 +1,110 @@ +#!/usr/bin/perl -w +use strict;use warnings; +use OpenILS::System; +use OpenILS::DOM::Element::userAuth; +use OpenILS::Utils::Config; +use OpenILS::DomainObject::oilsMethod; +use OpenILS::DomainObject::oilsPrimitive; +use Time::HiRes qw/time/; +use OpenILS::EX qw/:try/; + +$| = 1; + +# ---------------------------------------------------------------------------------------- +# This is a quick and dirty script to perform benchmarking against the math server. +# Note: 1 request performs a batch of 4 queries, one for each supported method: add, sub, +# mult, div. +# Usage: $ perl math_bench.pl +# ---------------------------------------------------------------------------------------- + + +my $count = $ARGV[0]; + +unless( $count ) { + print "usage: ./math_bench.pl \n"; + exit; +} + +warn "PID: $$\n"; + +my $config = OpenILS::Utils::Config->current; +OpenILS::System->bootstrap_client(); + +my $session = OpenILS::AppSession->create( + "math", username => 'math_bench', secret => '12345' ); + +try { + if( ! ($session->connect()) ) { die "Connect timed out\n"; } + +} catch OpenILS::EX with { + my $e = shift; + warn "Connection Failed *\n"; + die $e; +} + +my @times; +my %vals = ( add => 3, sub => -1, mult => 2, div => 0.5 ); + +for my $x (1..100) { + if( $x % 10 ) { print ".";} + else{ print $x/10; }; +} +print "\n"; + +my $c = 0; + +for my $scale ( 1..$count ) { + for my $mname ( keys %vals ) { + + my $method = OpenILS::DomainObject::oilsMethod->new( method => $mname ); + $method->params( 1,2 ); + + my $req; + my $resp; + my $starttime; + try { + + $starttime = time(); + $req = $session->request( $method ); + $resp = $req->recv( timeout => 10 ); + push @times, time() - $starttime; + + } catch OpenILS::EX with { + my $e = shift; + die "ERROR\n $e"; + + } catch Error with { + my $e = shift; + die "Caught unknown error: $e"; + }; + + + if( ! $req->complete ) { warn "\nIncomplete\n"; } + + + if ( $resp ) { + + my $ret = $resp->content(); + if( "$ret" eq $vals{$mname} ) { print "+"; } + + else { print "*BAD*\n" . $resp->toString(1) . "\n"; } + + } else { print "*NADA*"; } + + $req->finish(); + $c++; + + } + print "\n[$c] \n" unless $scale % 25; +} + +$session->kill_me(); + +my $total = 0; + +$total += $_ for (@times); + +$total /= scalar(@times); + +print "\n\n\tAverage Round Trip Time: $total Seconds\n"; + diff --git a/examples/math_shell.pl b/examples/math_shell.pl new file mode 100755 index 0000000..b61157b --- /dev/null +++ b/examples/math_shell.pl @@ -0,0 +1,139 @@ +#!/usr/bin/perl -w +use strict;use warnings; +use OpenILS::System; +use OpenILS::Utils::Config; +use OpenILS::DomainObject::oilsMethod; +use OpenILS::DomainObject::oilsPrimitive; +use OpenILS::EX qw/:try/; +$| = 1; + +# ---------------------------------------------------------------------------------------- +# Simple math shell where you can test the transport system. +# Enter simple, binary equations ony using +, -, *, and / +# Example: # 1+1 +# Usage: % perl math_shell.pl +# ---------------------------------------------------------------------------------------- + +# load the config +my $config = OpenILS::Utils::Config->current; + +# connect to the transport (jabber) server +OpenILS::System->bootstrap_client(); + +# build the AppSession object. +my $session = OpenILS::AppSession->create( + "math", username => 'math_bench', secret => '12345' ); + +# launch the shell +print "type 'exit' or 'quit' to leave the shell\n"; +print "# "; +while( my $request = <> ) { + + chomp $request ; + + # exit loop if user enters 'exit' or 'quit' + if( $request =~ /exit/i or $request =~ /quit/i ) { last; } + + # figure out what the user entered + my( $a, $mname, $b ) = parse_request( $request ); + + if( $a =~ /error/ ) { + print "Parse Error. Try again. \nExample # 1+1\n"; + next; + } + + + try { + + # Connect to the MATH server + if( ! ($session->connect()) ) { die "Connect timed out\n"; } + + } catch OpenILS::EX with { + my $e = shift; + die "* * Connection Failed with:\n$e"; + }; + + my $method = OpenILS::DomainObject::oilsMethod->new( method => $mname ); + $method->params( $a, $b ); + + my $req; + my $resp; + + try { + $req = $session->request( $method ); + + # we know that this request only has a single reply + # if your expecting a 'stream' of results, you can + # do: while( $resp = $req->recv( timeout => 10 ) ) {} + $resp = $req->recv( timeout => 10 ); + + } catch OpenILS::EX with { + + # Any transport layer or server problems will launch an exception + my $e = shift; + die "ERROR Receiving\n $e"; + + } catch Error with { + + # something just died somethere + my $e = shift; + die "Caught unknown error: $e"; + }; + + if ( $resp ) { + # ---------------------------------------------------------------------------------------- + # $resp is an OpenILS::DomainObject::oilsResponse object. $resp->content() returns whatever + # data the object has. If the server returns an exception that we're meant to see, then + # the data will be an exception object. In this case, barring any exception, we know that + # the data is an OpenILS::DomainObject::oilsScalar object which has a value() method + # that returns a perl scalar. For us, that scalar is just a number. + # ---------------------------------------------------------------------------------------- + + if( UNIVERSAL::isa( $resp, "OpenILS::EX" ) ) { + throw $resp; + } + + my $ret = $resp->content(); + print $ret->value(); + } + + $req->finish(); + + print "\n# "; + +} + +# disconnect from the MATH server +$session->kill_me(); +exit; + +# ------------------------------------------------------------------------------------ +# parse the user input string +# returns a list of the form (first param, operation, second param) +# These operations are what the MATH server recognizes as method names +# ------------------------------------------------------------------------------------ +sub parse_request { + my $string = shift; + my $op; + my @ops; + + while( 1 ) { + + @ops = split( /\+/, $string ); + if( @ops > 1 ) { $op = "add"; last; } + + @ops = split( /\-/, $string ); + if( @ops > 1 ) { $op = "sub"; last; } + + @ops = split( /\*/, $string ); + if( @ops > 1 ) { $op = "mult", last; } + + @ops = split( /\//, $string ); + if( @ops > 1 ) { $op = "div"; last; } + + return ("error"); + } + + return ($ops[0], $op, $ops[1]); +} + diff --git a/examples/math_simple.pl b/examples/math_simple.pl new file mode 100755 index 0000000..73b015e --- /dev/null +++ b/examples/math_simple.pl @@ -0,0 +1,86 @@ +#!/usr/bin/perl -w +use strict;use warnings; +use OpenILS::System; +use OpenILS::Utils::Config; +use OpenILS::DomainObject::oilsMethod; +use OpenILS::DomainObject::oilsPrimitive; +use OpenILS::EX qw/:try/; +$| = 1; + + +# ---------------------------------------------------------------------------------------- +# This script makes a single query, 1 + 2, to the the MATH test app and prints the result +# Usage: % perl math_simple.pl +# ---------------------------------------------------------------------------------------- + + +# connect to the transport (jabber) server +OpenILS::System->bootstrap_client(); + +# build the AppSession object. +my $session = OpenILS::AppSession->create( + "math", username => 'math_bench', secret => '12345' ); + +try { + + # Connect to the MATH server + if( ! ($session->connect()) ) { die "Connect timed out\n"; } + +} catch OpenILS::EX with { + my $e = shift; + die "* * Connection Failed with:\n$e"; +}; + +my $method = OpenILS::DomainObject::oilsMethod->new( method => "add" ); +$method->params( 1, 2 ); + +my $req; +my $resp; + +try { + $req = $session->request( $method ); + + # we know that this request only has a single reply + # if your expecting a 'stream' of results, you can + # do: while( $resp = $req->recv( timeout => 10 ) ) {} + $resp = $req->recv( timeout => 10 ); + +} catch OpenILS::EX with { + + # Any transport layer or server problems will launch an exception + my $e = shift; + die "ERROR Receiving\n $e"; + +} catch Error with { + + # something just died somethere + my $e = shift; + die "Caught unknown error: $e"; +}; + +if ( $resp ) { + # ---------------------------------------------------------------------------------------- + # $resp is an OpenILS::DomainObject::oilsResponse object. $resp->content() returns whatever + # data the object has. If the server returns an exception that we're meant to see, then + # the data will be an exception object. In this case, barring any exception, we know that + # the data is an OpenILS::DomainObject::oilsScalar object which has a value() method + # that returns a perl scalar. For us, that scalar is just a number. + # ---------------------------------------------------------------------------------------- + + if( UNIVERSAL::isa( $resp, "OpenILS::EX" ) ) { + throw $resp; + } + + my $ret = $resp->content(); + print "Should print 3 => " . $ret->value() . "\n"; + +} else { + die "No Response from Server!\n"; +} + +$req->finish(); + +# disconnect from the MATH server +$session->kill_me(); +exit; + diff --git a/include/opensrf/generic_utils.h b/include/opensrf/generic_utils.h new file mode 100644 index 0000000..aa74221 --- /dev/null +++ b/include/opensrf/generic_utils.h @@ -0,0 +1,80 @@ +#include +#include +#include +#include + +/* libxml stuff for the config reader */ +#include +#include +#include +#include +#include + +#ifdef DMALLOC +#include "dmalloc.h" +#endif + + +#ifndef GENERIC_UTILS_H +#define GENERIC_UTILS_H + + +/** Malloc's, checks for NULL, clears all memory bits and + * returns the pointer + * + * @param size How many bytes of memory to allocate + */ +inline void* safe_malloc( int size ); + +/* 10M limit on buffers for overflow protection */ +#define BUFFER_MAX_SIZE 10485760 + +// --------------------------------------------------------------------------------- +// Generic growing buffer. Add data all you want +// --------------------------------------------------------------------------------- +struct growing_buffer_struct { + char *buf; + int n_used; + int size; +}; +typedef struct growing_buffer_struct growing_buffer; + +growing_buffer* buffer_init( int initial_num_bytes); +int buffer_addchar(growing_buffer* gb, char c); +int buffer_add(growing_buffer* gb, char* c); +int buffer_reset( growing_buffer* gb); +char* buffer_data( growing_buffer* gb); +int buffer_free( growing_buffer* gb ); + + +void log_free(); + +// Utility method +void get_timestamp( char buf_25chars[]); + +// --------------------------------------------------------------------------------- +// Error handling interface. +// --------------------------------------------------------------------------------- + +void fatal_handler( char* message, ...); +void warning_handler( char* message, ... ); +void info_handler( char* message, ... ); + +// --------------------------------------------------------------------------------- +// Config file module +// --------------------------------------------------------------------------------- +struct config_reader_struct { + xmlDocPtr config_doc; + xmlXPathContextPtr xpathCx; +}; +typedef struct config_reader_struct config_reader; +config_reader* conf_reader; + +void config_reader_init( char* config_file ); + +void config_reader_free(); + +// allocastes a char*. FREE me. +char* config_value( const char* xpath_query, ... ); + +#endif diff --git a/include/opensrf/transport_client.h b/include/opensrf/transport_client.h new file mode 100644 index 0000000..3d02add --- /dev/null +++ b/include/opensrf/transport_client.h @@ -0,0 +1,87 @@ +#include "transport_session.h" +#include + +#ifdef DMALLOC +#include "dmalloc.h" +#endif + +#ifndef TRANSPORT_CLIENT_H +#define TRANSPORT_CLIENT_H + +#define MESSAGE_LIST_HEAD 1 +#define MESSAGE_LIST_ITEM 2 + + +// --------------------------------------------------------------------------- +// Represents a node in a linked list. The node holds a pointer to the next +// node (which is null unless set), a pointer to a transport_message, and +// and a type variable (which is not really curently necessary). +// --------------------------------------------------------------------------- +struct message_list_struct { + struct message_list_struct* next; + transport_message* message; + int type; +}; + +typedef struct message_list_struct transport_message_list; +typedef struct message_list_struct transport_message_node; + +// --------------------------------------------------------------------------- +// Our client struct. We manage a list of messages and a controlling session +// --------------------------------------------------------------------------- +struct transport_client_struct { + transport_message_list* m_list; + transport_session* session; +}; +typedef struct transport_client_struct transport_client; + +// --------------------------------------------------------------------------- +// Allocates and initializes and transport_client. This does no connecting +// The user must call client_free(client) when finished with the allocated +// object. +// --------------------------------------------------------------------------- +transport_client* client_init( char* server, int port ); + +// --------------------------------------------------------------------------- +// Connects to the Jabber server with the provided information. Returns 1 on +// success, 0 otherwise. +// --------------------------------------------------------------------------- +int client_connect( transport_client* client, + char* username, char* password, char* resource, int connect_timeout ); + +int client_disconnect( transport_client* client ); + +// --------------------------------------------------------------------------- +// De-allocates memory associated with a transport_client object. Users +// must use this method when finished with a client object. +// --------------------------------------------------------------------------- +int client_free( transport_client* client ); + +// --------------------------------------------------------------------------- +// Sends the given message. The message must at least have the recipient +// field set. +// --------------------------------------------------------------------------- +int client_send_message( transport_client* client, transport_message* msg ); + +// --------------------------------------------------------------------------- +// Returns 1 if this client is currently connected to the server, 0 otherwise +// --------------------------------------------------------------------------- +int client_connected( transport_client* client ); + +// --------------------------------------------------------------------------- +// This is the message handler required by transport_session. This handler +// takes all incoming messages and puts them into the back of a linked list +// of messages. +// --------------------------------------------------------------------------- +void client_message_handler( void* client, transport_message* msg ); + +// --------------------------------------------------------------------------- +// If there are any message in the message list, the 'oldest' message is +// returned. If not, this function will wait at most 'timeout' seconds +// for a message to arrive. Specifying -1 means that this function will not +// return unless a message arrives. +// --------------------------------------------------------------------------- +transport_message* client_recv( transport_client* client, int timeout ); + + +#endif diff --git a/include/opensrf/transport_message.h b/include/opensrf/transport_message.h new file mode 100644 index 0000000..21e3120 --- /dev/null +++ b/include/opensrf/transport_message.h @@ -0,0 +1,94 @@ +#include "libxml.h" + +#include "generic_utils.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef DMALLOC +#include "dmalloc.h" +#endif + +#ifndef TRANSPORT_MESSAGE_H +#define TRANSPORT_MESSAGE_H + + + +// --------------------------------------------------------------------------------- +// Jabber message object. +// --------------------------------------------------------------------------------- +struct transport_message_struct { + char* body; + char* subject; + char* thread; + char* recipient; + char* sender; + char* router_from; + char* router_to; + char* router_class; + char* router_command; + int is_error; + char* error_type; + int error_code; + int broadcast; + char* msg_xml; /* the entire message as XML complete with entity encoding */ +}; +typedef struct transport_message_struct transport_message; + +// --------------------------------------------------------------------------------- +// Allocates and returns a transport_message. All chars are safely re-allocated +// within this method. +// Returns NULL on error +// --------------------------------------------------------------------------------- +transport_message* message_init( char* body, char* subject, + char* thread, char* recipient, char* sender ); + + +void message_set_router_info( transport_message* msg, char* router_from, + char* router_to, char* router_class, char* router_command, int broadcast_enabled ); + +// --------------------------------------------------------------------------------- +// Formats the Jabber message as XML for encoding. +// Returns NULL on error +// --------------------------------------------------------------------------------- +char* message_to_xml( const transport_message* msg ); + + +// --------------------------------------------------------------------------------- +// Call this to create the encoded XML for sending on the wire. +// This is a seperate function so that encoding will not necessarily have +// to happen on all messages (i.e. typically only occurs outbound messages). +// --------------------------------------------------------------------------------- +int message_prepare_xml( transport_message* msg ); + +// --------------------------------------------------------------------------------- +// Deallocates the memory used by the transport_message +// Returns 0 on error +// --------------------------------------------------------------------------------- +int message_free( transport_message* msg ); + +// --------------------------------------------------------------------------------- +// Prepares the shared XML document +// --------------------------------------------------------------------------------- +//int message_init_xml(); + +// --------------------------------------------------------------------------------- +// Determines the username of a Jabber ID. This expects a pre-allocated char +// array for the return value. +// --------------------------------------------------------------------------------- +void jid_get_username( const char* jid, char buf[] ); + +// --------------------------------------------------------------------------------- +// Determines the resource of a Jabber ID. This expects a pre-allocated char +// array for the return value. +// --------------------------------------------------------------------------------- +void jid_get_resource( const char* jid, char buf[] ); + +void set_msg_error( transport_message*, char* error_type, int error_code); + +#endif diff --git a/include/opensrf/transport_session.h b/include/opensrf/transport_session.h new file mode 100644 index 0000000..b78f20d --- /dev/null +++ b/include/opensrf/transport_session.h @@ -0,0 +1,217 @@ +// --------------------------------------------------------------------------------- +// Manages the Jabber session. Data is taken from the TCP object and pushed into +// a SAX push parser as it arrives. When key Jabber documetn elements are met, +// logic ensues. +// --------------------------------------------------------------------------------- +#include "libxml.h" +#include "transport_socket.h" +#include "transport_message.h" +#include "generic_utils.h" + +#include +#include +#include +#include +#include /* only for xmlNewInputFromFile() */ +#include +#include +#include + +#ifndef TRANSPORT_SESSION_H +#define TRANSPORT_SESSION_H + +#define CONNECTING_1 1 /* just starting the connection to Jabber */ +#define CONNECTING_2 2 /* First packet sent and packet received from server */ + +/* Note. these are growing buffers, so all that's necessary is a sane starting point */ +#define JABBER_BODY_BUFSIZE 4096 +#define JABBER_SUBJECT_BUFSIZE 64 +#define JABBER_THREAD_BUFSIZE 64 +#define JABBER_JID_BUFSIZE 64 +#define JABBER_STATUS_BUFSIZE 16 + +#ifdef DMALLOC +#include "dmalloc.h" +#endif + +// --------------------------------------------------------------------------------- +// Takes data from the socket handler and pushes it directly into the push parser +// --------------------------------------------------------------------------------- +void grab_incoming( void * session, char* data ); + +// --------------------------------------------------------------------------------- +// Callback for handling the startElement event. Much of the jabber logic occurs +// in this and the characterHandler callbacks. +// Here we check for the various top level jabber elements: body, iq, etc. +// --------------------------------------------------------------------------------- +void startElementHandler( + void *session, const xmlChar *name, const xmlChar **atts); + +// --------------------------------------------------------------------------------- +// Callback for handling the endElement event. Updates the Jabber state machine +// to let us know the element is over. +// --------------------------------------------------------------------------------- +void endElementHandler( void *session, const xmlChar *name); + +// --------------------------------------------------------------------------------- +// This is where we extract XML text content. In particular, this is useful for +// extracting Jabber message bodies. +// --------------------------------------------------------------------------------- +void characterHandler( + void *session, const xmlChar *ch, int len); + +void parseWarningHandler( void *session, const char* msg, ... ); +void parseErrorHandler( void *session, const char* msg, ... ); + +// --------------------------------------------------------------------------------- +// Tells the SAX parser which functions will be used as event callbacks +// --------------------------------------------------------------------------------- +static xmlSAXHandler SAXHandlerStruct = { + NULL, /* internalSubset */ + NULL, /* isStandalone */ + NULL, /* hasInternalSubset */ + NULL, /* hasExternalSubset */ + NULL, /* resolveEntity */ + NULL, /* getEntity */ + NULL, /* entityDecl */ + NULL, /* notationDecl */ + NULL, /* attributeDecl */ + NULL, /* elementDecl */ + NULL, /* unparsedEntityDecl */ + NULL, /* setDocumentLocator */ + NULL, /* startDocument */ + NULL, /* endDocument */ + startElementHandler, /* startElement */ + endElementHandler, /* endElement */ + NULL, /* reference */ + characterHandler, /* characters */ + NULL, /* ignorableWhitespace */ + NULL, /* processingInstruction */ + NULL, /* comment */ + parseWarningHandler, /* xmlParserWarning */ + parseErrorHandler, /* xmlParserError */ + NULL, /* xmlParserFatalError : unused */ + NULL, /* getParameterEntity */ + NULL, /* cdataBlock; */ + NULL, /* externalSubset; */ + 1, + NULL, + NULL, /* startElementNs */ + NULL, /* endElementNs */ + NULL /* xmlStructuredErrorFunc */ +}; + +// --------------------------------------------------------------------------------- +// Our SAX handler pointer. +// --------------------------------------------------------------------------------- +static const xmlSAXHandlerPtr SAXHandler = &SAXHandlerStruct; + +// --------------------------------------------------------------------------------- +// Jabber state machine. This is how we know where we are in the Jabber +// conversation. +// --------------------------------------------------------------------------------- +struct jabber_state_machine_struct { + int connected; + int connecting; + int in_message; + int in_message_body; + int in_thread; + int in_subject; + int in_error; + int in_message_error; + int in_iq; + int in_presence; + int in_status; +}; +typedef struct jabber_state_machine_struct jabber_machine; + +// --------------------------------------------------------------------------------- +// Transport session. This maintains all the various parts of a session +// --------------------------------------------------------------------------------- +struct transport_session_struct { + + /* our socket connection */ + transport_socket* sock_obj; + /* our Jabber state machine */ + jabber_machine* state_machine; + /* our SAX push parser context */ + xmlParserCtxtPtr parser_ctxt; + + /* our text buffers for holding text data */ + growing_buffer* body_buffer; + growing_buffer* subject_buffer; + growing_buffer* thread_buffer; + growing_buffer* from_buffer; + growing_buffer* recipient_buffer; + growing_buffer* status_buffer; + growing_buffer* message_error_type; + int message_error_code; + + /* for OILS extenstions */ + growing_buffer* router_to_buffer; + growing_buffer* router_from_buffer; + growing_buffer* router_class_buffer; + growing_buffer* router_command_buffer; + int router_broadcast; + + /* this can be anything. It will show up in the + callbacks for your convenience. We will *not* + deallocate whatever this is when we're done. That's + your job. + */ + void* user_data; + + /* the Jabber message callback */ + void (*message_callback) ( void* user_data, transport_message* msg ); + //void (iq_callback) ( void* user_data, transport_iq_message* iq ); +}; +typedef struct transport_session_struct transport_session; + + +// ------------------------------------------------------------------ +// Allocates and initializes the necessary transport session +// data structures. +// ------------------------------------------------------------------ +transport_session* init_transport( char* server, int port, void* user_data ); + +// ------------------------------------------------------------------ +// Returns the value of the given XML attribute +// The xmlChar** construct is commonly returned from SAX event +// handlers. Pass that in with the name of the attribute you want +// to retrieve. +// ------------------------------------------------------------------ +char* get_xml_attr( const xmlChar** atts, char* attr_name ); + +// ------------------------------------------------------------------ +// Waits at most 'timeout' seconds for data to arrive from the +// TCP handler. A timeout of -1 means to wait indefinitely. +// ------------------------------------------------------------------ +int session_wait( transport_session* session, int timeout ); + +// --------------------------------------------------------------------------------- +// Sends the given Jabber message +// --------------------------------------------------------------------------------- +int session_send_msg( transport_session* session, transport_message* msg ); + +// --------------------------------------------------------------------------------- +// Returns 1 if this session is connected to the jabber server. 0 otherwise +// --------------------------------------------------------------------------------- +int session_connected( transport_session* ); + +// ------------------------------------------------------------------ +// Deallocates session memory +// ------------------------------------------------------------------ +int session_free( transport_session* session ); + +// ------------------------------------------------------------------ +// Connects to the Jabber server. Waits at most connect_timeout +// seconds before failing +// ------------------------------------------------------------------ +int session_connect( transport_session* session, + const char* username, const char* password, const char* resource, int connect_timeout ); + +int session_disconnect( transport_session* session ); + +int reset_session_buffers( transport_session* session ); + +#endif diff --git a/include/opensrf/transport_socket.h b/include/opensrf/transport_socket.h new file mode 100644 index 0000000..65a83cc --- /dev/null +++ b/include/opensrf/transport_socket.h @@ -0,0 +1,68 @@ +#include "generic_utils.h" + +#include +#include +#include +#include + +//--------------------------------------------------------------- +// WIN32 +//--------------------------------------------------------------- +#ifdef WIN32 +#include +#include +#else + +//--------------------------------------------------------------- +// Unix headers +//--------------------------------------------------------------- +#include +#include +#include +#include +#include +#include +#include +#include +#endif + +#ifdef DMALLOC +#include "dmalloc.h" +#endif + +#ifndef TRANSPORT_SOCKET_H +#define TRANSPORT_SOCKET_H + +/* how many characters we read from the socket at a time */ +#ifdef _ROUTER +#define BUFSIZE 412 +#else +#define BUFSIZE 4096 +#endif + +/* we maintain the socket information */ +struct transport_socket_struct { + int sock_fd; + int connected; + char* server; + int port; + void * user_data; + /* user_data may be anything. it's whatever you wish + to see showing up in the callback in addition to + the acutal character data*/ + void (*data_received_callback) (void * user_data, char*); +}; +typedef struct transport_socket_struct transport_socket; + +int tcp_connect( transport_socket* obj ); +int tcp_send( transport_socket* obj, const char* data ); +int tcp_disconnect( transport_socket* obj ); +int tcp_wait( transport_socket* obj, int timeout ); +int tcp_connected( transport_socket* obj ); + +/* utility methods */ +int set_fl( int fd, int flags ); +int clr_fl( int fd, int flags ); + + +#endif diff --git a/src/javascript/JSON.js b/src/javascript/JSON.js new file mode 100644 index 0000000..03ec070 --- /dev/null +++ b/src/javascript/JSON.js @@ -0,0 +1,89 @@ +// in case we run on an implimentation that doesn't have "undefined"; +var undefined; + +function Cast (obj, class_constructor) { + try { + if (eval(class_constructor + '.prototype["class_name"]')) { + var template = eval("new " + class_constructor + "()"); + // copy the methods over to 'obj' + for (var m in obj) { + if (typeof(obj[m]) != 'undefined') { + template[m] = obj[m]; + } + } + obj = template; + } + } catch( E ) { + obj['class_name'] = function () { return class_constructor }; + //dump( super_dump(E) + "\n"); + } finally { + return obj; + } +} + +function JSON2js (json) { + json = json.replace( /\/\*--\s*S\w*?\s*?\s+\w+\s*--\*\//g, 'Cast('); + json = json.replace( /\/\*--\s*E\w*?\s*?\s+(\w+)\s*--\*\//g, ', "$1")'); + var obj; + if (json != '') { + eval( 'obj = ' + json ); + } + obj.toString = function () { return js2JSON(this) }; + return obj; +} + +function js2JSON(arg) { +var i, o, u, v; + + switch (typeof arg) { + case 'object': + if (arg) { + if (arg.constructor == Array) { + o = ''; + for (i = 0; i < arg.length; ++i) { + v = js2JSON(arg[i]); + if (o) { + o += ','; + } + if (v !== u) { + o += v; + } else { + o += 'null,'; + } + } + return '[' + o + ']'; + } else if (typeof arg.toString != 'undefined') { + o = ''; + for (i in arg) { + v = js2JSON(arg[i]); + if (v !== u) { + if (o) { + o += ','; + } + o += js2JSON(i) + ':' + v; + } + } + var obj_start = '{'; + var obj_end = '}'; + try { + if ( arg.class_name() ) { + obj_start = '/*--S ' + arg.class_name() + '--*/{'; + obj_end = '}/*--E ' + arg.class_name() + '--*/'; + } + } catch( E ) {} + o = obj_start + o + obj_end; + return o; + } else { + return; + } + } + return 'null'; + case 'unknown': + case 'undefined': + case 'function': + return u; + case 'string': + default: + return '"' + String(arg).replace(/(["\\])/g, '\\$1') + '"'; + } +} diff --git a/src/javascript/md5.js b/src/javascript/md5.js new file mode 100644 index 0000000..46d2aab --- /dev/null +++ b/src/javascript/md5.js @@ -0,0 +1,256 @@ +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Configurable variables. You may need to tweak these to be compatible with + * the server-side, but the defaults work in most cases. + */ +var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */ +var b64pad = ""; /* base-64 pad character. "=" for strict RFC compliance */ +var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */ + +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ +function hex_md5(s){ return binl2hex(core_md5(str2binl(s), s.length * chrsz));} +function b64_md5(s){ return binl2b64(core_md5(str2binl(s), s.length * chrsz));} +function str_md5(s){ return binl2str(core_md5(str2binl(s), s.length * chrsz));} +function hex_hmac_md5(key, data) { return binl2hex(core_hmac_md5(key, data)); } +function b64_hmac_md5(key, data) { return binl2b64(core_hmac_md5(key, data)); } +function str_hmac_md5(key, data) { return binl2str(core_hmac_md5(key, data)); } + +/* + * Perform a simple self-test to see if the VM is working + */ +function md5_vm_test() +{ + return hex_md5("abc") == "900150983cd24fb0d6963f7d28e17f72"; +} + +/* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ +function core_md5(x, len) +{ + /* append padding */ + x[len >> 5] |= 0x80 << ((len) % 32); + x[(((len + 64) >>> 9) << 4) + 14] = len; + + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + + a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936); + d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i+ 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897); + d = md5_ff(d, a, b, c, x[i+ 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i+ 8], 7 , 1770035416); + d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i+10], 17, -42063); + b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i+12], 7 , 1804603682); + d = md5_ff(d, a, b, c, x[i+13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i+15], 22, 1236535329); + + a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510); + d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632); + c = md5_gg(c, d, a, b, x[i+11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691); + d = md5_gg(d, a, b, c, x[i+10], 9 , 38016083); + c = md5_gg(c, d, a, b, x[i+15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i+ 9], 5 , 568446438); + d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690); + c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i+ 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467); + d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784); + c = md5_gg(c, d, a, b, x[i+ 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734); + + a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558); + d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i+11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i+14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060); + d = md5_hh(d, a, b, c, x[i+ 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i+13], 4 , 681279174); + d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i+ 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487); + d = md5_hh(d, a, b, c, x[i+12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i+15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651); + + a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844); + d = md5_ii(d, a, b, c, x[i+ 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i+12], 6 , 1700485571); + d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i+10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i+ 8], 6 , 1873313359); + d = md5_ii(d, a, b, c, x[i+15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i+13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070); + d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i+ 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551); + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + return Array(a, b, c, d); + +} + +/* + * These functions implement the four basic operations the algorithm uses. + */ +function md5_cmn(q, a, b, x, s, t) +{ + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b); +} +function md5_ff(a, b, c, d, x, s, t) +{ + return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t); +} +function md5_gg(a, b, c, d, x, s, t) +{ + return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t); +} +function md5_hh(a, b, c, d, x, s, t) +{ + return md5_cmn(b ^ c ^ d, a, b, x, s, t); +} +function md5_ii(a, b, c, d, x, s, t) +{ + return md5_cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +/* + * Calculate the HMAC-MD5, of a key and some data + */ +function core_hmac_md5(key, data) +{ + var bkey = str2binl(key); + if(bkey.length > 16) bkey = core_md5(bkey, key.length * chrsz); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_md5(ipad.concat(str2binl(data)), 512 + data.length * chrsz); + return core_md5(opad.concat(hash), 512 + 128); +} + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +function safe_add(x, y) +{ + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); +} + +/* + * Bitwise rotate a 32-bit number to the left. + */ +function bit_rol(num, cnt) +{ + return (num << cnt) | (num >>> (32 - cnt)); +} + +/* + * Convert a string to an array of little-endian words + * If chrsz is ASCII, characters >255 have their hi-byte silently ignored. + */ +function str2binl(str) +{ + var bin = Array(); + var mask = (1 << chrsz) - 1; + for(var i = 0; i < str.length * chrsz; i += chrsz) + bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (i%32); + return bin; +} + +/* + * Convert an array of little-endian words to a string + */ +function binl2str(bin) +{ + var str = ""; + var mask = (1 << chrsz) - 1; + for(var i = 0; i < bin.length * 32; i += chrsz) + str += String.fromCharCode((bin[i>>5] >>> (i % 32)) & mask); + return str; +} + +/* + * Convert an array of little-endian words to a hex string. + */ +function binl2hex(binarray) +{ + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i++) + { + str += hex_tab.charAt((binarray[i>>2] >> ((i%4)*8+4)) & 0xF) + + hex_tab.charAt((binarray[i>>2] >> ((i%4)*8 )) & 0xF); + } + return str; +} + +/* + * Convert an array of little-endian words to a base-64 string + */ +function binl2b64(binarray) +{ + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + for(var i = 0; i < binarray.length * 4; i += 3) + { + var triplet = (((binarray[i >> 2] >> 8 * ( i %4)) & 0xFF) << 16) + | (((binarray[i+1 >> 2] >> 8 * ((i+1)%4)) & 0xFF) << 8 ) + | ((binarray[i+2 >> 2] >> 8 * ((i+2)%4)) & 0xFF); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > binarray.length * 32) str += b64pad; + else str += tab.charAt((triplet >> 6*(3-j)) & 0x3F); + } + } + return str; +} diff --git a/src/javascript/opensrf_app_session.js b/src/javascript/opensrf_app_session.js new file mode 100644 index 0000000..892a958 --- /dev/null +++ b/src/javascript/opensrf_app_session.js @@ -0,0 +1,509 @@ +/** @file oils_app_session.js + * @brief AppRequest and AppSession. + * The AppSession class does most of the communication work and the AppRequest + * contains the top level client API. + */ + +/** The default wait time when a client calls recv. It + * may be overrided by passing in a value to the recv method + */ +AppRequest.DEF_RECV_TIMEOUT = 10000; + +/** Provide a pre-built AppSession object and the payload of the REQUEST + * message you wish to send + */ +function AppRequest( session, payload ) { + + /** Our controling session */ + this.session = session; + + /** This requests message thread trace */ + this.thread_trace = null; + + /** This request REQUEST payload */ + this.payload = payload; + + /** True if this request has completed is request cycle */ + this.is_complete = false; + + /** Stores responses received from requests */ + this.recv_queue = new Array(); +} + +/** returns true if this AppRequest has completed its request cycle. */ +AppRequest.prototype.complete = function() { + if( this.is_complete ) { return true; } + this.session.queue_wait(0); + return this.is_complete; +} + +/** When called on an AppRequest object, its status will be + * set to complete regardless of its previous status + */ +AppRequest.prototype.set_complete = function() { + this.is_complete = true; +} + +/** Returns the current thread trace */ +AppRequest.prototype.get_thread_trace = function() { + return this.thread_trace; +} + +/** Pushes some payload onto the recv queue */ +AppRequest.prototype.push_queue = function( payload ) { + this.recv_queue.push( payload ); +} + +/** Returns the current payload of this request */ +AppRequest.prototype.get_payload = function() { + return this.payload; +} + +/** Removes this request from the our AppSession's request bucket + * Call this method when you are finished with a particular request + */ +AppRequest.prototype.finish = function() { + this.session.remove_request( this ); +} + + +/** Retrieves the current thread trace from the associated AppSession object, + * increments that session's thread trace, sets this AppRequest's thread trace + * to the new value. The request is then sent. + */ +AppRequest.prototype.make_request = function() { + var tt = this.session.get_thread_trace(); + this.session.set_thread_trace( ++tt ); + this.thread_trace = tt; + this.session.add_request( this ); + this.session.send( oilsMessage.REQUEST, tt, this.payload ); +} + +/** Checks the receive queue for message payloads. If any are found, the first + * is returned. Otherwise, this method will wait at most timeout seconds for + * a message to appear in the receive queue. Once it arrives it is returned. + * If no messages arrive in the timeout provided, null is returned. + + * NOTE: timeout is in * milliseconds * + */ + +AppRequest.prototype.recv = function( /*int*/ timeout ) { + + + if( this.recv_queue.length > 0 ) { + return this.recv_queue.shift(); + } + + //if( this.complete ) { return null; } + + if( timeout == null ) { + timeout = AppRequest.DEF_RECV_TIMEOUT; + } + + while( timeout > 0 ) { + + var start = new Date().getTime(); + this.session.queue_wait( timeout ); + + if( this.recv_queue.length > 0 ) { + return this.recv_queue.shift(); + } + + // shortcircuit the call if we're already complete + if( this.complete() ) { return null; } + + new Logger().debug( "AppRequest looping in recv " + + this.get_thread_trace() + " with timeout " + timeout, Logger.DEBUG ); + + var end = new Date().getTime(); + timeout -= ( end - start ); + } + + return null; +} + +/** Resend this AppRequest's REQUEST message, useful for recovering + * from disconnects, etc. + */ +AppRequest.prototype.resend = function() { + + new Logger().debug( "Resending msg with thread trace: " + + this.get_thread_trace(), Logger.DEBUG ); + this.session.send( oilsMessage.REQUEST, this.get_thread_trace(), this.payload ); +} + + + + +// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- +// AppSession code +// ----------------------------------------------------------------------------- + +/** Global cach of AppSession objects */ +AppSession.session_cache = new Array(); + +// ----------------------------------------------------------------------------- +// Session states +// ----------------------------------------------------------------------------- +/** True when we're attempting to connect to a remte service */ +AppSession.CONNECTING = 0; +/** True when we have successfully connected to a remote service */ +AppSession.CONNECTED = 1; +/** True when we have been disconnected from a remote service */ +AppSession.DISCONNECTED = 2; +/** The current default method protocol */ +AppSession.PROTOCOL = 1; + +/** Our connection with the outside world */ +AppSession.transport_handle = null; + + +/** Returns the session with the given session id */ +AppSession.find_session = function(session_id) { + return AppSession.session_cache[session_id]; +} + +/** Adds the given session to the global cache */ +AppSession.push_session = function(session) { + AppSession.session_cache[session.get_session_id()] = session; +} + +/** Deletes the session with the given session id from the global cache */ +AppSession.delete_session = function(session_id) { + AppSession.session_cache[session_id] = null; +} + +/** Builds a new session. + * @param remote_service The remote service we want to make REQUEST's of + */ +function AppSession( username, password, remote_service ) { + + + /** Our logger object */ + this.logger = new Logger(); + + random_num = Math.random() + ""; + random_num.replace( '.', '' ); + + /** Our session id */ + this.session_id = new Date().getTime() + "" + random_num; + + this.auth = new userAuth( username, password ); + + /** Our AppRequest queue */ + this.request_queue = new Array(); + + /** Our connectivity state */ + this.state = AppSession.DISCONNECTED; + + var config = new Config(); + + /** The remote ID of the service we are communicating with as retrieved + * from the config file + */ + this.orig_remote_id = config.get_value( "remote_service/" + remote_service ); + if( ! this.orig_remote_id ) { + throw new oils_ex_config( "No remote service id for: " + remote_service ); + } + + /** The current remote ID of the service we are communicating with */ + this.remote_id = this.orig_remote_id; + + /** Our current request threadtrace, which is incremented with each + * newly sent AppRequest */ + this.thread_trace = 0; + + /** Our queue of AppRequest objects */ + this.req_queue = new Array(); + + /** Our queue of AppRequests that are awaiting a resend of their payload */ + this.resend_queue = new Array(); + + // Build the transport_handle if if doesn't already exist + if( AppSession.transport_handle == null ) { + this.build_transport(); + } + + AppSession.push_session( this ); + +} + +/** The transport implementation is loaded from the config file and magically + * eval'ed into an object. All connection settings come from the client + * config. + * * This should only be called by the AppSession constructor and only when + * the transport_handle is null. + */ +AppSession.prototype.build_transport = function() { + + var config = new Config(); + var transport_impl = config.get_value( "transport/transport_impl" ); + if( ! transport_impl ) { + throw new oils_ex_config( "No transport implementation defined in config file" ); + } + + var username = config.get_value( "transport/username" ); + var password = config.get_value( "transport/password" ); + var this_host = config.get_value( "system/hostname" ); + var resource = this_host + "_" + new Date().getTime(); + var server = config.get_value( "transport/primary" ); + var port = config.get_value( "transport/port" ); + var tim = config.get_value( "transport/connect_timeout" ); + var timeout = tim * 1000; + + var eval_string = + "AppSession.transport_handle = new " + transport_impl + "( username, password, resource );"; + + eval( eval_string ); + + if( AppSession.transport_handle == null ) { + throw new oils_ex_config( "Transport implementation defined in config file is not valid" ); + } + + if( !AppSession.transport_handle.connect( server, port, timeout ) ) { + throw new oils_ex_transport( "Connection attempt to remote service timed out" ); + } + + if( ! AppSession.transport_handle.connected() ) { + throw new oils_ex_transport( "AppSession is unable to connect to the remote service" ); + } +} + + +/** Adds the given AppRequest object to this AppSession's request queue */ +AppSession.prototype.add_request = function( req_obj ) { + new Logger().debug( "Adding AppRequest: " + req_obj.get_thread_trace(), Logger.DEBUG ); + this.req_queue[req_obj.get_thread_trace()] = req_obj; +} + +/** Removes the AppRequest object from this AppSession's request queue */ +AppSession.prototype.remove_request = function( req_obj ) { + this.req_queue[req_obj.get_thread_trace()] = null; +} + +/** Returns the AppRequest with the given thread_trace */ +AppSession.prototype.get_request = function( thread_trace ) { + return this.req_queue[thread_trace]; +} + + +/** Returns this AppSession's session id */ +AppSession.prototype.get_session_id = function() { + return this.session_id; +} + +/** Resets the remote_id for the transport to the original remote_id retrieved + * from the config file + */ +AppSession.prototype.reset_remote = function() { + this.remote_id = this.orig_remote_id; +} + +/** Returns the current message thread trace */ +AppSession.prototype.get_thread_trace = function() { + return this.thread_trace; +} + +/** Sets the current thread trace */ +AppSession.prototype.set_thread_trace = function( tt ) { + this.thread_trace = tt; +} + +/** Returns the state that this session is in (i.e. CONNECTED) */ +AppSession.prototype.get_state = function() { + return this.state; +} + +/** Sets the session state. The state should be one of the predefined + * session AppSession session states. + */ +AppSession.prototype.set_state = function(state) { + this.state = state; +} + +/** Returns the current remote_id for this session */ +AppSession.prototype.get_remote_id = function() { + return this.remote_id; +} + +/** Sets the current remote_id for this session */ +AppSession.prototype.set_remote_id = function( id ) { + this.remote_id = id; +} + +/** Pushes an AppRequest object onto the resend queue */ +AppSession.prototype.push_resend = function( app_request ) { + this.resend_queue.push( app_request ); +} + +/** Destroys the current session. This will disconnect from the + * remote service, remove all AppRequests from the request + * queue, and finally remove this session from the global cache + */ +AppSession.prototype.destroy = function() { + + new Logger().debug( "Destroying AppSession: " + this.get_session_id(), Logger.DEBUG ); + + // disconnect from the remote service + if( this.get_state() != AppSession.DISCONNECTED ) { + this.disconnect(); + } + // remove us from the global cache + AppSession.delete_session( this.get_session_id() ); + + // Remove app request references + for( var index in this.req_queue ) { + this.req_queue[index] = null; + } +} + +/** This forces a resend of all AppRequests that are currently + * in the resend queue + */ +AppSession.prototype.flush_resend = function() { + + if( this.resend_queue.length > 0 ) { + new Logger().debug( "Resending " + + this.resend_queue.length + " messages", Logger.INFO ); + } + + var req = this.resend_queue.shift(); + + while( req != null ) { + req.resend(); + req = this.resend_queue.shift(); + } +} + +/** This method tracks down the AppRequest with the given thread_trace and + * pushes the payload into that AppRequest's recieve queue. + */ +AppSession.prototype.push_queue = function( dom_payload, thread_trace ) { + + var req = this.get_request( thread_trace ); + if( ! req ) { + new Logger().debug( "No AppRequest exists for TT: " + thread_trace, Logger.ERROR ); + return; + } + req.push_queue( dom_payload ); +} + + +/** Connect to the remote service. The connect timeout is read from the config. + * This method returns null if the connection fails. It returns a reference + * to this AppSession object otherwise. + */ +AppSession.prototype.connect = function() { + + if( this.get_state() == AppSession.CONNECTED ) { + return this; + } + + var config = new Config(); + var rem = config.get_value( "transport/connect_timeout" ); + if( ! rem ) { + throw new oils_ex_config( "Unable to retreive timeout value from config" ); + } + + var remaining = rem * 1000; // milliseconds + + this.reset_remote(); + this.set_state( AppSession.CONNECTING ); + this.send( oilsMessage.CONNECT, 0, "" ); + + new Logger().debug( "CONNECTING with timeout: " + remaining, Logger.DEBUG ); + + while( this.get_state() != AppSession.CONNECTED && remaining > 0 ) { + + var starttime = new Date().getTime(); + this.queue_wait( remaining ); + var endtime = new Date().getTime(); + remaining -= (endtime - starttime); + } + + if( ! this.get_state() == AppSession.CONNECTED ) { + return null; + } + + return this; +} + +/** Disconnects from the remote service */ +AppSession.prototype.disconnect = function() { + + if( this.get_state() == AppSession.DISCONNECTED ) { + return; + } + + this.send( oilsMessage.DISCONNECT, this.get_thread_trace(), "" ); + this.set_state( AppSession.DISCONNECTED ); + this.reset_remote(); +} + + +/** Builds a new message with the given type and thread_trace. If the message + * is a REQUEST, then the payload is added as well. + * This method will verify that the session is in the CONNECTED state before + * sending any REQUEST's by attempting to do a connect. + * + * Note: msg_type and thread_trace must be provided. + */ +AppSession.prototype.send = function( msg_type, thread_trace, payload ) { + + if( msg_type == null || thread_trace == null ) { + throw new oils_ex_args( "Not enough arguments provided to AppSession.send method" ); + } + + // go ahead and make sure there's nothing new to process + this.queue_wait(0); + + var msg; + if( msg_type == oilsMessage.CONNECT ) { + msg = new oilsMessage( msg_type, AppSession.PROTOCOL, this.auth ); + } else { + msg = new oilsMessage( msg_type, AppSession.PROTOCOL ); + } + + msg.setThreadTrace( thread_trace ); + + if( msg_type == oilsMessage.REQUEST ) { + if( ! payload ) { + throw new oils_ex_args( "No payload provided for REQUEST message in AppSession.send" ); + } + msg.add( payload ); + } + + + // Make sure we're connected + if( (msg_type != oilsMessage.DISCONNECT) && (msg_type != oilsMessage.CONNECT) && + (this.get_state() != AppSession.CONNECTED) ) { + if( ! this.connect() ) { + throw new oils_ex_session( this.get_session_id() + " | Unable to connect to remote service after redirect" ); + } + } + + this.logger.debug( "AppSession sending tt: " + + thread_trace + " to " + this.get_remote_id() + + " " + msg_type , Logger.INFO ); + + AppSession.transport_handle.send( this.get_remote_id(), this.get_session_id(), msg.toString(true) ); + +} + + +/** Waits up to 'timeout' milliseconds for some data to arrive. + * Any data that arrives will be process according to its + * payload and message type. This method will return after + * any data has arrived. + * @param timeout How many milliseconds to wait or data to arrive + */ +AppSession.prototype.queue_wait = function( timeout ) { + this.flush_resend(); // necessary if running parallel sessions + new Logger().debug( "In queue_wait " + timeout, Logger.DEBUG ); + var tran_msg = AppSession.transport_handle.process_msg( timeout ); + this.flush_resend(); +} + + + diff --git a/src/javascript/opensrf_config.js b/src/javascript/opensrf_config.js new file mode 100644 index 0000000..1beef71 --- /dev/null +++ b/src/javascript/opensrf_config.js @@ -0,0 +1,281 @@ +/** @file oils_config.js + * Config code and Logger code + * The config module is simple. It parses an xml config file and + * the values from the file are accessed via XPATH queries. + */ + +/** Searches up from Mozilla's chrome directory for the client + * config file and returns the file object + */ +function get_config_file() { + + var dirService = Components.classes["@mozilla.org/file/directory_service;1"]. + getService( Components.interfaces.nsIProperties ); + + chromeDir = dirService.get( "AChrom", Components.interfaces.nsIFile ); + chromeDir.append("evergreen"); + chromeDir.append("content"); + chromeDir.append("conf"); + chromeDir.append("client_config.xml"); + return chromeDir; + +} + + +/** Constructor. You may provide an optional path to the config file. **/ +function Config( config_file ) { + + if( Config.config != null ) { return Config.config; } + + config_file = get_config_file(); + + // ------------------------------------------------------------------ + // Grab the data from the config file + // ------------------------------------------------------------------ + var data = ""; + var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + + var sstream = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.nsIScriptableInputStream); + + fstream.init(config_file, 1, 0, false); + sstream.init(fstream); + data += sstream.read(-1); + + sstream.close(); + fstream.close(); + + + + var DOMParser = new Components.Constructor( + "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser" ); + + this.config_doc = new DOMParser().parseFromString( data, "text/xml" ); + + Config.config = this; + +} + +/** Returns the value stored in the config file found with the + * given xpath expression + * E.g. config.get_value( "/oils_config/dirs/log_dir" ); + * Note, the 'oils_config' node is the base of the xpath expression, + * so something like this will also work: + * config.get_value( "dirs/log_dir" ); + */ +Config.prototype.get_value = function( xpath_query ) { + + var evaluator = Components.classes["@mozilla.org/dom/xpath-evaluator;1"]. + createInstance( Components.interfaces.nsIDOMXPathEvaluator ); + + var xpath_obj = evaluator.evaluate( xpath_query, this.config_doc.documentElement, null, 0, null ); + if( ! xpath_obj ) { return null; } + + var node = xpath_obj.iterateNext(); + if( node == null ) { + throw new oils_ex_config( "No config option matching " + xpath_query ); + } + + return node.firstChild.nodeValue; +} + + + + + + +// ------------------------------------------------------------------ +// Logger code. +// ------------------------------------------------------------------ +/** The global logging object */ +Logger.logger = null; +/** No logging level */ +Logger.NONE = 0; +/** Error log level */ +Logger.ERROR = 1; +/** Info log level */ +Logger.INFO = 2; +/* Debug log level */ +Logger.DEBUG = 3; + +/** There exists a single logger object that all components share. + * Calling var logger = new Logger() will return the same one + * with each call. This is so we only need one file handle for + * each type of log file. + */ +function Logger() { + + if( Logger.logger != null ) { return Logger.logger } + + var config = new Config(); + this.log_level = config.get_value( "system/log_level" ); + + this.stdout_log = config.get_value( "system/stdout_log" ); + + if( ! this.stdout_log || this.stdout_log < 0 || this.stdout_log > 2 ) { + throw new oils_ex_config( "stdout_log setting is invalid: " + this.stdout_log + + ". Should be 0, 1, or 2." ); + } + + // ------------------------------------------------------------------ + // Load up all of the log files + // ------------------------------------------------------------------ + var transport_file = config.get_value( "logs/transport" ); + if( transport_file == null ) { + throw new oils_ex_config( "Unable to load transport log file: 'logs/transport'" ); + } + + var debug_file = config.get_value( "logs/debug" ); + if( debug_file == null ) { + throw new oils_ex_config( "Unable to load debug log file: 'logs/debug'" ); + } + + var error_file = config.get_value( "logs/error" ); + if( error_file == null ) { + throw new oils_ex_config( "Unable to load debug log file: 'logs/error'" ); + } + + + // ------------------------------------------------------------------ + // Build the file objects + // ------------------------------------------------------------------ + var transport_file_obj = Logger.get_log_file( transport_file ); + + var debug_file_obj = Logger.get_log_file( debug_file ); + + var error_file_obj = Logger.get_log_file( error_file ); + + + // ------------------------------------------------------------------ + // Build all of the file stream objects + // ------------------------------------------------------------------ + this.transport_stream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + + this.debug_stream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + + this.error_stream = Components.classes["@mozilla.org/network/file-output-stream;1"] + .createInstance(Components.interfaces.nsIFileOutputStream); + + // ------------------------------------------------------------------ + // Init all of the streams + // use 0x02 | 0x10 to open file for appending. + // ------------------------------------------------------------------ + this.transport_stream.init(transport_file_obj, 0x02 | 0x10 | 0x08, 0664, 0 ); + this.debug_stream.init( debug_file_obj, 0x02 | 0x10 | 0x08, 0664, 0 ); + this.error_stream.init( error_file_obj, 0x02 | 0x10 | 0x08, 0664, 0 ); + + Logger.logger = this; + +} + +/** Internal. Returns a XPCOM nsIFile object for the log file we're interested in */ +Logger.get_log_file = function( log_name ) { + + var dirService = Components.classes["@mozilla.org/file/directory_service;1"]. + getService( Components.interfaces.nsIProperties ); + + logFile = dirService.get( "AChrom", Components.interfaces.nsIFile ); + logFile.append("evergreen"); + logFile.append("content"); + logFile.append("log"); + logFile.append( log_name ); + + if( ! logFile.exists() ) { + logFile.create( 0, 0640 ); + } + + return logFile; +} + + + +/** Internal. Builds a log message complete with data, etc. */ +Logger.prototype.build_string = function( message, level ) { + + if( ! (message && level) ) { return null; } + + var lev = "INFO"; + if( level == Logger.ERROR ) { lev = "ERROR"; } + if( level == Logger.DEBUG ) { lev = "DEBUG"; } + + var date = new Date(); + var year = date.getYear(); + year += 1900; + + var month = ""+date.getMonth(); + if(month.length==1) {month="0"+month;} + var day = ""+date.getDate(); + if(day.length==1) {day="0"+day;} + var hour = ""+date.getHours(); + if(hour.length== 1){hour="0"+hour;} + var min = ""+date.getMinutes(); + if(min.length==1){min="0"+min;} + var sec = ""+date.getSeconds(); + if(sec.length==1){sec="0"+sec;} + var mil = ""+date.getMilliseconds(); + if(mil.length==1){sec="0"+sec;} + + var date_string = year + "-" + month + "-" + day + " " + + hour + ":" + min + ":" + sec + "." + mil; + + var str_array = message.split('\n'); + var ret_array = new Array(); + for( var i in str_array ) { + ret_str = "[" + date_string + "] " + lev + " " + str_array[i] + "\n"; + ret_array.push( ret_str ); + } + + var line = "-------------------------\n"; + ret_array.unshift( line ); + + return ret_array; +} + +/** Internal. Does the actual writing */ +Logger.prototype._log = function( data, stream, level ) { + + if( ! data ) { return; } + if( ! stream ) { + throw oils_ex_logger( "No file stream open for log message: " + data ); + } + if( ! level ) { level = Logger.DEBUG; } + + if( level > this.log_level ) { return; } + var str_array = this.build_string( data, level ); + if( ! str_array ) { return; } + + for( var i in str_array ) { + if( this.stdout_log > 0 ) { dump( str_array[i] ); } + if( this.stdout_log < 2 ) { stream.write( str_array[i], str_array[i].length ); } + } + + // write errors to the error log if they were destined for anywhere else + if( level == Logger.ERROR && stream != this.error_stream ) { + for( var i in str_array ) { + if( this.stdout_log > 0 ) { dump( str_array[i] ); } + if( this.stdout_log < 2 ) { this.error_stream.write( str_array[i], str_array[i].length ); } + } + } +} + + + +/** Writes the message to the error log */ +Logger.prototype.error = function( message, level ) { + this._log( message, this.error_stream, level ); +} + + +/** Writes to the debug log */ +Logger.prototype.debug = function( message, level ) { + this._log( message, this.debug_stream, level ); +} + + +/** Writes to the transport log */ +Logger.prototype.transport = function( message, level ) { + this._log( message, this.transport_stream, level ); +} diff --git a/src/javascript/opensrf_dom_element.js b/src/javascript/opensrf_dom_element.js new file mode 100644 index 0000000..2e419e3 --- /dev/null +++ b/src/javascript/opensrf_dom_element.js @@ -0,0 +1,263 @@ +/** @file oils_dom_element.js + * ----------------------------------------------------------------------------- + * This file holds all of the core OILS DOM elements. + * ----------------------------------------------------------------------------- + + * ----------------------------------------------------------------------------- + * Make sure you load md5.js into your script/html/etc. before loading this + * file + * ----------------------------------------------------------------------------- + * + * ----------------------------------------------------------------------------- + * 1. Use these if you are running via xpcshell, but not if you are running + * directly from mozilla. + * 2. BTW. XMLSerializer doesn't like being run outside of mozilla, so + * toString() fails. + * + * var DOMParser = new Components.Constructor( + * "@mozilla.org/xmlextras/domparser;1", "nsIDOMParser" ); + * + * var XMLSerializer = new Components.Constructor( + * "@mozilla.org/xmlextras/xmlserializer;1", "nsIDOMSerializer" ); + * ----------------------------------------------------------------------------- + * + * + */ + +/** + * ----------------------------------------------------------------------------- + * DOM - Top level generic class + * ----------------------------------------------------------------------------- + */ + +function DOM() { + this.doc = null; + this.root = null; + this.element = null; +} + +/** Returns a string representation of the XML doc. If full_bool + * is true, it will return the entire doc and not just the relevant + * portions (i.e. our object's element). + */ +DOM.prototype.toString = function(full_bool) { + if( full_bool ) { + return new XMLSerializer().serializeToString(this.doc); + } else { + return new XMLSerializer().serializeToString(this.element); + } +} + +/** Initializes a document and sets this.doc, this.root and this.element */ +DOM.prototype.init = function( elementName ) { + + this.doc = new DOMParser().parseFromString( + "", "text/xml" ); + this.root = this.doc.documentElement; + this.element = this.doc.createElement( "oils:" + elementName ); + return this.element; +} + +/** Good for creating objects from raw xml. This builds a new doc and inserts + * the node provided. So you can build a dummy object, then call replaceNode + * with the new xml to build an object from XML on the fly. + */ +DOM.prototype._replaceNode = function( name, new_element ) { + + var node = this.doc.importNode( new_element, true ); + + if( node.nodeName != "oils:" + name ) { + throw new oils_ex_dom( "Invalid Tag to " + name + "::replaceNode()" ); + } + + this.doc = new DOMParser().parseFromString( + "", "text/xml" ); + this.root = this.doc.documentElement; + + this.root.appendChild( node ); + this.element = this.root.firstChild; + return this; +} + + + +// ----------------------------------------------------------------------------- +// domainObjectAttr +// ----------------------------------------------------------------------------- + +domainObjectAttr.prototype = new DOM(); +domainObjectAttr.prototype.constructor = domainObjectAttr; +domainObjectAttr.baseClass = DOM.prototype.constructor; + +/** A domainObjectAttr is a single XML node with 'name' and 'value' attributes */ +function domainObjectAttr( name, value ) { + + var node = this.init( "domainObjectAttr" ); + if( ! name && value ) { return; } + node.setAttribute( "name", name ); + node.setAttribute( "value", value ); + this.root.appendChild( node ); +} + + +/** Returns the attribute name */ +domainObjectAttr.prototype.getName = function() { + return this.element.getAttribute("name"); +} + +/** Returns the attribut value. */ +domainObjectAttr.prototype. getValue = function() { + return this.element.getAttribute("value"); +} + +/** Sets the attribute value. */ +domainObjectAttr.prototype. setValue = function( value ) { + return this.element.setAttribute("value", value ); +} + +/** Pass in the this.element component of a domainObjectAttr and this will + * replace the old element with the new one + */ +domainObjectAttr.prototype.replaceNode = function( domainObjectAttr_element ) { + return this._replaceNode( "domainObjectAttr", domainObjectAttr_element ); +} + +// ----------------------------------------------------------------------------- +// userAuth +// ----------------------------------------------------------------------------- + + +userAuth.prototype = new DOM(); +userAuth.prototype.constructor = userAuth; +userAuth.baseClass = DOM.prototype.constructor; + +function userAuth( username, secret ) { + + var node = this.init( "userAuth" ); + if( !( username && secret) ) { return; } + node.setAttribute( "username", username ); + + //There is no way to specify the hash seed with the + //md5 utility provided + var hash = hex_md5( secret ); + node.setAttribute( "secret", hash ); + node.setAttribute( "hashseed", "" ); + + this.root.appendChild( node ); +} + +userAuth.prototype.getUsername = function() { + return this.element.getAttribute( "username" ); +} + +userAuth.prototype.getSecret = function() { + return this.element.getAttribute( "secret" ); +} + +userAuth.prototype.getHashseed = function() { + return this.element.getAttribute( "hashseed" ); +} + +userAuth.prototype.replaceNode = function( userAuth_element ) { + return this._replaceNode( "userAuth", userAuth_element ); +} + + +// ----------------------------------------------------------------------------- +// domainObject +// ----------------------------------------------------------------------------- + +domainObject.prototype = new DOM(); +domainObject.baseClass = DOM.prototype.constructor; +domainObject.prototype.constructor = domainObject; + +/** Holds the XML for a DomainObject (see oils_domain_object.js or the DomainObject class) */ +function domainObject( name ) { + this._init_domainObject( name ); +} + +/** Initializes the domainObject XML node */ +domainObject.prototype._init_domainObject = function( name ) { + + var node = this.init( "domainObject" ); + + if( ! name ) { return; } + node.setAttribute( "name", name ); + this.root.appendChild( node ); + +} + +/** Pass in any DOM Element or DomainObject and this method will as the + * new object as the next child of the root (domainObject) node + */ +domainObject.prototype.add = function( new_DOM ) { + + this.element.appendChild( + new_DOM.element.cloneNode( true ) ); +} + +/** Removes the original domainObject XML node and replaces it with the + * one provided. This is useful for building new domainObject's from + * raw xml. Just create a new one, then call replaceNode with the new XML. + */ +domainObject.prototype.replaceNode = function( domainObject_element ) { + return this._replaceNode( "domainObject", domainObject_element ); +} + + + +// ----------------------------------------------------------------------------- +// Param +// ----------------------------------------------------------------------------- + + +param.prototype = new DOM(); +param.baseClass = DOM.prototype.constructor; +param.prototype.constructor = param; + +function param( value ) { + var node = this.init( "param" ); + node.appendChild( this.doc.createTextNode( value ) ); + this.root.appendChild( node ); +} + +param.prototype.getValue = function() { + return this.element.textContent; +} + +param.prototype.replaceNode = function( param_element ) { + return this._replaceNode( "param", param_element ); +} + + +// ----------------------------------------------------------------------------- +// domainObjectCollection +// ----------------------------------------------------------------------------- + +domainObjectCollection.prototype = new DOM(); +domainObjectCollection.prototype.constructor = + domainObjectCollection; +domainObjectCollection.baseClass = DOM.prototype.constructor; + +function domainObjectCollection( name ) { + var node = this.init( "domainObjectCollection" ); + this.root.appendChild( node ); + if(name) { this._initCollection( name ); } +} + +domainObjectCollection.prototype._initCollection = function( name ) { + if( name ) { + this.element.setAttribute( "name", name ); + } +} + +domainObjectCollection.prototype.add = function( new_domainObject ) { + this.element.appendChild( + new_domainObject.element.cloneNode( true ) ); +} + +domainObjectCollection.prototype.replaceNode = function( new_node ) { + return this._replaceNode( "domainObjectCollection", new_node ); +} + + diff --git a/src/javascript/opensrf_domain_object.js b/src/javascript/opensrf_domain_object.js new file mode 100644 index 0000000..edbd247 --- /dev/null +++ b/src/javascript/opensrf_domain_object.js @@ -0,0 +1,609 @@ +// ----------------------------------------------------------------------------- +// This houses all of the domain object code. +// ----------------------------------------------------------------------------- + + + + +// ----------------------------------------------------------------------------- +// DomainObject + +DomainObject.prototype = new domainObject(); +DomainObject.prototype.constructor = DomainObject; +DomainObject.prototype.baseClass = domainObject.prototype.constructor; + +/** Top level DomainObject class. This most provides convience methods + * and a shared superclass + */ +function DomainObject( name ) { + if( name ) { this._init_domainObject( name ); } +} + +/** Returns the actual element of the given domainObjectAttr. */ +DomainObject.prototype._findAttr = function ( name ) { + + var nodes = this.element.childNodes; + + if( ! nodes || nodes.length < 1 ) { + throw new oils_ex_dom( "Invalid xml object in _findAttr: " + this.toString() ); + } + + var i=0; + var node = nodes.item(i); + + while( node != null ) { + + if( node.nodeName == "oils:domainObjectAttr" && + node.getAttribute("name") == name ) { + return node; + } + + node = nodes.item(++i); + } + + return null; +} + + + + +/** Returns the value stored in the given attribute */ +DomainObject.prototype.getAttr = function ( name ) { + + var node = this._findAttr( name ); + if( node ) { return node.getAttribute( "value" ); } + else { + throw new oils_ex_dom( "getAttr(); Getting nonexistent attribute: " + name ); + } +} + +/** Updates the value held by the given attribute */ +DomainObject.prototype.setAttr = function ( name, value ) { + + var node = this._findAttr( name ); + if( node ) { + node.setAttribute( "value", value ); + } else { + throw new oils_ex_dom( "setAttr(); Setting nonexistent attribute: " + name ); + } +} + +/** This takes a raw DOM Element node and creates the DomainObject that the node + * embodies and returns the object. All new DomainObjects should be added to + * this list if they require this type of dynamic functionality. + * NOTE Much of this will be deprecated as move to a more JSON-centric wire protocol + */ +DomainObject.newFromNode = function( node ) { + + switch( node.getAttribute("name") ) { + + case "oilsMethod": + return new oilsMethod().replaceNode( node ); + + case "oilsMessage": + return new oilsMessage().replaceNode( node ); + + case "oilsResponse": + return new oilsResponse().replaceNode( node ); + + case "oilsResult": + return new oilsResult().replaceNode( node ); + + case "oilsConnectStatus": + return new oilsConnectStatus().replaceNode( node ); + + case "oilsException": + return new oilsException().replaceNode( node ); + + case "oilsMethodException": + return new oilsMethodException().replaceNode( node ); + + case "oilsScalar": + return new oilsScalar().replaceNode( node ); + + case "oilsPair": + return new oilsPair().replaceNode( node ); + + case "oilsArray": + return new oilsArray().replaceNode( node ); + + case "oilsHash": + return new oilsHash().replaceNode( node ); + + + } + +} + + + +// ----------------------------------------------------------------------------- +// oilsMethod + +oilsMethod.prototype = new DomainObject(); +oilsMethod.prototype.constructor = oilsMethod; +oilsMethod.prototype.baseClass = DomainObject.prototype.constructor; + +/** + * oilsMethod Constructor + * + * @param method_name The name of the method your are sending + * to the remote server + * @param params_array A Javascript array of the params (any + * Javascript data type) + */ + +function oilsMethod( method_name, params_array ) { + this._init_domainObject( "oilsMethod" ); + this.add( new domainObjectAttr( "method", method_name ) ); + + if( params_array ) { + var params = this.doc.createElement( "oils:params" ); + this.element.appendChild( params ); + params.appendChild( this.doc.createTextNode( + js2JSON( params_array ) ) ); + } +} + + +/** Locates the params node of this method */ +oilsMethod.prototype._getParamsNode = function() { + + var nodes = this.element.childNodes; + if( ! nodes || nodes.length < 1 ) { return null; } + var x=0; + var node = nodes.item(x); + while( node != null ) { + if( node.nodeName == "oils:params" ) { + return node; + } + node = nodes.item(++x); + } + + return null; +} + + +/** Returns an array of param objects */ +oilsMethod.prototype.getParams = function() { + var node = this._getParamsNode(); + if(node != null ) { return JSON2js( node->textContent ); } +} + + + +// ----------------------------------------------------------------------------- +// oilsMessage + +// ----------------------------------------------------------------------------- +// oilsMessage message types +// ----------------------------------------------------------------------------- +/** CONNECT Message type */ +oilsMessage.CONNECT = 'CONNECT'; +/** DISCONNECT Message type */ +oilsMessage.DISCONNECT = 'DISCONNECT'; +/** STATUS Message type */ +oilsMessage.STATUS = 'STATUS'; +/** REQUEST Message type */ +oilsMessage.REQUEST = 'REQUEST'; +/** RESULT Message type */ +oilsMessage.RESULT = 'RESULT'; + + +oilsMessage.prototype = new DomainObject(); +oilsMessage.prototype.constructor = oilsMessage; +oilsMessage.prototype.baseClass = DomainObject.prototype.constructor; + +/** Core XML object for message passing */ +function oilsMessage( type, protocol, user_auth ) { + + if( !( type && protocol) ) { + type = oilsMessage.CONNECT; + protocol = "1"; + } + + if( ! ( type == oilsMessage.CONNECT || + type == oilsMessage.DISCONNECT || + type == oilsMessage.STATUS || + type == oilsMessage.REQUEST || + type == oilsMessage.RESULT ) ) { + throw new oils_ex_message( "Attempt to create oilsMessage with incorrect type: " + type ); + } + + this._init_domainObject( "oilsMessage" ); + + this.add( new domainObjectAttr( "type", type ) ); + this.add( new domainObjectAttr( "threadTrace", 0 ) ); + this.add( new domainObjectAttr( "protocol", protocol ) ); + + if( user_auth ) { this.add( user_auth ); } + +} + +/** Builds a new oilsMessage from raw xml. + * Checks are taken to make sure the xml is supposed to be an oilsMessage. + * If not, an oils_ex_dom exception is throw. + */ +oilsMessage.newFromXML = function( xml ) { + + try { + + if( ! xml || xml == "" ) { throw 1; } + + var doc = new DOMParser().parseFromString( xml, "text/xml" ); + if( ! doc ) { throw 1; } + + var root = doc.documentElement; + if( ! root ) { throw 1; } + + // ----------------------------------------------------------------------------- + // There are two options here. One is that we were provided the full message + // xml (i.e. contains the tag. The other option is that we were + // provided just the message xml portion (top level element is the + // + // ----------------------------------------------------------------------------- + + var element; + if( root.nodeName == "oils:root" ) { + + element = root.firstChild; + if( ! element ) { throw 1; } + + } else { + + if( root.nodeName == "oils:domainObject" ) { + element = root; + + } else { throw 1; } + + } + + + if( element.nodeName != "oils:domainObject" ) { throw 1; } + if( element.getAttribute( "name" ) != "oilsMessage" ) { throw 1; } + + return new oilsMessage().replaceNode( element ); + + } catch( E ) { + + if( E && E.message ) { + throw new oils_ex_dom( "Bogus XML for creating oilsMessage: " + E.message + "\n" + xml ); + + } else { + throw new oils_ex_dom( "Bogus XML for creating oilsMessage:\n" + xml ); + } + } + + return msg; +} + + + +/** Adds a copy of the given DomainObject to the message */ +oilsMessage.prototype.addPayload = function( new_DomainObject ) { + this.add( new_DomainObject ); +} + + +/** Returns the top level DomainObject contained in this message + * + * Note: The object retuturned will be the actual object, not just a + * generic DomainObject - e.g. oilsException. + */ +oilsMessage.prototype.getPayload = function() { + + var nodes = this.element.childNodes; + var x=0; + var node = nodes.item(0); + + while( node != null ) { + + if( node.nodeName == "oils:domainObject" ) { + + new Logger().debug( "Building oilsMessage payload from\n" + + new XMLSerializer().serializeToString( node ), Logger.DEBUG ); + return DomainObject.newFromNode( node ); + } + node = nodes.item(++x); + } + + return null; +} + +oilsMessage.prototype.getUserAuth = function() { + + var nodes = this.element.getElementsByTagName( "oils:userAuth" ); + if( ! nodes ) { return null; } + + var auth_elem = nodes.item(0); // should only be one + if ( ! auth_elem ) { return null; } + + return new userAuth().replaceNode( auth_elem ); + +} + +/** Returns the type of the message */ +oilsMessage.prototype.getType = function() { + return this.getAttr( "type" ); +} + +/** Returns the message protocol */ +oilsMessage.prototype.getProtocol = function() { + return this.getAttr( "protocol" ); +} + +/** Returns the threadTrace */ +oilsMessage.prototype.getThreadTrace = function() { + return this.getAttr( "threadTrace" ); +} + +/** Sets the thread trace - MUST be an integer */ +oilsMessage.prototype.setThreadTrace = function( trace ) { + this.setAttr( "threadTrace", trace ); +} + +/** Increments the thread trace by 1 */ +oilsMessage.prototype.updateThreadTrace = function() { + var tt = this.getThreadTrace(); + return this.setThreadTrace( ++tt ); +} + + + + + +// ----------------------------------------------------------------------------- +// oilsResponse + + +// ----------------------------------------------------------------------------- +// Response codes +// ----------------------------------------------------------------------------- + +/** + * Below are the various response statuses + */ +oilsResponse.STATUS_CONTINUE = 100 + +oilsResponse.STATUS_OK = 200 +oilsResponse.STATUS_ACCEPTED = 202 +oilsResponse.STATUS_COMPLETE = 205 + +oilsResponse.STATUS_REDIRECTED = 307 + +oilsResponse.STATUS_BADREQUEST = 400 +oilsResponse.STATUS_UNAUTHORIZED = 401 +oilsResponse.STATUS_FORBIDDEN = 403 +oilsResponse.STATUS_NOTFOUND = 404 +oilsResponse.STATUS_NOTALLOWED = 405 +oilsResponse.STATUS_TIMEOUT = 408 +oilsResponse.STATUS_EXPFAILED = 417 +oilsResponse.STATUS_INTERNALSERVERERROR = 500 +oilsResponse.STATUS_NOTIMPLEMENTED = 501 +oilsResponse.STATUS_VERSIONNOTSUPPORTED = 505 + + +oilsResponse.prototype = new DomainObject(); +oilsResponse.prototype.constructor = oilsResponse; +oilsResponse.prototype.baseClass = DomainObject.prototype.constructor; + +/** Constructor. status is the text describing the message status and + * statusCode must correspond to one of the oilsResponse status code numbers + */ +function oilsResponse( name, status, statusCode ) { + if( name && status && statusCode ) { + this._initResponse( name, status, statusCode ); + } +} + +/** Initializes the reponse XML */ +oilsResponse.prototype._initResponse = function( name, status, statusCode ) { + + this._init_domainObject( name ); + this.add( new domainObjectAttr( "status", status )); + this.add( new domainObjectAttr( "statusCode", statusCode ) ); +} + + + +/** Returns the status of the response */ +oilsResponse.prototype.getStatus = function() { + return this.getAttr( "status" ); +} + +/** Returns the statusCode of the response */ +oilsResponse.prototype.getStatusCode = function() { + return this.getAttr("statusCode"); +} + + +oilsConnectStatus.prototype = new oilsResponse(); +oilsConnectStatus.prototype.constructor = oilsConnectStatus; +oilsConnectStatus.prototype.baseClass = oilsResponse.prototype.constructor; + +/** Constructor. These give us info on our connection attempts **/ +function oilsConnectStatus( status, statusCode ) { + this._initResponse( "oilsConnectStatus", status, statusCode ); +} + + +oilsResult.prototype = new oilsResponse(); +oilsResult.prototype.constructor = oilsResult; +oilsResult.prototype.baseClass = oilsResponse.prototype.constructor; + + +/** Constructor. These usually carry REQUEST responses */ +function oilsResult( status, statusCode ) { + if( status && statusCode ) { + this._initResponse( "oilsResult", status, statusCode ); + } +} + +/** Returns the top level DomainObject within this result message. + * Note: The object retuturned will be the actual object, not just a + * generic DomainObject - e.g. oilsException. + */ +oilsResult.prototype.getContent = function() { + + var nodes = this.element.childNodes; + if( ! nodes || nodes.length < 1 ) { + throw new oils_ex_dom("Content node for oilsResult is invalid\n" + this.toString() ); + } + var x = 0; + var node = nodes.item(x); + while( node != null ) { + + if( node.nodeName == "oils:domainObject" || + node.nodeName == "oils:domainObjectCollection" ) { + + return DomainObject.newFromNode( node ); + } + node = nodes.item(++x); + } + + throw new oils_ex_dom("Content node for oilsResult is invalid\n" + this.toString() ); +} + + +oilsException.prototype = new oilsResponse(); +oilsException.prototype.constructor = oilsException; +oilsException.prototype.baseClass = oilsResponse.prototype.constructor; + +/** Top level exception */ +function oilsException( status, statusCode ) { + if( status && statusCode ) { + this._initResponse( "oilsException", status, statusCode ); + } +} + + +oilsMethodException.prototype = new oilsException(); +oilsMethodException.prototype.constructor = oilsMethodException; +oilsMethodException.prototype.baseClass = oilsException.prototype.constructor; + +/** Method exception */ +function oilsMethodException( status, statusCode ) { + if( status && statusCode ) { + this._initResponse( "oilsMethodException", status, statusCode ); + } +} + + +// ----------------------------------------------------------------------------- +// oilsScalar + + +oilsScalar.prototype = new DomainObject(); +oilsScalar.prototype.constructor = oilsScalar; +oilsScalar.prototype.baseClass = DomainObject.prototype.constructor; + +/** Contains a single JSON item as its text content */ +function oilsScalar( value ) { + this._init_domainObject( "oilsScalar" ); + this.element.appendChild( this.doc.createTextNode( value ) ); +} + +/** Returns the contained item as a Javascript object */ +oilsScalar.prototype.getValue = function() { + return JSON2js( this.element.textContent ); +} + + +// ----------------------------------------------------------------------------- +// oilsPair + + +oilsPair.prototype = new DomainObject() +oilsPair.prototype.constructor = oilsPair; +oilsPair.prototype.baseClass = DomainObject.prototype.constructor; + +function oilsPair( key, value ) { + + this._init_domainObject( "oilsPair" ); + this.element.appendChild( this.doc.createTextNode( value ) ); + this.element.setAttribute( "key", key ); +} + +oilsPair.prototype.getKey = function() { + return this.element.getAttribute( "key" ); +} + +oilsPair.prototype.getValue = function() { + return this.element.textContent; +} + + + +// ----------------------------------------------------------------------------- +// oilsArray - is a domainObjectCollection that stores a list of domainObject's +// or domainObjectCollections. +// ----------------------------------------------------------------------------- + +oilsArray.prototype = new domainObjectCollection() +oilsArray.prototype.constructor = oilsArray; +oilsArray.prototype.baseClass = domainObjectCollection.prototype.constructor; + +function oilsArray( obj_list ) { + this._initCollection( "oilsArray" ); + if(obj_list) { this.initArray( obj_list ); } +} + +// ----------------------------------------------------------------------------- +// Adds the array of objects to the array, these should be actual objects, not +// DOM nodes/elements, etc. +// ----------------------------------------------------------------------------- +oilsArray.prototype.initArray = function( obj_list ) { + if( obj_array != null && obj_array.length > 0 ) { + for( var i= 0; i!= obj_array.length; i++ ) { + this.add( obj_array[i] ); + } + } +} + +// ----------------------------------------------------------------------------- +// Returns an array of DomainObjects or domainObjectCollections. The objects +// returned will already be 'cast' into the correct object. i.e. you will not +// receieve an array of DomainObjects, but instead an array of oilsScalar's or +// an array of oilsHashe's, etc. +// ----------------------------------------------------------------------------- +oilsArray.prototype.getObjects = function() { + + var obj_list = new Array(); + var nodes = this.element.childNodes; + var i=0; + var node = nodes.item(i); + + while( node != null ) { + + if( node.nodeName == "oils:domainObject" || + node.nodeName == "oils:domainObjectCollection" ) { + obj_list[i++] = DomainObject.newFromNode( node ); + } + node = nodes.item(i); + } + + return obj_list; +} + + +// ----------------------------------------------------------------------------- +// oilsHash - an oilsHash is an oilsArray except it only stores oilsPair's +// ----------------------------------------------------------------------------- + +oilsHash.prototype = new oilsArray(); +oilsHash.prototype.constructor = oilsHash; +oilsHash.prototype.baseClass = oilsArray.prototype.constructor; + +function oilsHash( pair_array ) { + this._initCollection("oilsHash"); + if(pair_array) { this.initArray( pair_array ); } +} + +// ----------------------------------------------------------------------------- +// Returns the array of oilsPairs objects that this hash contains +// ----------------------------------------------------------------------------- +oilsHash.prototype.getPairs = function() { + return this.getObjects(); +} + + diff --git a/src/javascript/opensrf_jabber_transport.js b/src/javascript/opensrf_jabber_transport.js new file mode 100644 index 0000000..fbd6dac --- /dev/null +++ b/src/javascript/opensrf_jabber_transport.js @@ -0,0 +1,418 @@ +// ------------------------------------------------------------------ +// Houses the jabber transport code +// +// 1. jabber_connection - high level jabber component +// 2. jabber_message - message class +// 3. jabber_socket - socket handling code (shouldn't have to +// use this class directly) +// +// Requires oils_utils.js +// ------------------------------------------------------------------ + + + + + +// ------------------------------------------------------------------ +// JABBER_CONNECTION +// High level transport code + +// ------------------------------------------------------------------ +// Constructor +// ------------------------------------------------------------------ +jabber_connection.prototype = new transport_connection(); +jabber_connection.prototype.constructor = jabber_connection; +jabber_connection.baseClass = transport_connection.prototype.constructor; + +/** Initializes a jabber_connection object */ +function jabber_connection( username, password, resource ) { + + this.username = username; + this.password = password; + this.resource = resource; + this.socket = new jabber_socket(); + + this.host = ""; + +} + +/** Connects to the Jabber server. 'timeout' is the connect timeout + * in milliseconds + */ +jabber_connection.prototype.connect = function( host, port, timeout ) { + this.host = host; + return this.socket.connect( + this.username, this.password, this.resource, host, port, timeout ); +} + +/** Sends a message to 'recipient' with the provided message + * thread and body + */ +jabber_connection.prototype.send = function( recipient, thread, body ) { + var jid = this.username+"@"+this.host+"/"+this.resource; + var msg = new jabber_message( jid, recipient, thread, body ); + return this.socket.tcp_send( msg.to_string() ); +} + +/** This method will wait at most 'timeout' milliseconds + * for a Jabber message to arrive. If one arrives + * it is returned to the caller, other it returns null + */ +jabber_connection.prototype.recv = function( timeout ) { + return this.socket.recv( timeout ); +} + +/** Disconnects from the jabber server */ +jabber_connection.prototype.disconnect = function() { + return this.socket.disconnect(); +} + +/** Returns true if we are currently connected to the + * Jabber server + */ +jabber_connection.prototype.connected = function() { + return this.socket.connected(); +} + + + +// ------------------------------------------------------------------ +// JABBER_MESSAGE +// High level message handling code + + +jabber_message.prototype = new transport_message(); +jabber_message.prototype.constructor = jabber_message; +jabber_message.prototype.baseClass = transport_message.prototype.constructor; + +/** Builds a jabber_message object */ +function jabber_message( sender, recipient, thread, body ) { + + if( sender == null || recipient == null || recipient.length < 1 ) { return; } + + this.doc = new DOMParser().parseFromString("", "text/xml"); + this.root = this.doc.documentElement; + this.root.setAttribute( "from", sender ); + this.root.setAttribute( "to", recipient ); + + var body_node = this.doc.createElement("body"); + body_node.appendChild( this.doc.createTextNode( body ) ); + + var thread_node = this.doc.createElement("thread"); + thread_node.appendChild( this.doc.createTextNode( thread ) ); + + this.root.appendChild( body_node ); + this.root.appendChild( thread_node ); + +} + +/** Builds a new message from raw xml. + * If the message is a Jabber error message, then msg.is_error_msg + * is set to true; + */ +jabber_message.prototype.from_xml = function( xml ) { + var msg = new jabber_message(); + msg.doc = new DOMParser().parseFromString( xml, "text/xml" ); + msg.root = msg.doc.documentElement; + + if( msg.root.getAttribute( "type" ) == "error" ) { + msg.is_error_msg = true; + } else { + this.is_error_msg = false; + } + + return msg; +} + +/** Returns the 'from' field of the message */ +jabber_message.prototype.get_sender = function() { + return this.root.getAttribute( "from" ); +} + +/** Returns the jabber thread */ +jabber_message.prototype.get_thread = function() { + var nodes = this.root.getElementsByTagName( "thread" ); + var thread_node = nodes.item(0); + return thread_node.firstChild.nodeValue; +} + +/** Returns the message body */ +jabber_message.prototype.get_body = function() { + var nodes = this.root.getElementsByTagName( "body" ); + var body_node = nodes.item(0); + new Logger().transport( "Get Body returning:\n" + body_node.textContent, Logger.DEBUG ); + return body_node.textContent; +} + +/** Returns the message as a whole as an XML string */ +jabber_message.prototype.to_string = function() { + return new XMLSerializer().serializeToString(this.root); +} + + + + +// ------------------------------------------------------------------ +// TRANSPORT_SOCKET + +/** Initializes a new jabber_socket object */ +function jabber_socket() { + + this.is_connected = false; + this.outstream = ""; + this.instream = ""; + this.buffer = ""; + this.socket = ""; + +} + +/** Connects to the jabber server */ +jabber_socket.prototype.connect = + function( username, password, resource, host, port, timeout ) { + + var starttime = new Date().getTime(); + + // there has to be at least some kind of timeout + if( ! timeout || timeout < 100 ) { timeout = 1000; } + + try { + + this.xpcom_init( host, port ); + this.tcp_send( "" ); + + if( !this.tcp_recv( timeout ) ) { throw 1; } + + } catch( E ) { + throw new oils_ex_transport( "Could not open a socket to the transport server\n" + + "Server: " + host + " Port: " + port ); + } + + // Send the auth packet + this.tcp_send( "" + + username + "" + password + + "" + resource + "" ); + + var cur = new Date().getTime(); + var remaining = timeout - ( cur - starttime ); + this.tcp_recv( remaining ); + + if( ! this.connected() ) { + throw new oils_ex_transport( "Connection to transport server timed out" ); + } + + return true; + + +} + + +/** Sets up all of the xpcom components */ +jabber_socket.prototype.xpcom_init = function( host, port ) { + + var transportService = + Components.classes["@mozilla.org/network/socket-transport-service;1"] + .getService(Components.interfaces.nsISocketTransportService); + + this.transport = transportService.createTransport( null, 0, host, port, null); + + // ------------------------------------------------------------------ + // Build the stream objects + // ------------------------------------------------------------------ + this.outstream = this.transport.openOutputStream(0,0,0); + + var stream = this.transport.openInputStream(0,0,0); + + this.instream = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.nsIScriptableInputStream); + + this.instream.init(stream); + +} + +/** Send data to the TCP pipe */ +jabber_socket.prototype.tcp_send = function( data ) { + new Logger().transport( "Sending Data: \n" + data, Logger.INFO ); + this.outstream.write(data,data.length); +} + + +/** Accepts data coming directly from the socket. If we're not + * connected, we pass it off to procecc_connect(). Otherwise, + * this method adds the data to the local buffer. + */ +jabber_socket.prototype.process_data = function( data ) { + + new Logger().transport( "Received TCP data: " + data, Logger.DEBUG ); + + if( ! this.connected() ) { + this.process_connect( data ); + return; + } + + this.buffer += data; + +} + +/** Processes connect data to verify we are logged in correctly */ +jabber_socket.prototype.process_connect = function( data ) { + + var reg = /type=["\']result["\']/; + var err = /error/; + + if( reg.exec( data ) ) { + this.is_connected = true; + } else { + if( err.exec( data ) ) { + //throw new oils_ex_transport( "Server returned: \n" + data ); + throw new oils_ex_jabber_auth( "Server returned: \n" + data ); + // Throw exception, return something... + } + } +} + +/** Waits up to at most 'timeout' milliseconds for data to arrive + * in the TCP buffer. If there is at least one byte of data + * in the buffer, then all of the data that is in the buffer is sent + * to the process_data method for processing and the method returns. + */ +jabber_socket.prototype.tcp_recv = function( timeout ) { + + var count = this.instream.available(); + var did_receive = false; + + // ------------------------------------------------------------------ + // If there is any data in the tcp buffer, process it and return + // ------------------------------------------------------------------ + if( count > 0 ) { + + did_receive = true; + while( count > 0 ) { + this.process_data( this.instream.read( count ) ); + count = this.instream.available(); + } + + } else { + + // ------------------------------------------------------------------ + // Do the timeout dance + // ------------------------------------------------------------------ + + // ------------------------------------------------------------------ + // If there is no data in the buffer, wait up to timeout seconds + // for some data to arrive. Once it arrives, suck it all out + // and send it on for processing + // ------------------------------------------------------------------ + + var now, then; + now = new Date().getTime(); + then = now; + + // ------------------------------------------------------------------ + // Loop and poll for data every 50 ms. + // ------------------------------------------------------------------ + while( ((now-then) <= timeout) && count <= 0 ) { + sleep(50); + count = this.instream.available(); + now = new Date().getTime(); + } + + // ------------------------------------------------------------------ + // if we finally get some data, process it. + // ------------------------------------------------------------------ + if( count > 0 ) { + + did_receive = true; + while( count > 0 ) { // pull in all of the data there is + this.process_data( this.instream.read( count ) ); + count = this.instream.available(); + } + } + } + + return did_receive; + +} + +/** If a message is already sitting in the queue, it is returned. + * If not, this method waits till at most 'timeout' milliseconds + * for a full jabber message to arrive and then returns that. + * If none ever arrives, returns null. + */ +jabber_socket.prototype.recv = function( timeout ) { + + var now, then; + now = new Date().getTime(); + then = now; + + while( ((now-then) <= timeout) ) { + + if( this.buffer.length == 0 ) { + if( ! this.tcp_recv( timeout ) ) { + return null; + } + } + + //new Logger().transport( "\n\nTCP Buffer Before: \n" + this.buffer, Logger.DEBUG ); + + var buf = this.buffer; + this.buffer = ""; + + buf = buf.replace( /\n/g, '' ); // remove pesky newlines + + var reg = /.*?<\/message>/; + var iqr = /.*?<\/iq>/; + var out = reg.exec(buf); + + if( out ) { + + var msg_xml = out[0]; + this.buffer = buf.substring( msg_xml.length, buf.length ); + new Logger().transport( "Building Jabber message\n\n" + msg_xml, Logger.DEBUG ); + var jab_msg = new jabber_message().from_xml( msg_xml ); + if( jab_msg.is_error_msg ) { + new Logger().transport( "Received Jabber error message \n\n" + msg_xml, Logger.ERROR ); + } + + return jab_msg; + + + } else { + + out = iqr.exec(buf); + + if( out ) { + var msg_xml = out[0]; + this.buffer = buf.substring( msg_xml.length, buf.length ); + process_iq_data( msg_xml ); + return; + + } + } + + now = new Date().getTime(); + } + + return null; +} + +jabber_socket.prototype.process_iq_data = function( data ) { + new Logger().transport( "IQ Packet received... Not Implemented\n" + data, Logger.ERROR ); +} + +/** Disconnects from the jabber server and closes down shop */ +jabber_socket.prototype.disconnect = function() { + this.tcp_send( "" ); + this.instream.close(); + this.outstream.close(); +} + +/** True if connected */ +jabber_socket.prototype.connected = function() { + return this.is_connected; +} + + + + + diff --git a/src/javascript/opensrf_msg_stack.js b/src/javascript/opensrf_msg_stack.js new file mode 100644 index 0000000..51d5fd8 --- /dev/null +++ b/src/javascript/opensrf_msg_stack.js @@ -0,0 +1,203 @@ +// ----------------------------------------------------------------------------- +// Message stack code. +// ----------------------------------------------------------------------------- + + + +// ----------------------------------------------------------------------------- +// These just have to be defined for the 'static' methods to work +// ----------------------------------------------------------------------------- +function Transport() {} +function Message() {} +function Application() {} + +/** Transport handler. + * Takes a transport_message as parameter + * Parses the incoming message data and builds one or more oilsMessage objects + * from the XML. Each message is passed in turn to the Message.handler + * method. + */ +Transport.handler = function( msg ) { + + if( msg.is_error_msg ) { + throw new oils_ex_session( "Receved error message from jabber server for recipient: " + msg.get_sender() ); + return; + } + + var remote_id = msg.get_sender(); + var session_id = msg.get_thread(); + var body = msg.get_body(); + + var session = AppSession.find_session( session_id ); + + if( ! session ) { + new Logger().debug( "No AppSession found with id: " + session_id ); + return; + } + + session.set_remote_id( remote_id ); + + var nodelist; // oilsMessage nodes + + + // ----------------------------------------------------------------------------- + // Parse the incoming XML + // ----------------------------------------------------------------------------- + try { + + var doc = new DOMParser().parseFromString( body, "text/xml" ); + nodelist = doc.documentElement.getElementsByTagName( "oils:domainObject" ); + + if( ! nodelist || nodelist.length < 1 ) { + nodelist = doc.documentElement.getElementsByTagName( "domainObject" ); + if( ! nodelist || nodelist.length < 1 ) { throw 1; } + } + + } catch( E ) { + + var str = "Error parsing incoming message document"; + + if( E ) { throw new oils_ex_dom( str + "\n" + E.message + "\n" + body ); + } else { throw new oils_ex_dom( str + "\n" + body ); } + + } + + // ----------------------------------------------------------------------------- + // Pass the messages up the chain. + // ----------------------------------------------------------------------------- + try { + + var i = 0; + var node = nodelist.item(i); // a single oilsMessage + + while( node != null ) { + + if( node.getAttribute("name") != "oilsMessage" ) { + node = nodelist.item(++i); + continue; + } + + var oils_msg = new oilsMessage().replaceNode( node ); + + + // ----------------------------------------------------------------------------- + // this is really inefficient compared to the above line of code, + // however, this resolves some namespace oddities in DOMParser - + // namely, DOMParser puts dummy namesapaces in "a0" when, I'm assuming, it + // can't find the definition for the namesapace included. + // ----------------------------------------------------------------------------- + // var oils_msg = oilsMessage.newFromXML( new XMLSerializer().serializeToString( node ) ); + + new Logger().transport( "Transport passing up:\n" + oils_msg.toString(true), Logger.INFO ); + + // Pass the message off to the message layer + Message.handler( session, oils_msg ); + node = nodelist.item(++i); + } + + } catch( E ) { + + var str = "Processing Error"; + + if( E ) { throw new oils_ex_session( str + "\n" + E.message + "\n" + body ); } + else { throw new oils_ex_session( str + "\n" + body ); } + } +} + +/** Checks to see what type of message has arrived. If it is a 'STATUS' message, + * the appropriate transport layer actions are taken. Otherwise (RESULT), the + * message is passed to the Application.handler method. + */ +Message.handler = function( session, msg ) { + + var msg_type = msg.getType(); + var domain_object_payload = msg.getPayload(); + var tt = msg.getThreadTrace(); + + var code = domain_object_payload.getStatusCode(); + + new Logger().debug( "Message.handler received " + msg_type + " from " + + session.get_remote_id() + " with thread_trace " + tt + " and status " + code, Logger.INFO ); + new Logger().debug( "Message.handler received:\n" + domain_object_payload.toString(), Logger.DEBUG ); + + if( msg_type == oilsMessage.STATUS ) { + + switch( code ) { + + case oilsResponse.STATUS_OK + "": { + session.set_state( AppSession.CONNECTED ); + new Logger().debug( " * Connected Successfully: " + tt, Logger.INFO ); + return; + } + + case oilsResponse.STATUS_TIMEOUT + "": { + return Message.reset_session( session, tt, "Disconnected because of timeout" ); + } + + case oilsResponse.STATUS_REDIRECTED + "": { + return Message.reset_session( session, tt, "Disconnected because of redirect" ); + } + + case oilsResponse.STATUS_EXPFAILED + "": { + return Message.reset_session( session, tt, "Disconnected because of mangled session" ); + } + + case oilsResponse.STATUS_NOTALLOWED + "": { + new Logger().debug( "Method Not Allowed", Logger.ERROR ); + session.destroy(); + break; // we want the exception to be thrown below + } + + case oilsResponse.STATUS_CONTINUE +"": { + return; + } + + case oilsResponse.STATUS_COMPLETE + "": { + var req = session.get_request(tt); + if( req ) { req.set_complete(); } + new Logger().debug( " * Request completed: " + tt, Logger.INFO ); + return; + } + + default: { break; } + } + + } + + // throw any exceptions received from the server + if( domain_object_payload instanceof oilsException ) { + throw new oils_ex_session( domain_object_payload.getStatus() ); + } + + new Logger().debug( "Message Layer passing up:\n" + domain_object_payload.toString(), Logger.DEBUG ); + + Application.handler( session, domain_object_payload, tt ); + +} + +/** Utility method for cleaning up a session. Sets state to disconnected. + * resets the remoted_id, pushes the current app_request onto the resend + * queue. Logs a message. + */ +Message.reset_session = function( session, thread_trace, message ) { + session.set_state( AppSession.DISCONNECTED ); + session.reset_remote(); + var req = session.get_request( thread_trace ); + if( req && !req.complete ) { session.push_resend( req ); } + new Logger().debug( " * " + message + " : " + thread_trace, Logger.INFO ); +} + + +/** Pushes all incoming messages onto the session message queue. **/ +Application.handler = function( session, domain_object_payload, thread_trace ) { + + new Logger().debug( "Application Pushing onto queue: " + + thread_trace + "\n" + domain_object_payload.toString(), Logger.DEBUG ); + + session.push_queue( domain_object_payload, thread_trace ); +} + + + + + diff --git a/src/javascript/opensrf_transport.js b/src/javascript/opensrf_transport.js new file mode 100644 index 0000000..a0d846d --- /dev/null +++ b/src/javascript/opensrf_transport.js @@ -0,0 +1,64 @@ +/** @file oils_transport.js + * Houses the top level transport 'abstract' classes + * You can think of this like a header file which provides the + * interface that any transport code must implement + */ + + +// ------------------------------------------------------------------ +// TRANSPORT_CONNECTION + +/** Constructor */ +function transport_connection( username, password, resource ) { } + +/** Connects to the transport host */ +transport_connection.prototype.connect = function( host, /*int*/ port, /*int*/ timeout ) {} + +/** Sends a new message to recipient, with session_id and body */ +transport_connection.prototype.send = function( recipient, session_id, body ) {} + + +/** Returns a transport_message. This function will return + * immediately if there is a message available. Otherwise, it will + * wait at most 'timeout' seconds for one to arrive. Returns + * null if a message does not arrivae in time. + + * 'timeout' is specified in milliseconds + */ +transport_connection.prototype.recv = function( /*int*/ timeout ) {} + +/** This method calls recv and then passes the contents on to the + * message processing stack. + */ +transport_connection.prototype.process_msg = function( /*int*/ timeout ) { + var msg = this.recv( timeout ); + if( msg ) { Transport.handler( msg ); } +} + +/** Disconnects from the transpot host */ +transport_connection.prototype.disconnect = function() {} + +/** Returns true if this connection instance is currently connected + * to the transport host. + */ +transport_connection.prototype.connected = function() {} + + + +// ------------------------------------------------------------------ +// TRANSPORT_MESSAGE + + +/** Constructor */ +function transport_message( sender, recipient, session_id, body ) {} + +/** Returns the sender of the message */ +transport_message.prototype.get_sender = function() {} + +/** Returns the session id */ +transport_message.prototype.get_session = function() {} + +/** Returns the message body */ +transport_message.prototype.get_body = function() {} + + diff --git a/src/javascript/opensrf_utils.js b/src/javascript/opensrf_utils.js new file mode 100644 index 0000000..d348fd5 --- /dev/null +++ b/src/javascript/opensrf_utils.js @@ -0,0 +1,117 @@ +// ------------------------------------------------------------------ +// Houses utility functions +// ------------------------------------------------------------------ + +/** Prints to console. If alert_bool = true, displays a popup as well */ +function _debug( message, alert_bool ) { + + dump( "\n" + new Date() + "\n--------------------------------\n" + + message + "\n-----------------------------------\n" ); + if( alert_bool == true ) { alert( message ) }; + +} + + +/** Provides millisec sleep times, enjoy... */ +function sleep(gap){ + + var threadService = Components.classes["@mozilla.org/thread;1"]. + getService(Components.interfaces.nsIThread); + + var th = threadService.currentThread; + th.sleep(gap); +} + + + +/** Top level exception classe */ +function oils_ex() {} + +/** Initializes an exception */ +oils_ex.prototype.init_ex = function( name, message ) { + if( !(name && message) ) { return; } + this.name = name; + this.message = name + " : " + message; + new Logger().debug( "***\n" + this.message + "\n***", Logger.ERROR ); +} + +/** Returns a string representation of an exception */ +oils_ex.prototype.toString = function() { + return this.message; +} + + +oils_ex_transport.prototype = new oils_ex(); +oils_ex_transport.prototype.constructor = oils_ex_transport; +oils_ex_transport.baseClass = oils_ex.prototype.constructor; + +/** Thrown when the transport connection has problems*/ +function oils_ex_transport( message ) { + this.init_ex( "Transport Exception", message ); +} + + +oils_ex_transport_auth.prototype = new oils_ex_transport(); +oils_ex_transport_auth.prototype.constructor = oils_ex_transport_auth; +oils_ex_transport_auth.baseClass = oils_ex_transport.prototype.constructor; + +/** Thrown when the initial transport connection fails */ +function oils_ex_transport_auth( message ) { + this.init_ex( "Transport Authentication Error", message ); +} + +oils_ex_dom.prototype = new oils_ex(); +oils_ex_dom.prototype.constructor = oils_ex_dom; +oils_ex_dom.baseClass = oils_ex.prototype.constructor; + +/** Thrown when there is an XML problem */ +function oils_ex_dom( message ) { + this.init_ex( "DOM Error", message ); +} + +oils_ex_message.prototype = new oils_ex(); +oils_ex_message.prototype.constructor = oils_ex_message; +oils_ex_message.baseClass = oils_ex.prototype.constructor; + +/** Thrown when there is a message problem */ +function oils_ex_message( message ) { + this.init_ex( "OILS Message Layer Error", message ) ; +} + +oils_ex_config.prototype = new oils_ex(); +oils_ex_config.prototype.constructor = oils_ex_config; +oils_ex_config.prototype.baseClass = oils_ex.prototype.constructor; + +/** Thrown when values cannot be retrieved from the config file */ +function oils_ex_config( message ) { + this.init_ex( "OILS Config Exception", message ); +} + +oils_ex_logger.prototype = new oils_ex(); +oils_ex_logger.prototype.constructor = oils_ex_logger; +oils_ex_logger.prototype.baseClass = oils_ex.prototype.constructor; + +/** Thrown where there are logging problems */ +function oils_ex_logger( message ) { + this.init_ex( "OILS Logger Exception", message ); +} + + +oils_ex_args.prototype = new oils_ex(); +oils_ex_args.prototype.constructor = oils_ex_args; +oils_ex_args.prototype.baseClass = oils_ex.prototype.constructor; + +/** Thrown when when a method does not receive all of the required arguments */ +function oils_ex_args( message ) { + this.init_ex( "Method Argument Exception", message ); +} + + +oils_ex_session.prototype = new oils_ex(); +oils_ex_session.prototype.constructor = oils_ex_session; +oils_ex_session.prototype.baseClass = oils_ex.prototype.constructor; + +/** Thrown where there is a session processing problem */ +function oils_ex_session( message ) { + this.init_ex( "Session Exception", message ); +} diff --git a/src/libtransport/Makefile b/src/libtransport/Makefile new file mode 100644 index 0000000..9a5c0b5 --- /dev/null +++ b/src/libtransport/Makefile @@ -0,0 +1,31 @@ +# set this shell variable prior to calling make to run with malloc_check enabled +#MALLOC_CHECK_=1 # XXX debug only + +CC = gcc +LIB_DIR=../../lib +CC_OPTS = -Wall -O2 -I /usr/include/libxml2 -I /usr/include/libxml2/libxml -I ../../include -I ../../../../cc/libxml2-2.6.16 +LD_OPTS = -lxml2 +EXE_LD_OPTS = -L $(LIB_DIR) -lxml2 -ltransport +LIB_SOURCES = generic_utils.c transport_socket.c transport_session.c transport_message.c transport_client.c + +TARGETS=generic_utils.o transport_socket.o transport_message.o transport_session.o transport_client.o + +all: router basic_client + +basic_client: lib + $(CC) $(CC_OPTS) $(EXE_LD_OPTS) basic_client.c -o $@ + +# --- Libs ----------------------------------------------- + +lib: + $(CC) -c $(CC_OPTS) $(LIB_SOURCES) + $(CC) -shared -W1 $(LD_OPTS) $(TARGETS) -o $(LIB_DIR)/libtransport.so + + +# The router is compiled as a static binary because of some +# necessary #defines that would break the library +router: + $(CC) $(LD_OPTS) -D_ROUTER $(CC_OPTS) $(LIB_SOURCES) transport_router.c -o $@ + +clean: + /bin/rm -f *.o ../../lib/libtransport.so router basic_client diff --git a/src/libtransport/basic_client.c b/src/libtransport/basic_client.c new file mode 100644 index 0000000..61cb9e6 --- /dev/null +++ b/src/libtransport/basic_client.c @@ -0,0 +1,78 @@ +#include "transport_client.h" +#ifdef DMALLOC +#include "dmalloc.h" +#endif + +/** + * Simple jabber client + */ + + + + +/* connects and registers with the router */ +int main( int argc, char** argv ) { + + if( argc < 5 ) { + fatal_handler( "Usage: %s \n", argv[0] ); + return 99; + } + + transport_message* send; + transport_client* client = client_init( argv[2], 5222 ); + + // try to connect, allow 15 second connect timeout + if( client_connect( client, argv[1], "asdfjkjk", argv[3], 15 ) ) + info_handler("Connected...\n"); + else + fatal_handler( "NOT Connected...\n" ); + + if( fork() ) { + + fprintf(stderr, "Listener: %d\n", getpid() ); + char buf[300]; + memset(buf, 0, 300); + printf("=> "); + + while( fgets( buf, 299, stdin) ) { + + // remove newline + buf[strlen(buf)-1] = '\0'; + + if( strcmp(buf, "exit")==0) { + client_free( client ); + break; + } + + send = message_init( buf, "", "123454321", argv[4], NULL ); + client_send_message( client, send ); + message_free( send ); + printf("\n=> "); + memset(buf, 0, 300); + } + return 0; + + } else { + + fprintf(stderr, "Sender: %d\n", getpid() ); + + transport_message* recv; + while( (recv=client_recv( client, -1)) ) { + if( recv->is_error ) + fprintf( stderr, "\nReceived Error\t: ------------------\nFrom:\t\t" + "%s\nRouterFrom:\t%s\nBody:\t\t%s\nType %s\nCode %d\n=> ", recv->sender, recv->router_from, recv->body, recv->error_type, recv->error_code ); + else + fprintf( stderr, "\nReceived\t: ------------------\nFrom:\t\t" + "%s\nRouterFrom:\t%s\nBody:\t\t%s\n=> ", recv->sender, recv->router_from, recv->body ); + + message_free( recv ); + } + + } + return 0; + +} + + + + diff --git a/src/libtransport/generic_utils.c b/src/libtransport/generic_utils.c new file mode 100644 index 0000000..ba0b284 --- /dev/null +++ b/src/libtransport/generic_utils.c @@ -0,0 +1,392 @@ +#include "generic_utils.h" +#include +#include "pthread.h" + +int _init_log(); + +int balance = 0; + +#define LOG_ERROR 1 +#define LOG_WARNING 2 +#define LOG_INFO 3 + +void get_timestamp( char buf_25chars[]) { + time_t epoch = time(NULL); + char* localtime = strdup( ctime( &epoch ) ); + strcpy( buf_25chars, localtime ); + buf_25chars[ strlen(localtime)-1] = '\0'; // remove newline + free(localtime); +} + + +inline void* safe_malloc( int size ) { + void* ptr = (void*) malloc( size ); + if( ptr == NULL ) + fatal_handler("safe_malloc(): Out of Memory" ); + memset( ptr, 0, size ); + return ptr; +} + +// --------------------------------------------------------------------------------- +// Here we define how we want to handle various error levels. +// --------------------------------------------------------------------------------- + + +static FILE* log_file = NULL; +static int log_level = -1; +pthread_mutex_t mutex; + +void log_free() { if( log_file != NULL ) fclose(log_file ); } + +void fatal_handler( char* msg, ... ) { + + char buf[25]; + memset( buf, 0, 25 ); + get_timestamp( buf ); + pid_t pid = getpid(); + va_list args; + + if( _init_log() ) { + + if( log_level < LOG_ERROR ) + return; + + pthread_mutex_lock( &(mutex) ); + fprintf( log_file, "[%s %d] [%s] ", buf, pid, "ERR " ); + + va_start(args, msg); + vfprintf(log_file, msg, args); + va_end(args); + + fprintf(log_file, "\n"); + fflush( log_file ); + pthread_mutex_unlock( &(mutex) ); + + } + + /* also log to stderr for ERRORS*/ + fprintf( stderr, "[%s %d] [%s] ", buf, pid, "ERR " ); + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf( stderr, "\n" ); + + exit(99); +} + +void warning_handler( char* msg, ... ) { + + char buf[25]; + memset( buf, 0, 25 ); + get_timestamp( buf ); + pid_t pid = getpid(); + va_list args; + + if( _init_log() ) { + + if( log_level < LOG_WARNING ) + return; + + pthread_mutex_lock( &(mutex) ); + fprintf( log_file, "[%s %d] [%s] ", buf, pid, "WARN" ); + + va_start(args, msg); + vfprintf(log_file, msg, args); + va_end(args); + + fprintf(log_file, "\n"); + fflush( log_file ); + pthread_mutex_unlock( &(mutex) ); + + } else { + + fprintf( stderr, "[%s %d] [%s] ", buf, pid, "WARN" ); + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf( stderr, "\n" ); + } + +} + +void info_handler( char* msg, ... ) { + + char buf[25]; + memset( buf, 0, 25 ); + get_timestamp( buf ); + pid_t pid = getpid(); + va_list args; + + if( _init_log() ) { + + if( log_level < LOG_INFO ) + return; + pthread_mutex_lock( &(mutex) ); + fprintf( log_file, "[%s %d] [%s] ", buf, pid, "INFO" ); + + va_start(args, msg); + vfprintf(log_file, msg, args); + va_end(args); + + fprintf(log_file, "\n"); + fflush( log_file ); + pthread_mutex_unlock( &(mutex) ); + + } else { + + fprintf( stderr, "[%s %d] [%s] ", buf, pid, "INFO" ); + va_start(args, msg); + vfprintf(stderr, msg, args); + va_end(args); + fprintf( stderr, "\n" ); + fflush(stderr); + + } +} + + +int _init_log() { + + if( log_level != -1 ) + return 1; + + + pthread_mutex_init( &(mutex), NULL ); + + /* load the log level setting if we haven't already */ + + if( conf_reader == NULL ) { + return 0; + //fprintf( stderr, "No config file specified" ); + } + + char* log_level_str = config_value( "//router/log/level"); + if( log_level_str == NULL ) { + // fprintf( stderr, "No log level specified" ); + return 0; + } + log_level = atoi(log_level_str); + free(log_level_str); + + /* see if we have a log file yet */ + char* f = config_value("//router/log/file"); + + if( f == NULL ) { + // fprintf( stderr, "No log file specified" ); + return 0; + } + + log_file = fopen( f, "a" ); + if( log_file == NULL ) { + fprintf( stderr, "Unable to open log file %s for appending\n", f ); + return 0; + } + free(f); + return 1; + +} + + + +// --------------------------------------------------------------------------------- +// Flesh out a ubiqitous growing string buffer +// --------------------------------------------------------------------------------- + +growing_buffer* buffer_init(int num_initial_bytes) { + + if( num_initial_bytes > BUFFER_MAX_SIZE ) { + return NULL; + } + + + size_t len = sizeof(growing_buffer); + + growing_buffer* gb = (growing_buffer*) safe_malloc(len); + + gb->n_used = 0;/* nothing stored so far */ + gb->size = num_initial_bytes; + gb->buf = (char *) safe_malloc(gb->size + 1); + + return gb; +} + +int buffer_add(growing_buffer* gb, char* data) { + + + if( ! gb || ! data ) { return 0; } + int data_len = strlen( data ); + + if( data_len == 0 ) { return 0; } + int total_len = data_len + gb->n_used; + + while( total_len >= gb->size ) { + gb->size *= 2; + } + + if( gb->size > BUFFER_MAX_SIZE ) { + warning_handler( "Buffer reached MAX_SIZE of %d", BUFFER_MAX_SIZE ); + buffer_free( gb ); + return 0; + } + + char* new_data = (char*) safe_malloc( gb->size ); + + strcpy( new_data, gb->buf ); + free( gb->buf ); + gb->buf = new_data; + + strcat( gb->buf, data ); + gb->n_used = total_len; + return total_len; +} + + +int buffer_reset( growing_buffer *gb){ + if( gb == NULL ) { return 0; } + if( gb->buf == NULL ) { return 0; } + memset( gb->buf, 0, gb->size ); + gb->n_used = 0; + return 1; +} + +int buffer_free( growing_buffer* gb ) { + if( gb == NULL ) + return 0; + free( gb->buf ); + free( gb ); + return 1; +} + +char* buffer_data( growing_buffer *gb) { + return strdup( gb->buf ); +} + + + + + +// --------------------------------------------------------------------------------- +// Config module +// --------------------------------------------------------------------------------- + + +// --------------------------------------------------------------------------------- +// Allocate and build the conf_reader. This only has to happen once in a given +// system. Repeated calls are ignored. +// --------------------------------------------------------------------------------- +void config_reader_init( char* config_file ) { + if( conf_reader == NULL ) { + + if( config_file == NULL || strlen(config_file) == 0 ) { + fatal_handler( "config_reader_init(): No config file specified" ); + return; + } + + size_t len = sizeof( config_reader ); + conf_reader = (config_reader*) safe_malloc( len ); + + conf_reader->config_doc = xmlParseFile( config_file ); + conf_reader->xpathCx = xmlXPathNewContext( conf_reader->config_doc ); + if( conf_reader->xpathCx == NULL ) { + fatal_handler( "config_reader_init(): Unable to create xpath context"); + return; + } + } +} + +char* config_value( const char* xp_query, ... ) { + + if( conf_reader == NULL || xp_query == NULL ) { + fatal_handler( "config_value(): NULL param(s)" ); + return NULL; + } + + int slen = strlen(xp_query) + 512;/* this is unsafe ... */ + char xpath_query[ slen ]; + memset( xpath_query, 0, slen ); + va_list va_args; + va_start(va_args, xp_query); + vsprintf(xpath_query, xp_query, va_args); + va_end(va_args); + + + char* val; + int len = strlen(xpath_query) + 100; + char alert_buffer[len]; + memset( alert_buffer, 0, len ); + + // build the xpath object + xmlXPathObjectPtr xpathObj = xmlXPathEvalExpression( BAD_CAST xpath_query, conf_reader->xpathCx ); + + if( xpathObj == NULL ) { + sprintf( alert_buffer, "Could not build xpath object: %s", xpath_query ); + fatal_handler( alert_buffer ); + return NULL; + } + + + if( xpathObj->type == XPATH_NODESET ) { + + // ---------------------------------------------------------------------------- + // Grab nodeset from xpath query, then first node, then first text node and + // finaly the text node's value + // ---------------------------------------------------------------------------- + xmlNodeSet* node_list = xpathObj->nodesetval; + if( node_list == NULL ) { + sprintf( alert_buffer, "Could not build xpath object: %s", xpath_query ); + warning_handler(alert_buffer); + return NULL; + } + + if( node_list->nodeNr == 0 ) { + sprintf( alert_buffer, "Config XPATH query returned 0 results: %s", xpath_query ); + warning_handler(alert_buffer); + return NULL; + } + + + xmlNodePtr element_node = *(node_list)->nodeTab; + if( element_node == NULL ) { + sprintf( alert_buffer, "Config XPATH query returned 0 results: %s", xpath_query ); + warning_handler(alert_buffer); + return NULL; + } + + xmlNodePtr text_node = element_node->children; + if( text_node == NULL ) { + sprintf( alert_buffer, "Config variable has no value: %s", xpath_query ); + warning_handler(alert_buffer); + return NULL; + } + + val = text_node->content; + if( val == NULL ) { + sprintf( alert_buffer, "Config variable has no value: %s", xpath_query ); + warning_handler(alert_buffer); + return NULL; + } + + + } else { + sprintf( alert_buffer, "Xpath evaluation failed: %s", xpath_query ); + warning_handler(alert_buffer); + return NULL; + } + + char* value = strdup(val); + if( value == NULL ) { fatal_handler( "config_value(): Out of Memory!" ); } + + // Free XPATH structures + if( xpathObj != NULL ) xmlXPathFreeObject( xpathObj ); + + return value; +} + + +void config_reader_free() { + if( conf_reader == NULL ) { return; } + xmlXPathFreeContext( conf_reader->xpathCx ); + xmlFreeDoc( conf_reader->config_doc ); + free( conf_reader ); + conf_reader = NULL; +} diff --git a/src/libtransport/transport_client.c b/src/libtransport/transport_client.c new file mode 100644 index 0000000..40b2c36 --- /dev/null +++ b/src/libtransport/transport_client.c @@ -0,0 +1,212 @@ +#include "transport_client.h" + + +//int main( int argc, char** argv ); + +/* +int main( int argc, char** argv ) { + + transport_message* recv; + transport_message* send; + + transport_client* client = client_init( "spacely.georgialibraries.org", 5222 ); + + // try to connect, allow 15 second connect timeout + if( client_connect( client, "admin", "asdfjkjk", "system", 15 ) ) { + printf("Connected...\n"); + } else { + printf( "NOT Connected...\n" ); exit(99); + } + + while( (recv = client_recv( client, -1 )) ) { + + if( recv->body ) { + int len = strlen(recv->body); + char buf[len + 20]; + memset( buf, 0, len + 20); + sprintf( buf, "Echoing...%s", recv->body ); + send = message_init( buf, "Echoing Stuff", "12345", recv->sender, "" ); + } else { + send = message_init( " * ECHOING * ", "Echoing Stuff", "12345", recv->sender, "" ); + } + + if( send == NULL ) { printf("something's wrong"); } + client_send_message( client, send ); + + message_free( send ); + message_free( recv ); + } + + printf( "ended recv loop\n" ); + + return 0; + +} +*/ + + +transport_client* client_init( char* server, int port ) { + + if(server == NULL) return NULL; + + /* build and clear the client object */ + size_t c_size = sizeof( transport_client); + transport_client* client = (transport_client*) safe_malloc( c_size ); + + /* build and clear the message list */ + size_t l_size = sizeof( transport_message_list ); + client->m_list = (transport_message_list*) safe_malloc( l_size ); + + client->m_list->type = MESSAGE_LIST_HEAD; + client->session = init_transport( server, port, client ); + + + if(client->session == NULL) { + fatal_handler( "client_init(): Out of Memory"); + return NULL; + } + client->session->message_callback = client_message_handler; + + return client; +} + +int client_connect( transport_client* client, + char* username, char* password, char* resource, int connect_timeout ) { + if(client == NULL) return 0; + return session_connect( client->session, username, password, resource, connect_timeout ); +} + +int client_disconnect( transport_client* client ) { + if( client == NULL ) { return 0; } + return session_disconnect( client->session ); +} + +int client_connected( transport_client* client ) { + if(client == NULL) return 0; + return client->session->state_machine->connected; +} + +int client_send_message( transport_client* client, transport_message* msg ) { + if(client == NULL) return 0; + return session_send_msg( client->session, msg ); +} + + +transport_message* client_recv( transport_client* client, int timeout ) { + if( client == NULL ) { return NULL; } + + transport_message_node* node; + transport_message* msg; + + + /* see if there are any message in the messages queue */ + if( client->m_list->next != NULL ) { + /* pop off the first one... */ + node = client->m_list->next; + client->m_list->next = node->next; + msg = node->message; + free( node ); + return msg; + } + + if( timeout == -1 ) { /* wait potentially forever for data to arrive */ + + while( client->m_list->next == NULL ) { + if( ! session_wait( client->session, -1 ) ) { + return NULL; + } + } + + } else { /* wait at most timeout seconds */ + + + /* if not, loop up to 'timeout' seconds waiting for data to arrive */ + time_t start = time(NULL); + time_t remaining = (time_t) timeout; + + int counter = 0; + + int wait_ret; + while( client->m_list->next == NULL && remaining >= 0 ) { + + if( ! (wait_ret= session_wait( client->session, remaining)) ) + return NULL; + + ++counter; + +#ifdef _ROUTER + // session_wait returns -1 if there is no more data and we're a router + if( remaining == 0 && wait_ret == -1 ) { + break; + } +#else + if( remaining == 0 ) // or infinite loop + break; +#endif + + remaining -= (int) (time(NULL) - start); + } + + info_handler("It took %d reads to grab this messag", counter); + } + + /* again, see if there are any messages in the message queue */ + if( client->m_list->next != NULL ) { + /* pop off the first one... */ + node = client->m_list->next; + client->m_list->next = node->next; + msg = node->message; + free( node ); + return msg; + } else { + return NULL; + } +} + +/* throw the message into the message queue */ +void client_message_handler( void* client, transport_message* msg ){ + + if(client == NULL) return; + if(msg == NULL) return; + + transport_client* cli = (transport_client*) client; + + size_t len = sizeof(transport_message_node); + transport_message_node* node = + (transport_message_node*) safe_malloc(len); + node->type = MESSAGE_LIST_ITEM; + node->message = msg; + + + /* find the last node and put this onto the end */ + transport_message_node* tail = cli->m_list; + transport_message_node* current = tail->next; + + while( current != NULL ) { + tail = current; + current = current->next; + } + tail->next = node; +} + + +int client_free( transport_client* client ){ + if(client == NULL) return 0; + + session_free( client->session ); + transport_message_node* current = client->m_list->next; + transport_message_node* next; + + /* deallocate the list of messages */ + while( current != NULL ) { + next = current->next; + message_free( current->message ); + free(current); + current = next; + } + + free( client->m_list ); + free( client ); + return 1; +} + diff --git a/src/libtransport/transport_message.c b/src/libtransport/transport_message.c new file mode 100644 index 0000000..78ebb1b --- /dev/null +++ b/src/libtransport/transport_message.c @@ -0,0 +1,230 @@ +#include "transport_message.h" + + +// --------------------------------------------------------------------------------- +// Allocates and initializes a new transport_message +// --------------------------------------------------------------------------------- +transport_message* message_init( char* body, + char* subject, char* thread, char* recipient, char* sender ) { + + transport_message* msg = + (transport_message*) safe_malloc( sizeof(transport_message) ); + + if( body == NULL ) { body = ""; } + if( thread == NULL ) { thread = ""; } + if( subject == NULL ) { subject = ""; } + if( sender == NULL ) { sender = ""; } + if( recipient == NULL ) { recipient = ""; } + + msg->body = strdup(body); + msg->thread = strdup(thread); + msg->subject = strdup(subject); + msg->recipient = strdup(recipient); + msg->sender = strdup(sender); + + if( msg->body == NULL || msg->thread == NULL || + msg->subject == NULL || msg->recipient == NULL || + msg->sender == NULL ) { + + fatal_handler( "message_init(): Out of Memory" ); + return NULL; + } + + return msg; +} + +void message_set_router_info( transport_message* msg, char* router_from, + char* router_to, char* router_class, char* router_command, int broadcast_enabled ) { + + msg->router_from = strdup(router_from); + msg->router_to = strdup(router_to); + msg->router_class = strdup(router_class); + msg->router_command = strdup(router_command); + msg->broadcast = broadcast_enabled; + + if( msg->router_from == NULL || msg->router_to == NULL || + msg->router_class == NULL || msg->router_command == NULL ) + fatal_handler( "message_set_router_info(): Out of Memory" ); + + return; +} + + + +/* encodes the message for traversal */ +int message_prepare_xml( transport_message* msg ) { + if( msg->msg_xml != NULL ) { return 1; } + msg->msg_xml = message_to_xml( msg ); + return 1; +} + + +// --------------------------------------------------------------------------------- +// +// --------------------------------------------------------------------------------- +int message_free( transport_message* msg ){ + if( msg == NULL ) { return 0; } + + free(msg->body); + free(msg->thread); + free(msg->subject); + free(msg->recipient); + free(msg->sender); + free(msg->router_from); + free(msg->router_to); + free(msg->router_class); + free(msg->router_command); + if( msg->error_type != NULL ) free(msg->error_type); + if( msg->msg_xml != NULL ) free(msg->msg_xml); + free(msg); + return 1; +} + +// --------------------------------------------------------------------------------- +// Allocates a char* holding the XML representation of this jabber message +// --------------------------------------------------------------------------------- +char* message_to_xml( const transport_message* msg ) { + + int bufsize; + xmlChar* xmlbuf; + char* encoded_body; + + xmlNodePtr message_node; + xmlNodePtr body_node; + xmlNodePtr thread_node; + xmlNodePtr subject_node; + xmlNodePtr error_node; + + xmlDocPtr doc; + + xmlKeepBlanksDefault(0); + + if( ! msg ) { + warning_handler( "Passing NULL message to message_to_xml()"); + return 0; + } + + doc = xmlReadDoc( BAD_CAST "", NULL, NULL, XML_PARSE_NSCLEAN ); + message_node = xmlDocGetRootElement(doc); + + if( msg->is_error ) { + error_node = xmlNewChild(message_node, NULL, BAD_CAST "error" , NULL ); + xmlAddChild( message_node, error_node ); + xmlNewProp( error_node, BAD_CAST "type", BAD_CAST msg->error_type ); + char code_buf[16]; + memset( code_buf, 0, 16); + sprintf(code_buf, "%d", msg->error_code ); + xmlNewProp( error_node, BAD_CAST "code", BAD_CAST code_buf ); + } + + + /* set from and to */ + xmlNewProp( message_node, BAD_CAST "to", BAD_CAST msg->recipient ); + xmlNewProp( message_node, BAD_CAST "from", BAD_CAST msg->sender ); + xmlNewProp( message_node, BAD_CAST "router_from", BAD_CAST msg->router_from ); + xmlNewProp( message_node, BAD_CAST "router_to", BAD_CAST msg->router_to ); + xmlNewProp( message_node, BAD_CAST "router_class", BAD_CAST msg->router_class ); + xmlNewProp( message_node, BAD_CAST "router_command", BAD_CAST msg->router_command ); + + if( msg->broadcast ) + xmlNewProp( message_node, BAD_CAST "broadcast", BAD_CAST "1" ); + + /* Now add nodes where appropriate */ + char* body = msg->body; + char* subject = msg->subject; + char* thread = msg->thread; + + if( thread && strlen(thread) > 0 ) { + thread_node = xmlNewChild(message_node, NULL, (xmlChar*) "thread", (xmlChar*) thread ); + xmlAddChild( message_node, thread_node ); + } + + if( subject && strlen(subject) > 0 ) { + subject_node = xmlNewChild(message_node, NULL, (xmlChar*) "subject", (xmlChar*) subject ); + xmlAddChild( message_node, subject_node ); + } + + if( body && strlen(body) > 0 ) { + body_node = xmlNewChild(message_node, NULL, (xmlChar*) "body", (xmlChar*) body ); + xmlAddChild( message_node, body_node ); + } + + + xmlDocDumpFormatMemory( doc, &xmlbuf, &bufsize, 0 ); + encoded_body = strdup( (char*) xmlbuf ); + + if( encoded_body == NULL ) + fatal_handler("message_to_xml(): Out of Memory"); + + xmlFree(xmlbuf); + xmlFreeDoc( doc ); + xmlCleanupParser(); + + + /*** remove the XML declaration */ + + int len = strlen(encoded_body); + char tmp[len]; + memset( tmp, 0, len ); + int i; + int found_at = 0; + + /* when we reach the first >, take everything after it */ + for( i = 0; i!= len; i++ ) { + if( encoded_body[i] == 62) { /* ascii > */ + + /* found_at holds the starting index of the rest of the doc*/ + found_at = i + 1; + break; + } + } + + if( found_at ) { + /* move the shortened doc into the tmp buffer */ + strncpy( tmp, encoded_body + found_at, len - found_at ); + /* move the tmp buffer back into the allocated space */ + memset( encoded_body, 0, len ); + strcpy( encoded_body, tmp ); + } + + return encoded_body; +} + + + +void jid_get_username( const char* jid, char buf[] ) { + + if( jid == NULL ) { return; } + + /* find the @ and return whatever is in front of it */ + int len = strlen( jid ); + int i; + for( i = 0; i != len; i++ ) { + if( jid[i] == 64 ) { /*ascii @*/ + strncpy( buf, jid, i ); + return; + } + } +} + + +void jid_get_resource( const char* jid, char buf[]) { + if( jid == NULL ) { return; } + int len = strlen( jid ); + int i; + for( i = 0; i!= len; i++ ) { + if( jid[i] == 47 ) { /* ascii / */ + strncpy( buf, jid + i + 1, len - (i+1) ); + } + } +} + +void set_msg_error( transport_message* msg, char* type, int err_code ) { + + if( type != NULL && strlen( type ) > 0 ) { + msg->error_type = safe_malloc( strlen(type)+1); + strcpy( msg->error_type, type ); + msg->error_code = err_code; + } + msg->is_error = 1; +} diff --git a/src/libtransport/transport_session.c b/src/libtransport/transport_session.c new file mode 100644 index 0000000..71b0768 --- /dev/null +++ b/src/libtransport/transport_session.c @@ -0,0 +1,507 @@ +#include "transport_session.h" + + + +// --------------------------------------------------------------------------------- +// returns a built and allocated transport_session object. +// This codes does no network activity, only memory initilization +// --------------------------------------------------------------------------------- +transport_session* init_transport( char* server, int port, void* user_data ) { + + /* create the session struct */ + transport_session* session = + (transport_session*) safe_malloc( sizeof(transport_session) ); + + session->user_data = user_data; + + /* initialize the data buffers */ + session->body_buffer = buffer_init( JABBER_BODY_BUFSIZE ); + session->subject_buffer = buffer_init( JABBER_SUBJECT_BUFSIZE ); + session->thread_buffer = buffer_init( JABBER_THREAD_BUFSIZE ); + session->from_buffer = buffer_init( JABBER_JID_BUFSIZE ); + session->status_buffer = buffer_init( JABBER_STATUS_BUFSIZE ); + session->recipient_buffer = buffer_init( JABBER_JID_BUFSIZE ); + session->message_error_type = buffer_init( JABBER_JID_BUFSIZE ); + + /* for OILS extensions */ + session->router_to_buffer = buffer_init( JABBER_JID_BUFSIZE ); + session->router_from_buffer = buffer_init( JABBER_JID_BUFSIZE ); + session->router_class_buffer = buffer_init( JABBER_JID_BUFSIZE ); + session->router_command_buffer = buffer_init( JABBER_JID_BUFSIZE ); + + + + if( session->body_buffer == NULL || session->subject_buffer == NULL || + session->thread_buffer == NULL || session->from_buffer == NULL || + session->status_buffer == NULL || session->recipient_buffer == NULL || + session->router_to_buffer == NULL || session->router_from_buffer == NULL || + session->router_class_buffer == NULL || session->router_command_buffer == NULL ) { + + fatal_handler( "init_transport(): buffer_init returned NULL" ); + return 0; + } + + + /* initialize the jabber state machine */ + session->state_machine = (jabber_machine*) safe_malloc( sizeof(jabber_machine) ); + + /* initialize the sax push parser */ + session->parser_ctxt = xmlCreatePushParserCtxt(SAXHandler, session, "", 0, NULL); + + /* initialize the transport_socket structure */ + session->sock_obj = (transport_socket*) safe_malloc( sizeof(transport_socket) ); + + //int serv_size = strlen( server ); + session->sock_obj->server = server; + session->sock_obj->port = port; + session->sock_obj->data_received_callback = &grab_incoming; + + /* this will be handed back to us in callbacks */ + session->sock_obj->user_data = session; + + return session; +} + +/* XXX FREE THE BUFFERS */ +int session_free( transport_session* session ) { + if( ! session ) { return 0; } + + if( session->sock_obj ) + free( session->sock_obj ); + + if( session->state_machine ) free( session->state_machine ); + if( session->parser_ctxt) { + xmlCleanupCharEncodingHandlers(); + xmlFreeDoc( session->parser_ctxt->myDoc ); + xmlFreeParserCtxt(session->parser_ctxt); + } + xmlCleanupParser(); + + buffer_free(session->body_buffer); + buffer_free(session->subject_buffer); + buffer_free(session->thread_buffer); + buffer_free(session->from_buffer); + buffer_free(session->recipient_buffer); + buffer_free(session->status_buffer); + buffer_free(session->message_error_type); + buffer_free(session->router_to_buffer); + buffer_free(session->router_from_buffer); + buffer_free(session->router_class_buffer); + buffer_free(session->router_command_buffer); + + + free( session ); + return 1; +} + + +int session_wait( transport_session* session, int timeout ) { + if( ! session || ! session->sock_obj ) { + return 0; + } + int ret = tcp_wait( session->sock_obj, timeout ); + if( ! ret ) { + session->state_machine->connected = 0; + } + return ret; +} + +int session_send_msg( + transport_session* session, transport_message* msg ) { + + if( ! session ) { return 0; } + + if( ! session->state_machine->connected ) { + warning_handler("State machine is not connected in send_msg()"); + return 0; + } + + message_prepare_xml( msg ); + tcp_send( session->sock_obj, msg->msg_xml ); + + return 1; + +} + + +/* connects to server and connects to jabber */ +int session_connect( transport_session* session, + const char* username, const char* password, const char* resource, int connect_timeout ) { + + int size1 = 0; + int size2 = 0; + + if( ! session ) { + warning_handler( "session is null in connect" ); + return 0; + } + + + /* + session->state_machine->connected = + tcp_connected( session->sock_obj ); + + if( session->state_machine->connected ) { + return 1; + } + */ + + + char* server = session->sock_obj->server; + + if( ! session->sock_obj ) { + return 0; + } + + if( ! session->sock_obj->connected ) { + if( ! tcp_connect( session->sock_obj )) + return 0; + } + + /* the first Jabber connect stanza */ + size1 = 100 + strlen( server ); + char stanza1[ size1 ]; + memset( stanza1, 0, size1 ); + sprintf( stanza1, + "", + server ); + + /* the second jabber connect stanza including login info*/ + /* currently, we only support plain text login */ + size2 = 150 + strlen( username ) + strlen(password) + strlen(resource); + char stanza2[ size2 ]; + memset( stanza2, 0, size2 ); + + sprintf( stanza2, + "%s%s%s", + username, password, resource ); + + /* send the first stanze */ + session->state_machine->connecting = CONNECTING_1; + if( ! tcp_send( session->sock_obj, stanza1 ) ) { + warning_handler("error sending"); + return 0; + } + + /* wait for reply */ + tcp_wait( session->sock_obj, connect_timeout ); /* make the timeout smarter XXX */ + + /* server acknowledges our existence, now see if we can login */ + if( session->state_machine->connecting == CONNECTING_2 ) { + if( ! tcp_send( session->sock_obj, stanza2 ) ) { + warning_handler("error sending"); + return 0; + } + } + + /* wait for reply */ + tcp_wait( session->sock_obj, connect_timeout ); + + + if( session->state_machine->connected ) { + /* yar! */ + return 1; + } + + return 0; +} + +// --------------------------------------------------------------------------------- +// TCP data callback. Shove the data into the push parser. +// --------------------------------------------------------------------------------- +void grab_incoming( void * session, char* data ) { + transport_session* ses = (transport_session*) session; + if( ! ses ) { return; } + xmlParseChunk(ses->parser_ctxt, data, strlen(data), 0); +} + + +void startElementHandler( + void *session, const xmlChar *name, const xmlChar **atts) { + + transport_session* ses = (transport_session*) session; + if( ! ses ) { return; } + + + if( strcmp( name, "message" ) == 0 ) { + ses->state_machine->in_message = 1; + buffer_add( ses->from_buffer, get_xml_attr( atts, "from" ) ); + buffer_add( ses->recipient_buffer, get_xml_attr( atts, "to" ) ); + buffer_add( ses->router_from_buffer, get_xml_attr( atts, "router_from" ) ); + buffer_add( ses->router_to_buffer, get_xml_attr( atts, "router_to" ) ); + buffer_add( ses->router_class_buffer, get_xml_attr( atts, "router_class" ) ); + buffer_add( ses->router_command_buffer, get_xml_attr( atts, "router_command" ) ); + char* broadcast = get_xml_attr( atts, "broadcast" ); + if( broadcast ) + ses->router_broadcast = atoi( broadcast ); + + return; + } + + if( ses->state_machine->in_message ) { + + if( strcmp( name, "body" ) == 0 ) { + ses->state_machine->in_message_body = 1; + return; + } + + if( strcmp( name, "subject" ) == 0 ) { + ses->state_machine->in_subject = 1; + return; + } + + if( strcmp( name, "thread" ) == 0 ) { + ses->state_machine->in_thread = 1; + return; + } + + } + + if( strcmp( name, "presence" ) == 0 ) { + ses->state_machine->in_presence = 1; + buffer_add( ses->from_buffer, get_xml_attr( atts, "from" ) ); + buffer_add( ses->recipient_buffer, get_xml_attr( atts, "to" ) ); + return; + } + + if( strcmp( name, "status" ) == 0 ) { + ses->state_machine->in_status = 1; + return; + } + + + if( strcmp( name, "stream:error" ) == 0 ) { + ses->state_machine->in_error = 1; + warning_handler( "Received message from Jabber server" ); + return; + } + + + /* first server response from a connect attempt */ + if( strcmp( name, "stream:stream" ) == 0 ) { + if( ses->state_machine->connecting == CONNECTING_1 ) { + ses->state_machine->connecting = CONNECTING_2; + } + } + + if( strcmp( name, "error" ) == 0 ) { + ses->state_machine->in_message_error = 1; + buffer_add( ses->message_error_type, get_xml_attr( atts, "type" ) ); + ses->message_error_code = atoi( get_xml_attr( atts, "code" ) ); + warning_handler( "Received message" ); + return; + } + + if( strcmp( name, "iq" ) == 0 ) { + ses->state_machine->in_iq = 1; + + if( strcmp( get_xml_attr(atts, "type"), "result") == 0 + && ses->state_machine->connecting == CONNECTING_2 ) { + ses->state_machine->connected = 1; + ses->state_machine->connecting = 0; + return; + } + + if( strcmp( get_xml_attr(atts, "type"), "error") == 0 ) { + warning_handler( "Error connecting to jabber" ); + return; + } + } +} + +char* get_xml_attr( const xmlChar** atts, char* attr_name ) { + int i; + if (atts != NULL) { + for(i = 0;(atts[i] != NULL);i++) { + if( strcmp( atts[i++], attr_name ) == 0 ) { + if( atts[i] != NULL ) { + return (char*) atts[i]; + } + } + } + } + return NULL; +} + + +// ------------------------------------------------------------------ +// See which tags are ending +// ------------------------------------------------------------------ +void endElementHandler( void *session, const xmlChar *name) { + transport_session* ses = (transport_session*) session; + if( ! ses ) { return; } + + if( strcmp( name, "message" ) == 0 ) { + + /* pass off the message info the callback */ + if( ses->message_callback ) { + + /* here it's ok to pass in the raw buffers because + message_init allocates new space for the chars + passed in */ + transport_message* msg = message_init( + ses->body_buffer->buf, + ses->subject_buffer->buf, + ses->thread_buffer->buf, + ses->recipient_buffer->buf, + ses->from_buffer->buf ); + + message_set_router_info( msg, + ses->router_from_buffer->buf, + ses->router_to_buffer->buf, + ses->router_class_buffer->buf, + ses->router_command_buffer->buf, + ses->router_broadcast ); + + if( ses->message_error_type->n_used > 0 ) { + set_msg_error( msg, ses->message_error_type->buf, ses->message_error_code ); + } + + if( msg == NULL ) { return; } + ses->message_callback( ses->user_data, msg ); + } + + ses->state_machine->in_message = 0; + reset_session_buffers( session ); + return; + } + + if( strcmp( name, "body" ) == 0 ) { + ses->state_machine->in_message_body = 0; + return; + } + + if( strcmp( name, "subject" ) == 0 ) { + ses->state_machine->in_subject = 0; + return; + } + + if( strcmp( name, "thread" ) == 0 ) { + ses->state_machine->in_thread = 0; + return; + } + + if( strcmp( name, "iq" ) == 0 ) { + ses->state_machine->in_iq = 0; + if( ses->message_error_code > 0 ) { + warning_handler( "Error in IQ packet: code %d", ses->message_error_code ); + warning_handler( "Error 401 means not authorized" ); + } + reset_session_buffers( session ); + return; + } + + if( strcmp( name, "presence" ) == 0 ) { + ses->state_machine->in_presence = 0; + /* + if( ses->presence_callback ) { + // call the callback with the status, etc. + } + */ + reset_session_buffers( session ); + return; + } + + if( strcmp( name, "status" ) == 0 ) { + ses->state_machine->in_status = 0; + return; + } + + if( strcmp( name, "error" ) == 0 ) { + ses->state_machine->in_message_error = 0; + return; + } + + if( strcmp( name, "error:error" ) == 0 ) { + ses->state_machine->in_error = 0; + return; + } +} + +int reset_session_buffers( transport_session* ses ) { + buffer_reset( ses->body_buffer ); + buffer_reset( ses->subject_buffer ); + buffer_reset( ses->thread_buffer ); + buffer_reset( ses->from_buffer ); + buffer_reset( ses->recipient_buffer ); + buffer_reset( ses->router_from_buffer ); + buffer_reset( ses->router_to_buffer ); + buffer_reset( ses->router_class_buffer ); + buffer_reset( ses->router_command_buffer ); + buffer_reset( ses->message_error_type ); + + return 1; +} + +// ------------------------------------------------------------------ +// takes data out of the body of the message and pushes it into +// the appropriate buffer +// ------------------------------------------------------------------ +void characterHandler( + void *session, const xmlChar *ch, int len) { + + char data[len+1]; + memset( data, 0, len+1 ); + strncpy( data, (char*) ch, len ); + data[len] = 0; + + //printf( "Handling characters: %s\n", data ); + transport_session* ses = (transport_session*) session; + if( ! ses ) { return; } + + /* set the various message parts */ + if( ses->state_machine->in_message ) { + + if( ses->state_machine->in_message_body ) { + buffer_add( ses->body_buffer, data ); + } + + if( ses->state_machine->in_subject ) { + buffer_add( ses->subject_buffer, data ); + } + + if( ses->state_machine->in_thread ) { + buffer_add( ses->thread_buffer, data ); + } + } + + /* set the presence status */ + if( ses->state_machine->in_presence && ses->state_machine->in_status ) { + buffer_add( ses->status_buffer, data ); + } + + if( ses->state_machine->in_error ) { + /* for now... */ + warning_handler( "ERROR Xml fragment: %s\n", ch ); + } + +} + +/* XXX change to warning handlers */ +void parseWarningHandler( void *session, const char* msg, ... ) { + + va_list args; + va_start(args, msg); + fprintf(stdout, "WARNING"); + vfprintf(stdout, msg, args); + va_end(args); + fprintf(stderr, "XML WARNING: %s\n", msg ); +} + +void parseErrorHandler( void *session, const char* msg, ... ){ + + va_list args; + va_start(args, msg); + fprintf(stdout, "ERROR"); + vfprintf(stdout, msg, args); + va_end(args); + fprintf(stderr, "XML ERROR: %s\n", msg ); + +} + +int session_disconnect( transport_session* session ) { + if( session == NULL ) { return 0; } + tcp_send( session->sock_obj, ""); + return tcp_disconnect( session->sock_obj ); +} + diff --git a/src/libtransport/transport_socket.c b/src/libtransport/transport_socket.c new file mode 100644 index 0000000..6a9446c --- /dev/null +++ b/src/libtransport/transport_socket.c @@ -0,0 +1,289 @@ +#include "transport_socket.h" + + +/* +int main( char* argc, char** argv ) { + + transport_socket sock_obj; + sock_obj.port = 5222; + sock_obj.server = "10.0.0.4"; + sock_obj.data_received_callback = &print_stuff; + + printf("connecting...\n"); + if( (tcp_connect( &sock_obj )) < 0 ) { + printf( "error connecting" ); + } + + printf("sending...\n"); + if( tcp_send( &sock_obj, "\n" ) < 0 ) { + printf( "error sending" ); + } + + printf("waiting...\n"); + if( tcp_wait( &sock_obj, 15 ) < 0 ) { + printf( "error receiving" ); + } + + printf("disconnecting...\n"); + tcp_disconnect( &sock_obj ); + +} +*/ + + +// returns the socket fd, -1 on error +int tcp_connect( transport_socket* sock_obj ){ + + if( sock_obj == NULL ) { + fatal_handler( "connect(): null sock_obj" ); + return -1; + } + struct sockaddr_in remoteAddr, localAddr; + struct hostent *hptr; + int sock_fd; + + #ifdef WIN32 + WSADATA data; + char bfr; + if( WSAStartup(MAKEWORD(1,1), &data) ) { + fatal_handler( "somethin's broke with windows socket startup" ); + return -1; + } + #endif + + + + // ------------------------------------------------------------------ + // Create the socket + // ------------------------------------------------------------------ + if( (sock_fd = socket( AF_INET, SOCK_STREAM, 0 )) < 0 ) { + fatal_handler( "tcp_connect(): Cannot create socket" ); + return -1; + } + + // ------------------------------------------------------------------ + // Get the hostname + // ------------------------------------------------------------------ + if( (hptr = gethostbyname( sock_obj->server ) ) == NULL ) { + fatal_handler( "tcp_connect(): Unknown Host" ); + return -1; + } + + // ------------------------------------------------------------------ + // Construct server info struct + // ------------------------------------------------------------------ + memset( &remoteAddr, 0, sizeof(remoteAddr)); + remoteAddr.sin_family = AF_INET; + remoteAddr.sin_port = htons( sock_obj->port ); + memcpy( (char*) &remoteAddr.sin_addr.s_addr, + hptr->h_addr_list[0], hptr->h_length ); + + // ------------------------------------------------------------------ + // Construct local info struct + // ------------------------------------------------------------------ + memset( &localAddr, 0, sizeof( localAddr ) ); + localAddr.sin_family = AF_INET; + localAddr.sin_addr.s_addr = htonl( INADDR_ANY ); + localAddr.sin_port = htons(0); + + // ------------------------------------------------------------------ + // Bind to a local port + // ------------------------------------------------------------------ + if( bind( sock_fd, (struct sockaddr *) &localAddr, sizeof( localAddr ) ) < 0 ) { + fatal_handler( "tcp_connect(): Cannot bind to local port" ); + return -1; + } + + // ------------------------------------------------------------------ + // Connect to server + // ------------------------------------------------------------------ + if( connect( sock_fd, (struct sockaddr*) &remoteAddr, sizeof( struct sockaddr_in ) ) < 0 ) { + fatal_handler( "tcp_connect(): Cannot connect to server %s", sock_obj->server ); + return -1; + } + + sock_obj->sock_fd = sock_fd; + sock_obj->connected = 1; + return sock_fd; + +} + + +int tcp_send( transport_socket* sock_obj, const char* data ){ + + if( sock_obj == NULL ) { + fatal_handler( "tcp_send(): null sock_obj" ); + return 0; + } + + //fprintf( stderr, "TCP Sending: \n%s\n", data ); + + // ------------------------------------------------------------------ + // Send the data down the TCP pipe + // ------------------------------------------------------------------ + if( send( sock_obj->sock_fd, data, strlen(data), 0 ) < 0 ) { + fatal_handler( "tcp_send(): Error sending data" ); + return 0; + } + return 1; +} + + +int tcp_disconnect( transport_socket* sock_obj ){ + + if( sock_obj == NULL ) { + fatal_handler( "tcp_disconnect(): null sock_obj" ); + return -1; + } + + if( close( sock_obj->sock_fd ) == -1 ) { + + // ------------------------------------------------------------------ + // Not really worth throwing an exception for... should be logged. + // ------------------------------------------------------------------ + warning_handler( "tcp_disconnect(): Error closing socket" ); + return -1; + } + + return 0; +} + +// ------------------------------------------------------------------ +// And now for the gory C socket code. +// Returns 0 on failure, 1 otherwise +// ------------------------------------------------------------------ +int tcp_wait( transport_socket* sock_obj, int timeout ){ + + if( sock_obj == NULL ) { + fatal_handler( "tcp_wait(): null sock_obj" ); + return 0; + } + + int n = 0; + int retval = 0; + char buf[BUFSIZE]; + int sock_fd = sock_obj->sock_fd; + + + fd_set read_set; + + FD_ZERO( &read_set ); + FD_SET( sock_fd, &read_set ); + + // ------------------------------------------------------------------ + // Build the timeval struct + // ------------------------------------------------------------------ + struct timeval tv; + tv.tv_sec = timeout; + tv.tv_usec = 0; + + if( timeout == -1 ) { + + // ------------------------------------------------------------------ + // If timeout is -1, there is no timeout passed to the call to select + // ------------------------------------------------------------------ + if( (retval = select( sock_fd + 1 , &read_set, NULL, NULL, NULL)) == -1 ) { + warning_handler( "Call to select failed" ); + return 0; + } + + } else if( timeout != 0 ) { /* timeout of 0 means don't block */ + + if( (retval = select( sock_fd + 1 , &read_set, NULL, NULL, &tv)) == -1 ) { + warning_handler( "Call to select failed" ); + return 0; + } + } + + memset( &buf, 0, BUFSIZE ); + + if( set_fl( sock_fd, O_NONBLOCK ) < 0 ) + return 0; + +#ifdef _ROUTER // just read one buffer full of data + + n = recv(sock_fd, buf, BUFSIZE-1, 0); + sock_obj->data_received_callback( sock_obj->user_data, buf ); + if( n == 0 ) + n = -1; + +#else // read everything we can + + while( (n = recv(sock_fd, buf, BUFSIZE-1, 0) ) > 0 ) { + //printf("\nReceived: %s\n", buf); + sock_obj->data_received_callback( sock_obj->user_data, buf ); + memset( &buf, 0, BUFSIZE ); + } + +#endif + + if( clr_fl( sock_fd, O_NONBLOCK ) < 0 ) { + return 0; + } + + if( n < 0 ) { + if( errno != EAGAIN ) { + warning_handler( " * Error reading socket with errno %d", errno ); + return 0; + } + } + +#ifdef _ROUTER + return n; +#else + return 1; +#endif + +} + +int set_fl( int fd, int flags ) { + + int val; + + if( (val = fcntl( fd, F_GETFL, 0) ) < 0 ) { + fatal_handler("fcntl F_GETFL error"); + return -1; + } + + val |= flags; + + if( fcntl( fd, F_SETFL, val ) < 0 ) { + fatal_handler( "fcntl F_SETFL error" ); + return -1; + } + return 0; +} + +int clr_fl( int fd, int flags ) { + + int val; + + if( (val = fcntl( fd, F_GETFL, 0) ) < 0 ) { + fatal_handler("fcntl F_GETFL error" ); + return -1; + } + + val &= ~flags; + + if( fcntl( fd, F_SETFL, val ) < 0 ) { + fatal_handler( "fcntl F_SETFL error" ); + return -1; + } + return 0; +} + + +/* +int tcp_connected( transport_socket* obj ) { + + int ret; + if( ! obj->sock_fd ) { return 0; } + + ret = read( obj->sock_fd , NULL,0 ); + if( ret <= 0 ) { + return 0; + } + return 1; +} +*/ + diff --git a/src/patch/README b/src/patch/README new file mode 100644 index 0000000..3865013 --- /dev/null +++ b/src/patch/README @@ -0,0 +1,8 @@ +Patch Source Location Purpose +------------------------------------------------------------------------------------------------------ + +mod_offline.c jabberd-2.0s4 jabberd-2.0s4/sm/mod_offline.c Disables offline storage +------------------------------------------------------------------------------------------------------ + +nad.c jabberd-2.0s4 jabberd-2.0s4/util/nad.c Fixes segfault in a branch of code that we don't use + diff --git a/src/patch/mod_offline.c b/src/patch/mod_offline.c new file mode 100644 index 0000000..4234cd7 --- /dev/null +++ b/src/patch/mod_offline.c @@ -0,0 +1,266 @@ +/* + * jabberd - Jabber Open Source Server + * Copyright (c) 2002 Jeremie Miller, Thomas Muldowney, + * Ryan Eatmon, Robert Norris + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA02111-1307USA + */ + + +/** ! ! ! Patched version to disable offline storage for jabberd-2.0s4 ! ! ! */ + +#include "sm.h" + +/** @file sm/mod_offline.c + * @brief offline storage + * @author Robert Norris + * $Date$ + * $Revision$ + */ + +typedef struct _mod_offline_st { + int dropmessages; + int dropsubscriptions; +} *mod_offline_t; + +static mod_ret_t _offline_in_sess(mod_instance_t mi, sess_t sess, pkt_t pkt) { + st_ret_t ret; + os_t os; + os_object_t o; + os_type_t ot; + nad_t nad; + pkt_t queued; + int ns, elem, attr; + char cttl[15], cstamp[18]; + time_t ttl, stamp; + + /* if they're becoming available for the first time */ + if(pkt->type == pkt_PRESENCE && pkt->to == NULL && sess->user->top == NULL) { + + ret = storage_get(pkt->sm->st, "queue", jid_user(sess->jid), NULL, &os); + if(ret != st_SUCCESS) { + log_debug(ZONE, "storage_get returned %d", ret); + return mod_PASS; + } + + if(os_iter_first(os)) + do { + o = os_iter_object(os); + + if(os_object_get(o, "xml", (void **) &nad, &ot)) { + queued = pkt_new(pkt->sm, nad_copy(nad)); + if(queued == NULL) { + log_debug(ZONE, "invalid queued packet, not delivering"); + } else { + /* check expiry as necessary */ + if((ns = nad_find_scoped_namespace(queued->nad, uri_EXPIRE, NULL)) >= 0 && + (elem = nad_find_elem(queued->nad, 1, ns, "x", 1)) >= 0 && + (attr = nad_find_attr(queued->nad, elem, -1, "seconds", NULL)) >= 0) { + snprintf(cttl, 15, "%.*s", NAD_AVAL_L(queued->nad, attr), NAD_AVAL(queued->nad, attr)); + ttl = atoi(cttl); + + /* it should have a x:delay stamp, because we stamp everything we store */ + if((ns = nad_find_scoped_namespace(queued->nad, uri_DELAY, NULL)) >= 0 && + (elem = nad_find_elem(queued->nad, 1, ns, "x", 1)) >= 0 && + (attr = nad_find_attr(queued->nad, elem, -1, "stamp", NULL)) >= 0) { + snprintf(cstamp, 18, "%.*s", NAD_AVAL_L(queued->nad, attr), NAD_AVAL(queued->nad, attr)); + stamp = datetime_in(cstamp); + + if(stamp + ttl <= time(NULL)) { + log_debug(ZONE, "queued packet has expired, dropping"); + pkt_free(queued); + continue; + } + } + } + + log_debug(ZONE, "delivering queued packet to %s", jid_full(sess->jid)); + pkt_sess(queued, sess); + } + } + } while(os_iter_next(os)); + + os_free(os); + + /* drop the spool */ + storage_delete(pkt->sm->st, "queue", jid_user(sess->jid), NULL); + } + + /* pass it so that other modules and mod_presence can get it */ + return mod_PASS; +} + +static mod_ret_t _offline_pkt_user(mod_instance_t mi, user_t user, pkt_t pkt) { + mod_offline_t offline = (mod_offline_t) mi->mod->private; + int ns, elem, attr; + os_t os; + os_object_t o; + pkt_t event; + + /* send messages and s10ns to the top session */ + if(user->top != NULL && (pkt->type & pkt_MESSAGE || pkt->type & pkt_S10N)) { + pkt_sess(pkt, user->top); + return mod_HANDLED; + } + + /* save messages and s10ns for later */ + if((pkt->type & pkt_MESSAGE && !offline->dropmessages) || + (pkt->type & pkt_S10N && !offline->dropsubscriptions)) { + log_debug(ZONE, "saving message for later"); + + pkt_delay(pkt, time(NULL), user->sm->id); + + /* new object */ + os = os_new(); + o = os_object_new(os); + + os_object_put(o, "xml", pkt->nad, os_type_NAD); + + /* store it */ + switch(storage_put(user->sm->st, "queue", jid_user(user->jid), os)) { + case st_FAILED: + os_free(os); + return -stanza_err_INTERNAL_SERVER_ERROR; + + case st_NOTIMPL: + os_free(os); + return -stanza_err_SERVICE_UNAVAILABLE; /* xmpp-im 9.5#4 */ + + default: + os_free(os); + + /* send offline events if they asked for it */ + if((ns = nad_find_scoped_namespace(pkt->nad, uri_EVENT, NULL)) >= 0 && + (elem = nad_find_elem(pkt->nad, 1, ns, "x", 1)) >= 0 && + nad_find_elem(pkt->nad, elem, ns, "offline", 1) >= 0) { + + event = pkt_create(user->sm, "message", NULL, jid_full(pkt->from), jid_full(pkt->to)); + + attr = nad_find_attr(pkt->nad, 1, -1, "type", NULL); + if(attr >= 0) + nad_set_attr(event->nad, 1, -1, "type", NAD_AVAL(pkt->nad, attr), NAD_AVAL_L(pkt->nad, attr)); + + ns = nad_add_namespace(event->nad, uri_EVENT, NULL); + nad_append_elem(event->nad, ns, "x", 2); + nad_append_elem(event->nad, ns, "offline", 3); + + nad_append_elem(event->nad, ns, "id", 3); + attr = nad_find_attr(pkt->nad, 1, -1, "id", NULL); + if(attr >= 0) + nad_append_cdata(event->nad, NAD_AVAL(pkt->nad, attr), NAD_AVAL_L(pkt->nad, attr), 4); + + pkt_router(event); + } + + pkt_free(pkt); + return mod_HANDLED; + } + } + + return mod_PASS; +} + +static void _offline_user_delete(mod_instance_t mi, jid_t jid) { + os_t os; + os_object_t o; + os_type_t ot; + nad_t nad; + pkt_t queued; + int ns, elem, attr; + char cttl[15], cstamp[18]; + time_t ttl, stamp; + + log_debug(ZONE, "deleting queue for %s", jid_user(jid)); + + /* bounce the queue */ + if(storage_get(mi->mod->mm->sm->st, "queue", jid_user(jid), NULL, &os) == st_SUCCESS) { + if(os_iter_first(os)) + do { + o = os_iter_object(os); + + if(os_object_get(o, "xml", (void **) &nad, &ot)) { + queued = pkt_new(mi->mod->mm->sm, nad); + if(queued == NULL) { + log_debug(ZONE, "invalid queued packet, not delivering"); + } else { + /* check expiry as necessary */ + if((ns = nad_find_scoped_namespace(queued->nad, uri_EXPIRE, NULL)) >= 0 && + (elem = nad_find_elem(queued->nad, 1, ns, "x", 1)) >= 0 && + (attr = nad_find_attr(queued->nad, elem, -1, "seconds", NULL)) >= 0) { + snprintf(cttl, 15, "%.*s", NAD_AVAL_L(queued->nad, attr), NAD_AVAL(queued->nad, attr)); + ttl = atoi(cttl); + + /* it should have a x:delay stamp, because we stamp everything we store */ + if((ns = nad_find_scoped_namespace(queued->nad, uri_DELAY, NULL)) >= 0 && + (elem = nad_find_elem(queued->nad, 1, ns, "x", 1)) >= 0 && + (attr = nad_find_attr(queued->nad, elem, -1, "stamp", NULL)) >= 0) { + snprintf(cstamp, 18, "%.*s", NAD_AVAL_L(queued->nad, attr), NAD_AVAL(queued->nad, attr)); + stamp = datetime_in(cstamp); + + if(stamp + ttl <= time(NULL)) { + log_debug(ZONE, "queued packet has expired, dropping"); + pkt_free(queued); + continue; + } + } + } + + log_debug(ZONE, "bouncing queued packet from %s", jid_full(queued->from)); + pkt_router(pkt_error(queued, stanza_err_ITEM_NOT_FOUND)); + } + } + } while(os_iter_next(os)); + + os_free(os); + } + + storage_delete(mi->sm->st, "queue", jid_user(jid), NULL); +} + +static void _offline_free(module_t mod) { + mod_offline_t offline = (mod_offline_t) mod->private; + + free(offline); +} + +int offline_init(mod_instance_t mi, char *arg) { + module_t mod = mi->mod; + char *configval; + mod_offline_t offline; + int dropmessages = 0; + int dropsubscriptions = 0; + + if(mod->init) return 0; + + configval = config_get_one(mod->mm->sm->config, "offline.dropmessages", 0); + if (configval != NULL) + dropmessages = 1; + configval = config_get_one(mod->mm->sm->config, "offline.dropsubscriptions", 0); + if (configval != NULL) + dropsubscriptions = 1; + + offline = (mod_offline_t) malloc(sizeof(struct _mod_offline_st)); + offline->dropmessages = dropmessages; + offline->dropsubscriptions = dropsubscriptions; + + mod->private = offline; + + mod->in_sess = _offline_in_sess; + mod->pkt_user = _offline_pkt_user; + mod->user_delete = _offline_user_delete; + mod->free = _offline_free; + + return 0; +} diff --git a/src/patch/nad.c b/src/patch/nad.c new file mode 100644 index 0000000..d9914c4 --- /dev/null +++ b/src/patch/nad.c @@ -0,0 +1,1155 @@ +/* + * jabberd - Jabber Open Source Server + * Copyright (c) 2002 Jeremie Miller, Thomas Muldowney, + * Ryan Eatmon, Robert Norris + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA02111-1307USA + */ + + +/** ! ! ! Patched version to fix segfault issue for jabberd-2.0s4 ! ! ! */ + +/** + * !!! Things to do (after 2.0) + * + * - make nad_find_scoped_namespace() take an element index, and only search + * the scope on that element (currently, it searchs all elements from + * end to start, which isn't really correct, though it works in most cases + * + * - new functions: + * * insert one nad (or part thereof) into another nad + * * clear a part of a nad (like xmlnode_hide) + * + * - audit use of depth array and parent (see j2 bug #792) + */ + +#include "util.h" + +#ifdef HAVE_EXPAT +#include "expat/expat.h" +#endif + +/* define NAD_DEBUG to get pointer tracking - great for weird bugs that you can't reproduce */ +#ifdef NAD_DEBUG + +static xht _nad_alloc_tracked = NULL; +static xht _nad_free_tracked = NULL; + +static void _nad_ptr_check(const char *func, nad_t nad) { + char loc[24]; + snprintf(loc, sizeof(loc), "%x", (int) nad); + + if(xhash_get(_nad_alloc_tracked, loc) == NULL) { + fprintf(stderr, ">>> NAD OP %s: 0x%x not allocated!\n", func, (int) nad); + abort(); + } + + if(xhash_get(_nad_free_tracked, loc) != NULL) { + fprintf(stderr, ">>> NAD OP %s: 0x%x previously freed!\n", func, (int) nad); + abort(); + } + + fprintf(stderr, ">>> NAD OP %s: 0x%x\n", func, (int) nad); +} +#else +#define _nad_ptr_check(func,nad) +#endif + +#define BLOCKSIZE 1024 + +/** internal: do and return the math and ensure it gets realloc'd */ +int _nad_realloc(void **oblocks, int len) +{ + void *nblocks; + int nlen; + + /* round up to standard block sizes */ + nlen = (((len-1)/BLOCKSIZE)+1)*BLOCKSIZE; + + /* keep trying till we get it */ + while((nblocks = realloc(*oblocks, nlen)) == NULL) sleep(1); + *oblocks = nblocks; + return nlen; +} + +/** this is the safety check used to make sure there's always enough mem */ +#define NAD_SAFE(blocks, size, len) if((size) > len) len = _nad_realloc((void**)&(blocks),(size)); + +/** internal: append some cdata and return the index to it */ +int _nad_cdata(nad_t nad, const char *cdata, int len) +{ + NAD_SAFE(nad->cdata, nad->ccur + len, nad->clen); + + memcpy(nad->cdata + nad->ccur, cdata, len); + nad->ccur += len; + return nad->ccur - len; +} + +/** internal: create a new attr on any given elem */ +int _nad_attr(nad_t nad, int elem, int ns, const char *name, const char *val, int vallen) +{ + int attr; + + /* make sure there's mem for us */ + NAD_SAFE(nad->attrs, (nad->acur + 1) * sizeof(struct nad_attr_st), nad->alen); + + attr = nad->acur; + nad->acur++; + nad->attrs[attr].next = nad->elems[elem].attr; + nad->elems[elem].attr = attr; + nad->attrs[attr].lname = strlen(name); + nad->attrs[attr].iname = _nad_cdata(nad,name,nad->attrs[attr].lname); + if(vallen > 0) + nad->attrs[attr].lval = vallen; + else + nad->attrs[attr].lval = strlen(val); + nad->attrs[attr].ival = _nad_cdata(nad,val,nad->attrs[attr].lval); + nad->attrs[attr].my_ns = ns; + + return attr; +} + +/** create a new cache, simple pointer to a list of nads */ +nad_cache_t nad_cache_new(void) +{ + nad_cache_t cache; + while((cache = malloc(sizeof(nad_cache_t))) == NULL) sleep(1); + *cache = NULL; + +#ifdef NAD_DEBUG + if(_nad_alloc_tracked == NULL) _nad_alloc_tracked = xhash_new(501); + if(_nad_free_tracked == NULL) _nad_free_tracked = xhash_new(501); +#endif + + return cache; +} + + +/** free the cache and any nads in it */ +void nad_cache_free(nad_cache_t cache) +{ + nad_t cur; + while((cur = *cache) != NULL) + { + *cache = cur->next; + free(cur->elems); + free(cur->attrs); + free(cur->nss); + free(cur->cdata); + free(cur->depths); + free(cur); + } + free(cache); +} + +/** get the next nad from the cache, or create some */ +nad_t nad_new(nad_cache_t cache) +{ + nad_t nad; + +#ifndef NAD_DEBUG + /* If cache==NULL, then this NAD is not in a cache */ + + if ((cache!=NULL) && (*cache != NULL)) + { + nad = *cache; + *cache = nad->next; + nad->ccur = nad->ecur = nad->acur = nad->ncur = 0; + nad->scope = -1; + nad->cache = cache; + nad->next = NULL; + return nad; + } +#endif + + while((nad = malloc(sizeof(struct nad_st))) == NULL) sleep(1); + memset(nad,0,sizeof(struct nad_st)); + + nad->scope = -1; + nad->cache = cache; + +#ifdef NAD_DEBUG + { + char loc[24]; + snprintf(loc, sizeof(loc), "%x", (int) nad); + xhash_put(_nad_alloc_tracked, pstrdup(xhash_pool(_nad_alloc_tracked), loc), (void *) 1); + } + _nad_ptr_check(__func__, nad); +#endif + + return nad; +} + +nad_t nad_copy(nad_t nad) +{ + nad_t copy; + + _nad_ptr_check(__func__, nad); + + if(nad == NULL) return NULL; + + /* create a new nad not participating in a cache */ + copy = nad_new(NULL); + + /* if it's not large enough, make bigger */ + NAD_SAFE(copy->elems, nad->elen, copy->elen); + NAD_SAFE(copy->attrs, nad->alen, copy->alen); + NAD_SAFE(copy->nss, nad->nlen, copy->nlen); + NAD_SAFE(copy->cdata, nad->clen, copy->clen); + + /* copy all data */ + memcpy(copy->elems, nad->elems, nad->elen); + memcpy(copy->attrs, nad->attrs, nad->alen); + memcpy(copy->nss, nad->nss, nad->nlen); + memcpy(copy->cdata, nad->cdata, nad->clen); + + /* sync data */ + copy->ecur = nad->ecur; + copy->acur = nad->acur; + copy->ncur = nad->ncur; + copy->ccur = nad->ccur; + + copy->scope = nad->scope; + + return copy; +} + +/** free nad, or plug nad back in the cache */ +void nad_free(nad_t nad) +{ + if(nad == NULL) return; + +#ifdef NAD_DEBUG + _nad_ptr_check(__func__, nad); + { + char loc[24]; + snprintf(loc, sizeof(loc), "%x", (int) nad); + xhash_zap(_nad_alloc_tracked, loc); + xhash_put(_nad_free_tracked, pstrdup(xhash_pool(_nad_free_tracked), loc), (void *) nad); + } +#else + /* If nad->cache != NULL, then put back into cache, otherwise this nad is not in a cache */ + + if (nad->cache != NULL) { + nad->next = *(nad->cache); + *(nad->cache) = nad; + return; + } +#endif + + /* Free nad */ + free(nad->elems); + free(nad->attrs); + free(nad->cdata); + free(nad->nss); + free(nad->depths); +} + +/** locate the next elem at a given depth with an optional matching name */ +int nad_find_elem(nad_t nad, int elem, int ns, const char *name, int depth) +{ + int my_ns; + int lname = 0; + + _nad_ptr_check(__func__, nad); + + /* make sure there are valid args */ + if(elem >= nad->ecur || name == NULL) return -1; + + /* set up args for searching */ + depth = nad->elems[elem].depth + depth; + if(name != NULL) lname = strlen(name); + + /* search */ + for(elem++;elem < nad->ecur;elem++) + { + /* if we hit one with a depth less than ours, then we don't have the + * same parent anymore, bail */ + if(nad->elems[elem].depth < depth) + return -1; + + if(nad->elems[elem].depth == depth && (lname <= 0 || (lname == nad->elems[elem].lname && strncmp(name,nad->cdata + nad->elems[elem].iname, lname) == 0)) && + (ns < 0 || ((my_ns = nad->elems[elem].my_ns) >= 0 && NAD_NURI_L(nad, ns) == NAD_NURI_L(nad, my_ns) && strncmp(NAD_NURI(nad, ns), NAD_NURI(nad, my_ns), NAD_NURI_L(nad, ns)) == 0))) + return elem; + } + + return -1; +} + +/** get a matching attr on this elem, both name and optional val */ +int nad_find_attr(nad_t nad, int elem, int ns, const char *name, const char *val) +{ + int attr, my_ns; + int lname, lval = 0; + + _nad_ptr_check(__func__, nad); + + /* make sure there are valid args */ + if(elem >= nad->ecur || name == NULL) return -1; + + attr = nad->elems[elem].attr; + lname = strlen(name); + if(val != NULL) lval = strlen(val); + + while(attr >= 0) + { + /* hefty, match name and if a val, also match that */ + if(lname == nad->attrs[attr].lname && strncmp(name,nad->cdata + nad->attrs[attr].iname, lname) == 0 && + (lval <= 0 || (lval == nad->attrs[attr].lval && strncmp(val,nad->cdata + nad->attrs[attr].ival, lval) == 0)) && + (ns < 0 || ((my_ns = nad->attrs[attr].my_ns) >= 0 && NAD_NURI_L(nad, ns) == NAD_NURI_L(nad, my_ns) && strncmp(NAD_NURI(nad, ns), NAD_NURI(nad, my_ns), NAD_NURI_L(nad, ns)) == 0))) + return attr; + attr = nad->attrs[attr].next; + } + return -1; +} + +/** get a matching ns on this elem, both uri and optional prefix */ +int nad_find_namespace(nad_t nad, int elem, const char *uri, const char *prefix) +{ + int check, ns; + + _nad_ptr_check(__func__, nad); + + if(uri == NULL) + return -1; + + /* work backwards through our parents, looking for our namespace on each one. + * if we find it, link it. if not, the namespace is undeclared - for now, just drop it */ + check = elem; + while(check >= 0) + { + ns = nad->elems[check].ns; + while(ns >= 0) + { + if(strlen(uri) == NAD_NURI_L(nad, ns) && strncmp(uri, NAD_NURI(nad, ns), NAD_NURI_L(nad, ns)) == 0 && (prefix == NULL || (nad->nss[ns].iprefix >= 0 && strlen(prefix) == NAD_NPREFIX_L(nad, ns) && strncmp(prefix, NAD_NPREFIX(nad, ns), NAD_NPREFIX_L(nad, ns)) == 0))) + return ns; + ns = nad->nss[ns].next; + } + check = nad->elems[check].parent; + } + + return -1; +} + +/** find a namespace in scope */ +int nad_find_scoped_namespace(nad_t nad, const char *uri, const char *prefix) +{ + int ns; + + _nad_ptr_check(__func__, nad); + + if(uri == NULL) + return -1; + + for(ns = 0; ns < nad->ncur; ns++) + { + if(strlen(uri) == NAD_NURI_L(nad, ns) && strncmp(uri, NAD_NURI(nad, ns), NAD_NURI_L(nad, ns)) == 0 && + (prefix == NULL || + (nad->nss[ns].iprefix >= 0 && + strlen(prefix) == NAD_NPREFIX_L(nad, ns) && strncmp(prefix, NAD_NPREFIX(nad, ns), NAD_NPREFIX_L(nad, ns)) == 0))) + return ns; + } + + return -1; +} + +/** create, update, or zap any matching attr on this elem */ +void nad_set_attr(nad_t nad, int elem, int ns, const char *name, const char *val, int vallen) +{ + int attr; + + _nad_ptr_check(__func__, nad); + + /* find one to replace first */ + if((attr = nad_find_attr(nad, elem, ns, name, NULL)) < 0) + { + /* only create new if there's a value to store */ + if(val != NULL) + _nad_attr(nad, elem, ns, name, val, vallen); + return; + } + + /* got matching, update value or zap */ + if(val == NULL) + { + nad->attrs[attr].lval = nad->attrs[attr].lname = 0; + }else{ + if(vallen > 0) + nad->attrs[attr].lval = vallen; + else + nad->attrs[attr].lval = strlen(val); + nad->attrs[attr].ival = _nad_cdata(nad,val,nad->attrs[attr].lval); + } + +} + +/** shove in a new child elem after the given one */ +int nad_insert_elem(nad_t nad, int parent, int ns, const char *name, const char *cdata) +{ + int elem = parent + 1; + + _nad_ptr_check(__func__, nad); + + NAD_SAFE(nad->elems, (nad->ecur + 1) * sizeof(struct nad_elem_st), nad->elen); + + /* relocate all the rest of the elems (unless we're at the end already) */ + if(nad->ecur != elem) + memmove(&nad->elems[elem + 1], &nad->elems[elem], (nad->ecur - elem) * sizeof(struct nad_elem_st)); + nad->ecur++; + + /* set up req'd parts of new elem */ + nad->elems[elem].parent = parent; + nad->elems[elem].lname = strlen(name); + nad->elems[elem].iname = _nad_cdata(nad,name,nad->elems[elem].lname); + nad->elems[elem].attr = -1; + nad->elems[elem].ns = nad->scope; nad->scope = -1; + nad->elems[elem].itail = nad->elems[elem].ltail = 0; + nad->elems[elem].my_ns = ns; + + /* add cdata if given */ + if(cdata != NULL) + { + nad->elems[elem].lcdata = strlen(cdata); + nad->elems[elem].icdata = _nad_cdata(nad,cdata,nad->elems[elem].lcdata); + }else{ + nad->elems[elem].icdata = nad->elems[elem].lcdata = 0; + } + + /* parent/child */ + nad->elems[elem].depth = nad->elems[parent].depth + 1; + + return elem; +} + +/** wrap an element with another element */ +void nad_wrap_elem(nad_t nad, int elem, int ns, const char *name) +{ + int cur; + + _nad_ptr_check(__func__, nad); + + if(elem >= nad->ecur) return; + + NAD_SAFE(nad->elems, (nad->ecur + 1) * sizeof(struct nad_elem_st), nad->elen); + + /* relocate all the rest of the elems after us */ + memmove(&nad->elems[elem + 1], &nad->elems[elem], (nad->ecur - elem) * sizeof(struct nad_elem_st)); + nad->ecur++; + + /* set up req'd parts of new elem */ + nad->elems[elem].lname = strlen(name); + nad->elems[elem].iname = _nad_cdata(nad,name,nad->elems[elem].lname); + nad->elems[elem].attr = -1; + nad->elems[elem].ns = nad->scope; nad->scope = -1; + nad->elems[elem].itail = nad->elems[elem].ltail = 0; + nad->elems[elem].icdata = nad->elems[elem].lcdata = 0; + nad->elems[elem].my_ns = ns; + + /* raise the bar on all the children */ + nad->elems[elem+1].depth++; + for(cur = elem + 2; cur < nad->ecur && nad->elems[cur].depth > nad->elems[elem].depth; cur++) nad->elems[cur].depth++; + + /* relink the parents */ + nad->elems[elem].parent = nad->elems[elem + 1].parent; + nad->elems[elem + 1].parent = elem; +} + +/** create a new elem on the list */ +int nad_append_elem(nad_t nad, int ns, const char *name, int depth) +{ + int elem; + + _nad_ptr_check(__func__, nad); + + /* make sure there's mem for us */ + NAD_SAFE(nad->elems, (nad->ecur + 1) * sizeof(struct nad_elem_st), nad->elen); + + elem = nad->ecur; + nad->ecur++; + nad->elems[elem].lname = strlen(name); + nad->elems[elem].iname = _nad_cdata(nad,name,nad->elems[elem].lname); + nad->elems[elem].icdata = nad->elems[elem].lcdata = 0; + nad->elems[elem].itail = nad->elems[elem].ltail = 0; + nad->elems[elem].attr = -1; + nad->elems[elem].ns = nad->scope; nad->scope = -1; + nad->elems[elem].depth = depth; + nad->elems[elem].my_ns = ns; + + /* make sure there's mem in the depth array, then track us */ + NAD_SAFE(nad->depths, (depth + 1) * sizeof(int), nad->dlen); + nad->depths[depth] = elem; + + /* our parent is the previous guy in the depth array */ + if(depth <= 0) + nad->elems[elem].parent = -1; + else + nad->elems[elem].parent = nad->depths[depth - 1]; + + return elem; +} + +/** attach new attr to the last elem */ +int nad_append_attr(nad_t nad, int ns, const char *name, const char *val) +{ + _nad_ptr_check(__func__, nad); + + return _nad_attr(nad, nad->ecur - 1, ns, name, val, 0); +} + +/** append new cdata to the last elem */ +void nad_append_cdata(nad_t nad, const char *cdata, int len, int depth) +{ + int elem = nad->ecur - 1; + + _nad_ptr_check(__func__, nad); + + /* make sure this cdata is the child of the last elem to append */ + if(nad->elems[elem].depth == depth - 1) + { + if(nad->elems[elem].icdata == 0) + nad->elems[elem].icdata = nad->ccur; + _nad_cdata(nad,cdata,len); + nad->elems[elem].lcdata += len; + return; + } + + /* otherwise, pin the cdata on the tail of the last element at this depth */ + elem = nad->depths[depth]; + if(nad->elems[elem].itail == 0) + nad->elems[elem].itail = nad->ccur; + _nad_cdata(nad,cdata,len); + nad->elems[elem].ltail += len; +} + +/** bring a new namespace into scope */ +int nad_add_namespace(nad_t nad, const char *uri, const char *prefix) +{ + int ns; + + _nad_ptr_check(__func__, nad); + + /* only add it if its not already in scope */ + ns = nad_find_scoped_namespace(nad, uri, NULL); + if(ns >= 0) + return ns; + + /* make sure there's mem for us */ + NAD_SAFE(nad->nss, (nad->ncur + 1) * sizeof(struct nad_ns_st), nad->nlen); + + ns = nad->ncur; + nad->ncur++; + nad->nss[ns].next = nad->scope; + nad->scope = ns; + + nad->nss[ns].luri = strlen(uri); + nad->nss[ns].iuri = _nad_cdata(nad, uri, nad->nss[ns].luri); + if(prefix != NULL) + { + nad->nss[ns].lprefix = strlen(prefix); + nad->nss[ns].iprefix = _nad_cdata(nad, prefix, nad->nss[ns].lprefix); + } + else + nad->nss[ns].iprefix = -1; + + return ns; +} + +/** declare a namespace on an already-existing element */ +int nad_append_namespace(nad_t nad, int elem, const char *uri, const char *prefix) { + int ns; + + _nad_ptr_check(__func__, nad); + + /* see if its already scoped on this element */ + ns = nad_find_namespace(nad, elem, uri, NULL); + if(ns >= 0) + return ns; + + /* make some room */ + NAD_SAFE(nad->nss, (nad->ncur + 1) * sizeof(struct nad_ns_st), nad->nlen); + + ns = nad->ncur; + nad->ncur++; + nad->nss[ns].next = nad->elems[elem].ns; + nad->elems[elem].ns = ns; + + nad->nss[ns].luri = strlen(uri); + nad->nss[ns].iuri = _nad_cdata(nad, uri, nad->nss[ns].luri); + if(prefix != NULL) + { + nad->nss[ns].lprefix = strlen(prefix); + nad->nss[ns].iprefix = _nad_cdata(nad, prefix, nad->nss[ns].lprefix); + } + else + nad->nss[ns].iprefix = -1; + + return ns; +} + +void _nad_escape(nad_t nad, int data, int len, int flag) +{ + char *c; + int ic; + + if(len <= 0) return; + + /* first, if told, find and escape ' */ + while(flag >= 3 && (c = memchr(nad->cdata + data,'\'',len)) != NULL) + { + /* get offset */ + ic = c - nad->cdata; + + /* cute, eh? handle other data before this normally */ + _nad_escape(nad, data, ic - data, 2); + + /* ensure enough space, and add our escaped ' */ + NAD_SAFE(nad->cdata, nad->ccur + 6, nad->clen); + memcpy(nad->cdata + nad->ccur, "'", 6); + nad->ccur += 6; + + /* just update and loop for more */ + len -= (ic+1) - data; + data = ic+1; + } + + /* next look for < */ + while(flag >= 2 && (c = memchr(nad->cdata + data,'<',len)) != NULL) + { + ic = c - nad->cdata; + _nad_escape(nad, data, ic - data, 1); + + /* ensure enough space, and add our escaped < */ + NAD_SAFE(nad->cdata, nad->ccur + 4, nad->clen); + memcpy(nad->cdata + nad->ccur, "<", 4); + nad->ccur += 4; + + /* just update and loop for more */ + len -= (ic+1) - data; + data = ic+1; + } + + /* check for ]]>, we need to escape the > */ + /* WE DID THIS (add the '0' as the first test to the while loop + because the loops dies 3 lines in... (and we don't reall + need this)) ... + */ + while( 0 && flag >= 1 && (c = memchr(nad->cdata + data, '>', len)) != NULL) + { + ic = c - nad->cdata; + + _nad_escape(nad, data, ic - data, 0); + + /* check for the sequence */ + + if( c >= nad->cdata + 2 && c[-1] == ']' && c[-2] == ']') + { + /* ensure enough space, and add our escaped > */ + NAD_SAFE(nad->cdata, nad->ccur + 4, nad->clen); + memcpy(nad->cdata + nad->ccur, ">", 4); + nad->ccur += 4; + } + + /* otherwise, just plug the > in as-is */ + else + { + NAD_SAFE(nad->cdata, nad->ccur + 1, nad->clen); + *(nad->cdata + nad->ccur) = '>'; + nad->ccur++; + } + + /* just update and loop for more */ + len -= (ic+1) - data; + data = ic+1; + } + + /* if & is found, escape it */ + while((c = memchr(nad->cdata + data,'&',len)) != NULL) + { + ic = c - nad->cdata; + + /* ensure enough space */ + NAD_SAFE(nad->cdata, nad->ccur + 5 + (ic - data), nad->clen); + + /* handle normal data */ + memcpy(nad->cdata + nad->ccur, nad->cdata + data, (ic - data)); + nad->ccur += (ic - data); + + /* append escaped < */ + memcpy(nad->cdata + nad->ccur, "&", 5); + nad->ccur += 5; + + /* just update and loop for more */ + len -= (ic+1) - data; + data = ic+1; + } + + /* nothing exciting, just append normal cdata */ + NAD_SAFE(nad->cdata, nad->ccur + len, nad->clen); + memcpy(nad->cdata + nad->ccur, nad->cdata + data, len); + nad->ccur += len; +} + +/** internal recursive printing function */ +int _nad_lp0(nad_t nad, int elem) +{ + int attr; + int ndepth; + int ns; + + /* there's a lot of code in here, but don't let that scare you, it's just duplication in order to be a bit more efficient cpu-wise */ + + /* this whole thing is in a big loop for processing siblings */ + while(elem != nad->ecur) + { + + /* make enough space for the opening element */ + ns = nad->elems[elem].my_ns; + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + NAD_SAFE(nad->cdata, nad->ccur + nad->elems[elem].lname + nad->nss[ns].lprefix + 2, nad->clen); + } else { + NAD_SAFE(nad->cdata, nad->ccur + nad->elems[elem].lname + 1, nad->clen); + } + + /* opening tag */ + *(nad->cdata + nad->ccur++) = '<'; + + /* add the prefix if necessary */ + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->nss[ns].iprefix, nad->nss[ns].lprefix); + nad->ccur += nad->nss[ns].lprefix; + *(nad->cdata + nad->ccur++) = ':'; + } + + /* copy in the name */ + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->elems[elem].iname, nad->elems[elem].lname); + nad->ccur += nad->elems[elem].lname; + + /* add the namespaces */ + for(ns = nad->elems[elem].ns; ns >= 0; ns = nad->nss[ns].next) + { + /* never explicitly declare the implicit xml namespace */ + if(nad->nss[ns].luri == strlen(uri_XML) && strncmp(uri_XML, nad->cdata + nad->nss[ns].iuri, nad->nss[ns].luri) == 0) + continue; + + /* make space */ + if(nad->nss[ns].iprefix >= 0) + { + NAD_SAFE(nad->cdata, nad->ccur + nad->nss[ns].luri + nad->nss[ns].lprefix + 10, nad->clen); + } else { + NAD_SAFE(nad->cdata, nad->ccur + nad->nss[ns].luri + 9, nad->clen); + } + + /* start */ + memcpy(nad->cdata + nad->ccur, " xmlns", 6); + nad->ccur += 6; + + /* prefix if necessary */ + if(nad->nss[ns].iprefix >= 0) + { + *(nad->cdata + nad->ccur++) = ':'; + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->nss[ns].iprefix, nad->nss[ns].lprefix); + nad->ccur += nad->nss[ns].lprefix; + } + + *(nad->cdata + nad->ccur++) = '='; + *(nad->cdata + nad->ccur++) = '\''; + + /* uri */ + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->nss[ns].iuri, nad->nss[ns].luri); + nad->ccur += nad->nss[ns].luri; + + *(nad->cdata + nad->ccur++) = '\''; + } + + for(attr = nad->elems[elem].attr; attr >= 0; attr = nad->attrs[attr].next) + { + if(nad->attrs[attr].lname <= 0) continue; + + /* make enough space for the wrapper part */ + ns = nad->attrs[attr].my_ns; + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + NAD_SAFE(nad->cdata, nad->ccur + nad->attrs[attr].lname + nad->nss[ns].lprefix + 4, nad->clen); + } else { + NAD_SAFE(nad->cdata, nad->ccur + nad->attrs[attr].lname + 3, nad->clen); + } + + *(nad->cdata + nad->ccur++) = ' '; + + /* add the prefix if necessary */ + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->nss[ns].iprefix, nad->nss[ns].lprefix); + nad->ccur += nad->nss[ns].lprefix; + *(nad->cdata + nad->ccur++) = ':'; + } + + /* copy in the name parts */ + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->attrs[attr].iname, nad->attrs[attr].lname); + nad->ccur += nad->attrs[attr].lname; + *(nad->cdata + nad->ccur++) = '='; + *(nad->cdata + nad->ccur++) = '\''; + + /* copy in the escaped value */ + _nad_escape(nad, nad->attrs[attr].ival, nad->attrs[attr].lval, 3); + + /* make enough space for the closing quote and add it */ + NAD_SAFE(nad->cdata, nad->ccur + 1, nad->clen); + *(nad->cdata + nad->ccur++) = '\''; + } + + /* figure out what's next */ + if(elem+1 == nad->ecur) + ndepth = -1; + else + ndepth = nad->elems[elem+1].depth; + + /* handle based on if there are children, update nelem after done */ + if(ndepth <= nad->elems[elem].depth) + { + /* make sure there's enough for what we could need */ + NAD_SAFE(nad->cdata, nad->ccur + 2, nad->clen); + if(nad->elems[elem].lcdata == 0) + { + memcpy(nad->cdata + nad->ccur, "/>", 2); + nad->ccur += 2; + }else{ + *(nad->cdata + nad->ccur++) = '>'; + + /* copy in escaped cdata */ + _nad_escape(nad, nad->elems[elem].icdata, nad->elems[elem].lcdata,2); + + /* make room */ + ns = nad->elems[elem].my_ns; + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + NAD_SAFE(nad->cdata, nad->ccur + 4 + nad->elems[elem].lname + nad->nss[ns].lprefix, nad->clen); + } else { + NAD_SAFE(nad->cdata, nad->ccur + 3 + nad->elems[elem].lname, nad->clen); + } + + /* close tag */ + memcpy(nad->cdata + nad->ccur, "ccur += 2; + + /* add the prefix if necessary */ + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->nss[ns].iprefix, nad->nss[ns].lprefix); + nad->ccur += nad->nss[ns].lprefix; + *(nad->cdata + nad->ccur++) = ':'; + } + + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->elems[elem].iname, nad->elems[elem].lname); + nad->ccur += nad->elems[elem].lname; + *(nad->cdata + nad->ccur++) = '>'; + } + + /* always try to append the tail */ + _nad_escape(nad, nad->elems[elem].itail, nad->elems[elem].ltail,2); + + /* if no siblings either, bail */ + if(ndepth < nad->elems[elem].depth) + return elem+1; + + /* next sibling */ + elem++; + }else{ + int nelem; + /* process any children */ + + /* close ourself and append any cdata first */ + NAD_SAFE(nad->cdata, nad->ccur + 1, nad->clen); + *(nad->cdata + nad->ccur++) = '>'; + _nad_escape(nad, nad->elems[elem].icdata, nad->elems[elem].lcdata,2); + + /* process children */ + nelem = _nad_lp0(nad,elem+1); + + /* close and tail up */ + ns = nad->elems[elem].my_ns; + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + NAD_SAFE(nad->cdata, nad->ccur + 4 + nad->elems[elem].lname + nad->nss[ns].lprefix, nad->clen); + } else { + NAD_SAFE(nad->cdata, nad->ccur + 3 + nad->elems[elem].lname, nad->clen); + } + memcpy(nad->cdata + nad->ccur, "ccur += 2; + if(ns >= 0 && nad->nss[ns].iprefix >= 0) + { + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->nss[ns].iprefix, nad->nss[ns].lprefix); + nad->ccur += nad->nss[ns].lprefix; + *(nad->cdata + nad->ccur++) = ':'; + } + memcpy(nad->cdata + nad->ccur, nad->cdata + nad->elems[elem].iname, nad->elems[elem].lname); + nad->ccur += nad->elems[elem].lname; + *(nad->cdata + nad->ccur++) = '>'; + _nad_escape(nad, nad->elems[elem].itail, nad->elems[elem].ltail,2); + + /* if the next element is not our sibling, we're done */ + if(nelem < nad->ecur && nad->elems[nelem].depth < nad->elems[elem].depth) + return nelem; + + /* for next sibling in while loop */ + elem = nelem; + } + + /* here's the end of that big while loop */ + } + + return elem; +} + +void nad_print(nad_t nad, int elem, char **xml, int *len) +{ + int ixml = nad->ccur; + + _nad_ptr_check(__func__, nad); + + _nad_lp0(nad,elem); + *len = nad->ccur - ixml; + *xml = nad->cdata + ixml; +} + +/** + * nads serialize to a buffer of this form: + * + * [buflen][ecur][acur][ncur][ccur][elems][attrs][nss][cdata] + * + * nothing is done with endianness or word length, so the nad must be + * serialized and deserialized on the same platform + * + * buflen is not actually used by deserialize(), but is provided as a + * convenience to the application so it knows how many bytes to read before + * passing them in to deserialize() + * + * the depths array is not stored, so after deserialization + * nad_append_elem() and nad_append_cdata() will not work. this is rarely + * a problem + */ + +void nad_serialize(nad_t nad, char **buf, int *len) { + char *pos; + + _nad_ptr_check(__func__, nad); + + *len = sizeof(int) * 5 + /* 4 ints in nad_t, plus one for len */ + sizeof(struct nad_elem_st) * nad->ecur + + sizeof(struct nad_attr_st) * nad->acur + + sizeof(struct nad_ns_st) * nad->ncur + + sizeof(char) * nad->ccur; + + *buf = (char *) malloc(*len); + pos = *buf; + + * (int *) pos = *len; pos += sizeof(int); + * (int *) pos = nad->ecur; pos += sizeof(int); + * (int *) pos = nad->acur; pos += sizeof(int); + * (int *) pos = nad->ncur; pos += sizeof(int); + * (int *) pos = nad->ccur; pos += sizeof(int); + + memcpy(pos, nad->elems, sizeof(struct nad_elem_st) * nad->ecur); pos += sizeof(struct nad_elem_st) * nad->ecur; + memcpy(pos, nad->attrs, sizeof(struct nad_attr_st) * nad->acur); pos += sizeof(struct nad_attr_st) * nad->acur; + memcpy(pos, nad->nss, sizeof(struct nad_ns_st) * nad->ncur); pos += sizeof(struct nad_ns_st) * nad->ncur; + memcpy(pos, nad->cdata, sizeof(char) * nad->ccur); +} + +nad_t nad_deserialize(nad_cache_t cache, const char *buf) { + nad_t nad = nad_new(cache); + const char *pos = buf + sizeof(int); /* skip len */ + + _nad_ptr_check(__func__, nad); + + nad->ecur = * (int *) pos; pos += sizeof(int); + nad->acur = * (int *) pos; pos += sizeof(int); + nad->ncur = * (int *) pos; pos += sizeof(int); + nad->ccur = * (int *) pos; pos += sizeof(int); + nad->elen = nad->ecur; + nad->alen = nad->acur; + nad->nlen = nad->ncur; + nad->clen = nad->ccur; + + if(nad->ecur > 0) + { + nad->elems = (struct nad_elem_st *) malloc(sizeof(struct nad_elem_st) * nad->ecur); + memcpy(nad->elems, pos, sizeof(struct nad_elem_st) * nad->ecur); + pos += sizeof(struct nad_elem_st) * nad->ecur; + } + + if(nad->acur > 0) + { + nad->attrs = (struct nad_attr_st *) malloc(sizeof(struct nad_attr_st) * nad->acur); + memcpy(nad->attrs, pos, sizeof(struct nad_attr_st) * nad->acur); + pos += sizeof(struct nad_attr_st) * nad->acur; + } + + if(nad->ncur > 0) + { + nad->nss = (struct nad_ns_st *) malloc(sizeof(struct nad_ns_st) * nad->ncur); + memcpy(nad->nss, pos, sizeof(struct nad_ns_st) * nad->ncur); + pos += sizeof(struct nad_ns_st) * nad->ncur; + } + + if(nad->ccur > 0) + { + nad->cdata = (char *) malloc(sizeof(char) * nad->ccur); + memcpy(nad->cdata, pos, sizeof(char) * nad->ccur); + } + + return nad; +} + +#ifdef HAVE_EXPAT + +/** parse a buffer into a nad */ + +struct build_data { + nad_t nad; + int depth; +}; + +static void _nad_parse_element_start(void *arg, const char *name, const char **atts) { + struct build_data *bd = (struct build_data *) arg; + char buf[1024]; + char *uri, *elem, *prefix; + const char **attr; + int ns; + + /* make a copy */ + strncpy(buf, name, 1024); + buf[1023] = '\0'; + + /* expat gives us: + prefixed namespaced elem: uri|elem|prefix + default namespaced elem: uri|elem + un-namespaced elem: elem + */ + + /* extract all the bits */ + uri = buf; + elem = strchr(uri, '|'); + if(elem != NULL) { + *elem = '\0'; + elem++; + prefix = strchr(elem, '|'); + if(prefix != NULL) { + *prefix = '\0'; + prefix++; + } + ns = nad_add_namespace(bd->nad, uri, prefix); + } else { + /* un-namespaced, just take it as-is */ + uri = NULL; + elem = buf; + prefix = NULL; + ns = -1; + } + + /* add it */ + nad_append_elem(bd->nad, ns, elem, bd->depth); + + /* now the attributes, one at a time */ + attr = atts; + while(attr[0] != NULL) { + + /* make a copy */ + strncpy(buf, attr[0], 1024); + buf[1023] = '\0'; + + /* extract all the bits */ + uri = buf; + elem = strchr(uri, '|'); + if(elem != NULL) { + *elem = '\0'; + elem++; + prefix = strchr(elem, '|'); + if(prefix != NULL) { + *prefix = '\0'; + prefix++; + } + ns = nad_add_namespace(bd->nad, uri, prefix); + } else { + /* un-namespaced, just take it as-is */ + uri = NULL; + elem = buf; + prefix = NULL; + ns = -1; + } + + /* add it */ + nad_append_attr(bd->nad, ns, elem, (char *) attr[1]); + + attr += 2; + } + + bd->depth++; +} + +static void _nad_parse_element_end(void *arg, const char *name) { + struct build_data *bd = (struct build_data *) arg; + + bd->depth--; +} + +static void _nad_parse_cdata(void *arg, const char *str, int len) { + struct build_data *bd = (struct build_data *) arg; + + /* go */ + nad_append_cdata(bd->nad, (char *) str, len, bd->depth); +} + +static void _nad_parse_namespace_start(void *arg, const char *prefix, const char *uri) { + struct build_data *bd = (struct build_data *) arg; + + nad_add_namespace(bd->nad, (char *) uri, (char *) prefix); +} + +nad_t nad_parse(nad_cache_t cache, const char *buf, int len) { + struct build_data bd; + XML_Parser p; + + if(len == 0) + len = strlen(buf); + + p = XML_ParserCreateNS(NULL, '|'); + if(p == NULL) + return NULL; + + bd.nad = nad_new(cache); + bd.depth = 0; + + XML_SetUserData(p, (void *) &bd); + XML_SetElementHandler(p, _nad_parse_element_start, _nad_parse_element_end); + XML_SetCharacterDataHandler(p, _nad_parse_cdata); + XML_SetStartNamespaceDeclHandler(p, _nad_parse_namespace_start); + + if(!XML_Parse(p, buf, len, 1)) { + XML_ParserFree(p); + nad_free(bd.nad); + return NULL; + } + + XML_ParserFree(p); + + if(bd.depth != 0) + return NULL; + + return bd.nad; +} + +#endif diff --git a/src/perlmods/JSON.pm b/src/perlmods/JSON.pm new file mode 100644 index 0000000..18454b5 --- /dev/null +++ b/src/perlmods/JSON.pm @@ -0,0 +1,235 @@ +package JSON::number; +sub new { + my $class = shift; + my $x = shift || $class; + return bless \$x => __PACKAGE__; +} +use overload ( '""' => \&toString ); +use overload ( '0+' => sub { ${$_[0]} } ); + +sub toString { defined($_[1]) ? ${$_[1]} : ${$_[0]} } + +package JSON::bool::true; +sub new { return bless {} => __PACKAGE__ } +use overload ( '""' => \&toString ); +use overload ( 'bool' => sub { 1 } ); +use overload ( '0+' => sub { 1 } ); + +sub toString { 'true' } + +package JSON::bool::false; +sub new { return bless {} => __PACKAGE__ } +use overload ( '""' => \&toString ); +use overload ( 'bool' => sub { 0 } ); +use overload ( '0+' => sub { 0 } ); + +sub toString { 'false' } + +package JSON; +use vars qw/%_class_map/; + +sub register_class_hint { + my $class = shift; + my %args = @_; + + $_class_map{$args{hint}} = \%args; + $_class_map{$args{name}} = \%args; +} + +sub JSON2perl { + my ($class, $json) = @_; + $json ||= $class; + + $json =~ s/\/\/.+$//gmo; # remove C++ comments + $json =~ s/(? '; + next; + } elsif ($element eq 'true') { + $output .= 'bless( {}, "JSON::bool::true")'; + next; + } elsif ($element eq 'false') { + $output .= 'bless( {}, "JSON::bool::false")'; + next; + } + + $output .= $element; + } + + return eval $output; +} + +sub perl2JSON { + my ($class, $perl) = @_; + $perl ||= $class; + + my $output = ''; + if (!defined($perl)) { + $output = 'null'; + } elsif (ref($perl) and ref($perl) =~ /^JSON/) { + $output .= $perl; + } elsif ( ref($perl) && exists($_class_map{ref($perl)}) ) { + $output .= '/*--S '.$_class_map{ref($perl)}{hint}.'--*/'; + if (lc($_class_map{ref($perl)}{type}) eq 'hash') { + my %hash = %$perl; + $output .= perl2JSON(\%hash); + } elsif (lc($_class_map{ref($perl)}{type}) eq 'array') { + my @array = @$perl; + $output .= perl2JSON(\@array); + } + $output .= '/*--E '.$_class_map{ref($perl)}{hint}.'--*/'; + } elsif (ref($perl) and ref($perl) =~ /HASH/) { + $output .= '{'; + my $c = 0; + for my $key (sort keys %$perl) { + $output .= ',' if ($c); + + $output .= perl2JSON($key).':'.perl2JSON($$perl{$key}); + $c++; + } + $output .= '}'; + } elsif (ref($perl) and ref($perl) =~ /ARRAY/) { + $output .= '['; + my $c = 0; + for my $part (@$perl) { + $output .= ',' if ($c); + + $output .= perl2JSON($part); + $c++; + } + $output .= ']'; + } else { + $perl =~ s/\\/\\\\/sgo; + $perl =~ s/"/\\"/sgo; + $perl =~ s/\t/\\t/sgo; + $perl =~ s/\f/\\f/sgo; + $perl =~ s/\r/\\r/sgo; + $perl =~ s/\n/\\n/sgo; + $output = '"'.$perl.'"'; + } + + return $output; +} + +my $depth = 0; +sub perl2prettyJSON { + my ($class, $perl, $nospace) = @_; + $perl ||= $class; + + my $output = ''; + if (!defined($perl)) { + $output = 'null'; + } elsif (ref($perl) and ref($perl) =~ /^JSON/) { + $output .= $perl; + } elsif ( ref($perl) && exists($_class_map{ref($perl)}) ) { + $depth++; + $output .= "\n"; + $output .= " "x$depth; + $output .= '/*--S '.$_class_map{ref($perl)}{hint}."--*/ "; + if (lc($_class_map{ref($perl)}{type}) eq 'hash') { + my %hash = %$perl; + $output .= perl2prettyJSON(\%hash,undef,1); + } elsif (lc($_class_map{ref($perl)}{type}) eq 'array') { + my @array = @$perl; + $output .= perl2prettyJSON(\@array,undef,1); + } + #$output .= " "x$depth; + $output .= ' /*--E '.$_class_map{ref($perl)}{hint}.'--*/'; + $depth--; + } elsif (ref($perl) and ref($perl) =~ /HASH/) { + #$depth++; + $output .= " "x$depth unless ($nospace); + $output .= "{\n"; + my $c = 0; + $depth++; + for my $key (sort keys %$perl) { + $output .= ",\n" if ($c); + + $output .= perl2prettyJSON($key)." : ".perl2prettyJSON($$perl{$key}, undef, 1); + $c++; + } + $depth--; + $output .= "\n"; + $output .= " "x$depth; + $output .= '}'; + #$depth--; + } elsif (ref($perl) and ref($perl) =~ /ARRAY/) { + #$depth++; + $output .= " "x$depth unless ($nospace); + $output .= "[\n"; + my $c = 0; + $depth++; + for my $part (@$perl) { + $output .= ",\n" if ($c); + + $output .= perl2prettyJSON($part); + $c++; + } + $depth--; + $output .= "\n"; + $output .= " "x$depth; + $output .= "]"; + #$depth--; + } else { + $perl =~ s/\\/\\\\/sgo; + $perl =~ s/"/\\"/sgo; + $perl =~ s/\t/\\t/sgo; + $perl =~ s/\f/\\f/sgo; + $perl =~ s/\r/\\r/sgo; + $perl =~ s/\n/\\n/sgo; + $output .= " "x$depth unless($nospace); + $output .= '"'.$perl.'"'; + } + + return $output; +} + +1; diff --git a/src/perlmods/OpenSRF.pm b/src/perlmods/OpenSRF.pm new file mode 100644 index 0000000..18d157a --- /dev/null +++ b/src/perlmods/OpenSRF.pm @@ -0,0 +1,75 @@ +package OpenILS; +use strict; +use Error; +use vars qw/$VERSION $AUTOLOAD/; +$VERSION = do { my @r=(q$Revision$=~/\d+/g); sprintf "%d."."%02d"x$#r,@r }; + +=head1 OpenILS + +=cut + +=head2 Overview + + Top level class for OpenILS perl modules. + +=cut + +# Exception base classes +#use Exception::Class +# ( OpenILSException => { fields => [ 'errno' ] }); +#push @Exception::Class::ISA, 'Error'; + +=head3 AUTOLOAD() + + Traps methods calls for methods that have not been defined so they + don't propogate up the class hierarchy. + +=cut +sub AUTOLOAD { + my $self = shift; + my $type = ref($self) || $self; + my $name = $AUTOLOAD; + my $otype = ref $self; + + my ($package, $filename, $line) = caller; + my ($package1, $filename1, $line1) = caller(1); + my ($package2, $filename2, $line2) = caller(2); + my ($package3, $filename3, $line3) = caller(3); + my ($package4, $filename4, $line4) = caller(4); + my ($package5, $filename5, $line5) = caller(5); + $name =~ s/.*://; # strip fully-qualified portion + warn <<" WARN"; +**** +** ${name}() isn't there. Please create me somewhere (like in $type)! +** Error at $package ($filename), line $line +** Call Stack (5 deep): +** $package1 ($filename1), line $line1 +** $package2 ($filename2), line $line2 +** $package3 ($filename3), line $line3 +** $package4 ($filename4), line $line4 +** $package5 ($filename5), line $line5 +** Object type was $otype +**** + WARN +} + + + +=head3 alert_abstract() + + This method is called by abstract methods to ensure that + the process dies when an undefined abstract method is called + +=cut +sub alert_abstract() { + my $c = shift; + my $class = ref( $c ) || $c; + my ($file, $line, $method) = (caller(1))[1..3]; + die " * Call to abstract method $method at $file, line $line"; +} + +sub class { + return scalar(caller); +} + +1; diff --git a/src/perlmods/OpenSRF/AppSession.pm b/src/perlmods/OpenSRF/AppSession.pm new file mode 100644 index 0000000..0fa3d45 --- /dev/null +++ b/src/perlmods/OpenSRF/AppSession.pm @@ -0,0 +1,827 @@ +package OpenSRF::AppSession; +use OpenSRF::DOM; +use OpenSRF::DOM::Element::userAuth; +use OpenSRF::DomainObject::oilsMessage; +use OpenSRF::DomainObject::oilsMethod; +use OpenSRF::DomainObject::oilsResponse qw/:status/; +use OpenSRF::Transport::PeerHandle; +use OpenSRF::Utils::Config; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::EX; +use OpenSRF; +use Exporter; +use base qw/Exporter OpenSRF/; +use Time::HiRes qw( time usleep ); +use warnings; +use strict; + +our @EXPORT_OK = qw/CONNECTING INIT_CONNECTED CONNECTED DISCONNECTED CLIENT SERVER/; +our %EXPORT_TAGS = ( state => [ qw/CONNECTING INIT_CONNECTED CONNECTED DISCONNECTED/ ], + endpoint => [ qw/CLIENT SERVER/ ], +); + +my $logger = "OpenSRF::Utils::Logger"; + +our %_CACHE; +our @_CLIENT_CACHE; +our @_RESEND_QUEUE; + +sub kill_client_session_cache { + for my $session ( @_CLIENT_CACHE ) { + $session->kill_me; + } +} + +sub CONNECTING { return 3 }; +sub INIT_CONNECTED { return 4 }; +sub CONNECTED { return 1 }; +sub DISCONNECTED { return 2 }; + +sub CLIENT { return 2 }; +sub SERVER { return 1 }; + +sub find { + return undef unless (defined $_[1]); + return $_CACHE{$_[1]} if (exists($_CACHE{$_[1]})); +} + +sub find_client { + my( $self, $app ) = @_; + $logger->debug( "Client Cache contains: " .scalar(@_CLIENT_CACHE), INTERNAL ); + my ($client) = grep { $_->[0] eq $app and $_->[1] == 1 } @_CLIENT_CACHE; + $client->[1] = 0; + return $client->[2]; +} + +sub transport_connected { + my $self = shift; + if( ! exists $self->{peer_handle} || + ! $self->{peer_handle} ) { + return 0; + } + return $self->{peer_handle}->tcp_connected(); +} + +# ---------------------------------------------------------------------------- +# Clears the transport buffers +# call this if you are not through with the sesssion, but you want +# to have a clean slate. You shouldn't have to call this if +# you are correctly 'recv'ing all of the data from a request. +# however, if you don't want all of the data, this will +# slough off any excess +# * * Note: This will delete data for all sessions using this transport +# handle. For example, all client sessions use the same handle. +# ---------------------------------------------------------------------------- +sub buffer_reset { + + my $self = shift; + if( ! exists $self->{peer_handle} || ! $self->{peer_handle} ) { + return 0; + } + $self->{peer_handle}->buffer_reset(); +} + + +sub client_cache { + my $self = shift; + push @_CLIENT_CACHE, [ $self->app, 1, $self ]; +} + +# when any incoming data is received, this method is called. +sub server_build { + my $class = shift; + $class = ref($class) || $class; + + my $sess_id = shift; + my $remote_id = shift; + my $service = shift; + + return undef unless ($sess_id and $remote_id and $service); + + my $conf = OpenSRF::Utils::Config->current; + + if( ! $conf ) { + OpenSRF::EX::Config->throw( "No suitable config found" ); + } + + if ( my $thingy = $class->find($sess_id) ) { + $thingy->remote_id( $remote_id ); + $logger->debug( "AppSession returning existing session $sess_id", DEBUG ); + return $thingy; + } else { + $logger->debug( "AppSession building new server session $sess_id", DEBUG ); + } + + if( $service eq "client" ) { + #throw OpenSRF::EX::PANIC ("Attempting to build a client session as a server" . + # " Session ID [$sess_id], remote_id [$remote_id]"); + $logger->debug("Attempting to build a client session as ". + "a server Session ID [$sess_id], remote_id [$remote_id]", ERROR ); + } + + + my $max_requests = $conf->$service->max_requests; + $logger->debug( "Max Requests for $service is $max_requests", INTERNAL );# if $max_requests; + + $logger->transport( "AppSession creating new session: $sess_id", INTERNAL ); + + my $self = bless { recv_queue => [], + request_queue => [], + requests => 0, + endpoint => SERVER, + state => CONNECTING, + session_id => $sess_id, + remote_id => $remote_id, + peer_handle => OpenSRF::Transport::PeerHandle->retrieve($service), + max_requests => $max_requests, + session_threadTrace => 0, + service => $service, + } => $class; + + return $_CACHE{$sess_id} = $self; +} + +sub service { return shift()->{service}; } + +sub continue_request { + my $self = shift; + $self->{'requests'}++; + return $self->{'requests'} <= $self->{'max_requests'} ? 1 : 0; +} + +sub last_sent_payload { + my( $self, $payload ) = @_; + if( $payload ) { + return $self->{'last_sent_payload'} = $payload; + } + return $self->{'last_sent_payload'}; +} + +sub last_sent_type { + my( $self, $type ) = @_; + if( $type ) { + return $self->{'last_sent_type'} = $type; + } + return $self->{'last_sent_type'}; +} + +# When we're a client and we want to connect to a remote service +# create( $app, username => $user, secret => $passwd ); +# OR +# create( $app, sysname => $user, secret => $shared_secret ); +sub create { + my $class = shift; + $class = ref($class) || $class; + + my $app = shift; + my %auth_args = @_; + + + unless ( $app && + exists($auth_args{secret}) && + ( exists($auth_args{username}) || + exists($auth_args{sysname}) ) ) { + throw OpenSRF::EX::User ( 'Insufficient authentication information for session creation'); + } + + if( my $thingy = OpenSRF::AppSession->find_client( $app ) ) { + $logger->debug( "AppSession returning existing client session for $app", DEBUG ); + return $thingy; + } else { + $logger->debug( "AppSession creating new client session for $app", DEBUG ); + } + + + + my $auth = OpenSRF::DOM::Element::userAuth->new( %auth_args ); + + my $conf = OpenSRF::Utils::Config->current; + + if( ! $conf ) { + OpenSRF::EX::Config->throw( "No suitable config found" ); + } + + my $sess_id = time . rand( $$ ); + while ( $class->find($sess_id) ) { + $sess_id = time . rand( $$ ); + } + + my $r_id = $conf->$app->transport_target || + die("No remote id for $app!"); + + my $self = bless { app_name => $app, + client_auth => $auth, + #recv_queue => [], + request_queue => [], + endpoint => CLIENT, + state => DISCONNECTED,#since we're init'ing + session_id => $sess_id, + remote_id => $r_id, + orig_remote_id => $r_id, + # peer_handle => OpenSRF::Transport::PeerHandle->retrieve($app), + peer_handle => OpenSRF::Transport::PeerHandle->retrieve("client"), + session_threadTrace => 0, + } => $class; + + $self->client_cache(); + $_CACHE{$sess_id} = $self; + return $self->find_client( $app ); +} + +sub app { + return shift()->{app_name}; +} + +sub reset { + my $self = shift; + $self->remote_id($$self{orig_remote_id}); +} + +# 'connect' can be used as a constructor if called as a class method, +# or used to connect a session that has disconnectd if called against +# an existing session that seems to be disconnected, or was just built +# using 'create' above. + +# connect( $app, username => $user, secret => $passwd ); +# OR +# connect( $app, sysname => $user, secret => $shared_secret ); + +# --- Returns undef if the connect attempt times out. +# --- Returns the OpenSRF::EX object if one is returned by the server +# --- Returns self if connected +sub connect { + my $self = shift; + my $class = ref($self) || $self; + + return $self if ( ref( $self ) and $self->state && $self->state == CONNECTED ); + + my $app = shift; + + $self = $class->create($app, @_) if (!ref($self)); + return undef unless ($self); + + + $self->reset; + $self->state(CONNECTING); + $self->send('CONNECT', ""); + + my $time_remaining = OpenSRF::Utils::Config->current->client->connect_timeout; + + while ( $self->state != CONNECTED and $time_remaining > 0 ) { + my $starttime = time; + $self->queue_wait($time_remaining); + my $endtime = time; + $time_remaining -= ($endtime - $starttime); + } + + return undef unless($self->state == CONNECTED); + + return $self; +} + +sub finish { + my $self = shift; + #$self->disconnect if ($self->endpoint == CLIENT); + for my $ses ( @_CLIENT_CACHE ) { + if ($ses->[2]->session_id eq $self->session_id) { + $ses->[1] = 1; + } + } +} + +sub kill_me { + my $self = shift; + if( ! $self->session_id ) { return 0; } + $self->disconnect; + $logger->transport( "AppSession killing self: " . $self->session_id(), DEBUG ); + my @a; + for my $ses ( @_CLIENT_CACHE ) { + push @a, $ses + if ($ses->[2]->session_id ne $self->session_id); + } + @_CLIENT_CACHE = @a; + delete $_CACHE{$self->session_id}; + delete($$self{$_}) for (keys %$self); +} + +sub disconnect { + my $self = shift; + unless( $self->state == DISCONNECTED ) { + $self->send('DISCONNECT', "") if ($self->endpoint == CLIENT);; + $self->state( DISCONNECTED ); + } + $self->reset; +} + +sub request { + my $self = shift; + my $meth = shift; + + my $method; + if (!ref $meth) { + $method = new OpenSRF::DomainObject::oilsMethod ( method => $meth ); + } else { + $method = $meth; + } + + $method->params( @_ ); + + $self->send('REQUEST',$method); +} + + +sub send { + my $self = shift; + my @payload_list = @_; # this is a Domain Object + + $logger->debug( "In send", INTERNAL ); + + my $tT; + + if( @payload_list % 2 ) { $tT = pop @payload_list; } + + if( ! @payload_list ) { + $logger->debug( "payload_list param is incomplete in AppSession::send()", ERROR ); + return undef; + } + + my $doc = OpenSRF::DOM->createDocument(); + + $logger->debug( "In send2", INTERNAL ); + + my $disconnect = 0; + my $connecting = 0; + + while( @payload_list ) { + + my ($msg_type, $payload) = ( shift(@payload_list), shift(@payload_list) ); + + if ($msg_type eq 'DISCONNECT' ) { + $disconnect++; + if( $self->state == DISCONNECTED) { + next; + } + } + + if( $msg_type eq "CONNECT" ) { $connecting++; } + + + if( $payload ) { + $logger->debug( "Payload is ".$payload->toString, INTERNAL ); + } + + + my $msg = OpenSRF::DomainObject::oilsMessage->new(); + $logger->debug( "AppSession after creating oilsMessage $msg", INTERNAL ); + + $msg->type($msg_type); + $logger->debug( "AppSession after adding type" . $msg->toString(), INTERNAL ); + + $msg->userAuth($self->client_auth) if ($self->endpoint == CLIENT && $msg_type eq 'CONNECT'); + + no warnings; + $msg->threadTrace( $tT || int($self->session_threadTrace) || int($self->last_threadTrace) ); + use warnings; + + if ($msg->type eq 'REQUEST') { + if ( !defined($tT) || $self->last_threadTrace != $tT ) { + $msg->update_threadTrace; + $self->session_threadTrace( $msg->threadTrace ); + $tT = $self->session_threadTrace; + OpenSRF::AppRequest->new($self, $payload); + } + } + + $msg->protocol(1); + $msg->payload($payload) if $payload; + + $doc->documentElement->appendChild( $msg ); + + + $logger->debug( "AppSession sending ".$msg->type." to ".$self->remote_id. + " with threadTrace [".$msg->threadTrace."]", INFO ); + + } + + if ($self->endpoint == CLIENT and ! $disconnect) { + $self->queue_wait(0); + unless ($self->state == CONNECTED || ($self->state == CONNECTING && $connecting )) { + my $v = $self->connect(); + if( ! $v ) { + $logger->debug( "Unable to connect to remote service in AppSession::send()", ERROR ); + return undef; + } + if( $v and $v->class->isa( "OpenSRF::EX" ) ) { + return $v; + } + } + } + + $logger->debug( "AppSession sending doc: " . $doc->toString(), INTERNAL ); + + + $self->{peer_handle}->send( + to => $self->remote_id, + thread => $self->session_id, + body => $doc->toString ); + + return $self->app_request( $tT ); +} + +sub app_request { + my $self = shift; + my $tT = shift; + + return undef unless (defined $tT); + my ($req) = grep { $_->threadTrace == $tT } @{ $self->{request_queue} }; + + return $req; +} + +sub remove_app_request { + my $self = shift; + my $req = shift; + + my @list = grep { $_->threadTrace != $req->threadTrace } @{ $self->{request_queue} }; + + $self->{request_queue} = \@list; +} + +sub endpoint { + return $_[0]->{endpoint}; +} + + +sub session_id { + my $self = shift; + return $self->{session_id}; +} + +sub push_queue { + my $self = shift; + my $resp = shift; + my $req = $self->app_request($resp->[1]); + return $req->push_queue( $resp->[0] ) if ($req); + push @{ $self->{recv_queue} }, $resp->[0]; +} + +sub last_threadTrace { + my $self = shift; + my $new_last_threadTrace = shift; + + my $old_last_threadTrace = $self->{last_threadTrace}; + if (defined $new_last_threadTrace) { + $self->{last_threadTrace} = $new_last_threadTrace; + return $new_last_threadTrace unless ($old_last_threadTrace); + } + + return $old_last_threadTrace; +} + +sub session_threadTrace { + my $self = shift; + my $new_last_threadTrace = shift; + + my $old_last_threadTrace = $self->{session_threadTrace}; + if (defined $new_last_threadTrace) { + $self->{session_threadTrace} = $new_last_threadTrace; + return $new_last_threadTrace unless ($old_last_threadTrace); + } + + return $old_last_threadTrace; +} + +sub last_message_type { + my $self = shift; + my $new_last_message_type = shift; + + my $old_last_message_type = $self->{last_message_type}; + if (defined $new_last_message_type) { + $self->{last_message_type} = $new_last_message_type; + return $new_last_message_type unless ($old_last_message_type); + } + + return $old_last_message_type; +} + +sub last_message_protocol { + my $self = shift; + my $new_last_message_protocol = shift; + + my $old_last_message_protocol = $self->{last_message_protocol}; + if (defined $new_last_message_protocol) { + $self->{last_message_protocol} = $new_last_message_protocol; + return $new_last_message_protocol unless ($old_last_message_protocol); + } + + return $old_last_message_protocol; +} + +sub remote_id { + my $self = shift; + my $new_remote_id = shift; + + my $old_remote_id = $self->{remote_id}; + if (defined $new_remote_id) { + $self->{remote_id} = $new_remote_id; + return $new_remote_id unless ($old_remote_id); + } + + return $old_remote_id; +} + +sub client_auth { + my $self = shift; + my $new_ua = shift; + + my $old_ua = $self->{client_auth}; + if (defined $new_ua) { + $self->{client_auth} = $new_ua; + return $new_ua unless ($old_ua); + } + + return $old_ua->cloneNode(1); +} + +sub state { + my $self = shift; + my $new_state = shift; + + my $old_state = $self->{state}; + if (defined $new_state) { + $self->{state} = $new_state; + return $new_state unless ($old_state); + } + + return $old_state; +} + +sub DESTROY { + my $self = shift; + delete $$self{$_} for keys %$self; + return undef; +} + +sub recv { + my $self = shift; + my @proto_args = @_; + my %args; + + if ( @proto_args ) { + if ( !(@proto_args % 2) ) { + %args = @proto_args; + } elsif (@proto_args == 1) { + %args = ( timeout => @proto_args ); + } + } + + #$logger->debug( ref($self). " recv_queue before wait: " . $self->_print_queue(), INTERNAL ); + + if( exists( $args{timeout} ) ) { + $args{timeout} = int($args{timeout}); + } else { + $args{timeout} = 10; + } + + #$args{timeout} = 0 if ($self->complete); + + $logger->debug( ref($self) ."->recv with timeout " . $args{timeout}, INTERNAL ); + + $args{count} ||= 1; + + my $avail = @{ $self->{recv_queue} }; + my $time_remaining = $args{timeout}; + + while ( $avail < $args{count} and $time_remaining > 0 ) { + last if $self->complete; + my $starttime = time; + $self->queue_wait($time_remaining); + my $endtime = time; + $time_remaining -= ($endtime - $starttime); + $avail = @{ $self->{recv_queue} }; + } + + #$logger->debug( ref($self)." queue after wait: " . $self->_print_queue(), INTERNAL ); + + my @list; + while ( my $msg = shift @{ $self->{recv_queue} } ) { + push @list, $msg; + last if (scalar(@list) >= $args{count}); + } + +# $self->{recv_queue} = [@unlist, @{ $self->{recv_queue} }]; + $logger->debug( "Number of matched responses: " . @list, DEBUG ); + + $self->queue_wait(0); # check for statuses + + return $list[0] unless (wantarray); + return @list; +} + +sub push_resend { + my $self = shift; + push @OpenSRF::AppSession::_RESEND_QUEUE, @_; +} + +sub flush_resend { + my $self = shift; + $logger->debug( "Resending..." . @_RESEND_QUEUE, DEBUG ); + while ( my $req = shift @OpenSRF::AppSession::_RESEND_QUEUE ) { + $req->resend; + } +} + + +sub queue_wait { + my $self = shift; + if( ! $self->{peer_handle} ) { return 0; } + my $timeout = shift || 0; + $logger->debug( "Calling queue_wait($timeout)" , DEBUG ); + $logger->debug( "Timestamp before process($timeout) : " . $logger->format_time(), INTERNAL ); + my $o = $self->{peer_handle}->process($timeout); + $logger->debug( "Timestamp after process($timeout) : " . $logger->format_time(), INTERNAL ); + $self->flush_resend; + return $o; +} + +sub _print_queue { + my( $self ) = @_; + my $string = ""; + foreach my $msg ( @{$self->{recv_queue}} ) { + $string = $string . $msg->toString(1) . "\n"; + } + return $string; +} + +sub status { + my $self = shift; + $self->send( 'STATUS', @_ ); +} + +#------------------------------------------------------------------------------- + +package OpenSRF::AppRequest; +use base qw/OpenSRF::AppSession/; +use OpenSRF::Utils::Logger qw/:level/; +use OpenSRF::DomainObject::oilsResponse qw/:status/; + +sub new { + my $class = shift; + $class = ref($class) || $class; + + my $session = shift; + my $threadTrace = $session->session_threadTrace || $session->last_threadTrace; + my $payload = shift; + + my $self = { session => $session, + threadTrace => $threadTrace, + payload => $payload, + complete => 0, + recv_queue => [], + }; + + bless $self => $class; + + push @{ $self->session->{request_queue} }, $self; + + return $self; +} + +sub queue_size { + my $size = @{$_[0]->{recv_queue}}; + return $size; +} + +sub send { + shift()->session->send(@_); +} + +sub finish { + my $self = shift; + $self->session->remove_app_request($self); + delete($$self{$_}) for (keys %$self); +} + +sub session { + return shift()->{session}; +} + +sub complete { + my $self = shift; + my $complete = shift; + return $self->{complete} if ($self->{complete}); + if (defined $complete) { + $self->{complete} = $complete; + } else { + $self->session->queue_wait(0); + } + return $self->{complete}; +} + +sub wait_complete { + my $self = shift; + my $timeout = shift || 1; + my $time_remaining = $timeout; + + while ( ! $self->complete and $time_remaining > 0 ) { + my $starttime = time; + $self->queue_wait($time_remaining); + my $endtime = time; + $time_remaining -= ($endtime - $starttime); + } + + return $self->complete; +} + +sub threadTrace { + return shift()->{threadTrace}; +} + +sub push_queue { + my $self = shift; + my $resp = shift; + push @{ $self->{recv_queue} }, $resp; + OpenSRF::Utils::Logger->debug( "AppRequest pushed ".$resp->toString(), INTERNAL ); +} + +sub queue_wait { + my $self = shift; + OpenSRF::Utils::Logger->debug( "Calling queue_wait(@_)", DEBUG ); + return $self->session->queue_wait(@_) +} + +sub payload { return shift()->{payload}; } + +sub resend { + my $self = shift; + OpenSRF::Utils::Logger->debug( + "I'm resending the request for threadTrace ". $self->threadTrace, DEBUG); + OpenSRF::Utils::Logger->debug($self->payload->toString,INTERNAL); + return $self->session->send('REQUEST', $self->payload, $self->threadTrace ); +} + +sub status { + my $self = shift; + my $msg = shift; + $self->session->send( 'STATUS',$msg, $self->threadTrace ); +} + +sub respond { + my $self = shift; + my $msg = shift; + + my $response; + if (!ref($msg) || ($msg->can('getAttribute') && $msg->getAttribute('name') !~ /oilsResult/)) { + $response = new OpenSRF::DomainObject::oilsResult; + $response->content($msg); + } else { + $response = $msg; + } + + $self->session->send('RESULT', $response, $self->threadTrace); +} + +sub respond_complete { + my $self = shift; + my $msg = shift; + + my $response; + if (!ref($msg) || ($msg->can('getAttribute') && $msg->getAttribute('name') !~ /oilsResult/)) { + $response = new OpenSRF::DomainObject::oilsResult; + $response->content($msg); + } else { + $response = $msg; + } + + my $stat = OpenSRF::DomainObject::oilsConnectStatus->new( + statusCode => STATUS_COMPLETE(), + status => 'Request Complete' ); + + $self->session->send( 'RESULT' => $response, 'STATUS' => $stat, $self->threadTrace); + +} + + +1; + + +__END__ + +[client] +interval:connect_timeout = 2 seconds + +[servers] +subsection:StorageServer = Storage_config + +[Storage_config] +#transport = xmlrpc +#transport_target = http://open-ils.org/RPC2/services/Storage + +transport = jabber +transport_target = Storage@open-ils.org/SERVICE_RECEIVER +method_class = OpenSRF::App::Storage::PGStore; + + + + + diff --git a/src/perlmods/OpenSRF/Application.pm b/src/perlmods/OpenSRF/Application.pm new file mode 100644 index 0000000..13f80dc --- /dev/null +++ b/src/perlmods/OpenSRF/Application.pm @@ -0,0 +1,222 @@ +package OpenSRF::Application; +use base qw/OpenSRF/; +use OpenSRF::AppSession; +use OpenSRF::DomainObject::oilsMethod; +use OpenSRF::DomainObject::oilsResponse qw/:status/; +use OpenSRF::Utils::Logger qw/:level/; +use Time::HiRes qw/time/; +use vars qw/$_app $log/; +use OpenSRF::EX qw/:try/; +use strict; +use warnings; + +$log = 'OpenSRF::Utils::Logger'; + +our $in_request = 0; +our @pending_requests; + +sub application_implementation { + my $self = shift; + my $app = shift; + + if (defined $app) { + $_app = $app; + eval "use $_app;"; + if( $@ ) { + $log->error( "Error loading application_implementation: $app -> $@", ERROR); + } + + } + + return $_app; +} + +sub handler { + my ($self, $session, $app_msg) = @_; + + $log->debug( "In Application::handler()", DEBUG ); + + my $app = $self->application_implementation; + + if( $app ) { + $log->debug( "Application is $app", DEBUG); + } + $log->debug( "Message is ".$app_msg->toString(1), INTERNAL); + + + if ($session->last_message_type eq 'REQUEST') { + $log->debug( "We got a REQUEST: ". $app_msg->method, INFO ); + + my $method_name = $app_msg->method; + $log->debug( " * Looking up $method_name inside $app", DEBUG); + + my $method_proto = $session->last_message_protocol; + $log->debug( " * Method API Level [$method_proto]", DEBUG); + + my $coderef = $app->method_lookup( $method_name, $method_proto ); + + unless ($coderef) { + $session->status( OpenSRF::DomainObject::oilsMethodException->new() ); + return 1; + } + + #if ( $session->client_auth->username || $session->client_auth->userid ) { + # unless ( $coderef->is_action ) { + # $session->status( + # OpenSRF::DomainObject::oilsMethodException->new( + # statusCode => STATUS_NOTALLOWED(), + # status => "User cannot use [$method_name]" ) ); + # return 1; + # } + #} + + + $log->debug( " (we got coderef $coderef", DEBUG); + + unless ($session->continue_request) { + $session->status( + OpenSRF::DomainObject::oilsConnectStatus->new( + statusCode => STATUS_REDIRECTED(), + status => 'Disconnect on max requests' ) ); + $session->kill_me; + return 1; + } + + if (ref $coderef) { + my @args = $app_msg->params; + my $appreq = OpenSRF::AppRequest->new( $session ); + + $log->debug( "in_request = $in_request : [" . $appreq->threadTrace."]", DEBUG ); + if( $in_request ) { + $log->debug( "Pushing onto pending requests: " . $appreq->threadTrace, DEBUG ); + push @pending_requests, [ $appreq, \@args, $coderef ]; + return 1; + } + + + $in_request++; + + $log->debug( "Executing coderef for {$method_name -> ".join(', ', @args)."}", INTERNAL ); + + my $resp; + try { + my $start = time(); + $resp = $coderef->run( $appreq, @args); + my $time = sprintf '%.3f', time() - $start; + $log->debug( "Method duration for {$method_name -> ".join(', ', @args)."}: ". $time, DEBUG ); + if( ref( $resp ) ) { + $log->debug( "Calling respond_complete: ". $resp->toString(), INTERNAL ); + $appreq->respond_complete( $resp ); + } else { + $appreq->status( OpenSRF::DomainObject::oilsConnectStatus->new( + statusCode => STATUS_COMPLETE(), + status => 'Request Complete' ) ); + } + } catch Error with { + my $e = shift; + $e = $e->{-text} || $e->message if (ref $e); + my $sess_id = $session->session_id; + $session->status( + OpenSRF::DomainObject::oilsMethodException->new( + statusCode => STATUS_INTERNALSERVERERROR(), + status => " *** Call to [$method_name] failed for session ". + "[$sess_id], thread trace [".$appreq->threadTrace."]:\n".$e + ) + ); + }; + + + + # ---------------------------------------------- + + + # XXX may need this later + # $_->[1] = 1 for (@OpenSRF::AppSession::_CLIENT_CACHE); + + $in_request--; + + $log->debug( "Pending Requests: " . scalar(@pending_requests), INTERNAL ); + + # cycle through queued requests + while( my $aref = shift @pending_requests ) { + $in_request++; + my $resp; + try { + my $start = time; + my $response = $aref->[2]->run( $aref->[0], @{$aref->[1]} ); + my $time = sprintf '%.3f', time - $start; + $log->debug( "Method duration for {[".$aref->[2]->name." -> ".join(', ',@{$aref->[1]}).'}: '.$time, DEBUG ); + + $appreq = $aref->[0]; + if( ref( $response ) ) { + $log->debug( "Calling respond_complete: ". $response->toString(), INTERNAL ); + $appreq->respond_complete( $response ); + } else { + $appreq->status( OpenSRF::DomainObject::oilsConnectStatus->new( + statusCode => STATUS_COMPLETE(), + status => 'Request Complete' ) ); + } + $log->debug( "Executed: " . $appreq->threadTrace, DEBUG ); + } catch Error with { + my $e = shift; + $session->status( + OpenSRF::DomainObject::oilsMethodException->new( + statusCode => STATUS_INTERNALSERVERERROR(), + status => "Call to [".$aref->[2]->name."] faild: ".$e->{-text} + ) + ); + }; + $in_request--; + } + + return 1; + } + my $res = OpenSRF::DomainObject::oilsMethodException->new; + $session->send('ERROR', $res); + $session->kill_me; + return 1; + + } else { + $log->debug( "Pushing ". $app_msg->toString ." onto queue", INTERNAL ); + $session->push_queue([ $app_msg, $session->last_threadTrace ]); + } + + $session->last_message_type(''); + $session->last_message_protocol(''); + + return 1; +} + +sub method_lookup { + my $self = shift; + my $method = shift; + my $proto = shift; + + my $class = ref($self) || $self; + + $log->debug("Looking up [$method] in [$self]", INTERNAL); + + my $obj = bless {} => $self; + if (my $coderef = $self->can("${method}_${proto}")) { + $$obj{code} = $coderef; + $$obj{name} = "${method}_${proto}"; + return $obj; + } + return undef; +} + +sub run { + my $self = shift; + $self->{code}->(@_); +} + +sub is_action { + my $self = shift; + if (my $can = $self->can($self->{name} . '_action')) { + return $can->(); + } + return 0; +} + + +1; diff --git a/src/perlmods/OpenSRF/Application/Client.pm b/src/perlmods/OpenSRF/Application/Client.pm new file mode 100644 index 0000000..fc0665a --- /dev/null +++ b/src/perlmods/OpenSRF/Application/Client.pm @@ -0,0 +1,6 @@ +package OpenSRF::App::Client; +use base 'OpenSRF'; +use OpenSRF::Utils::Logger qw/:level/; + + +1; diff --git a/src/perlmods/OpenSRF/Application/Demo/Math.pm b/src/perlmods/OpenSRF/Application/Demo/Math.pm new file mode 100644 index 0000000..be5b46a --- /dev/null +++ b/src/perlmods/OpenSRF/Application/Demo/Math.pm @@ -0,0 +1,125 @@ +package OpenSRF::Application::Demo::Math; +use base qw/OpenSRF::Application/; +use OpenSRF::Application; +use OpenSRF::Utils::Logger qw/:level/; +use OpenSRF::DomainObject::oilsResponse; +use OpenSRF::EX qw/:try/; +use strict; +use warnings; + +sub DESTROY{} + +our $log = 'OpenSRF::Utils::Logger'; + +#sub method_lookup { +# +# my( $class, $method_name, $method_proto ) = @_; +# +# if( $method_name eq "add" ) { +# return \&add; +# } + +# if( $method_name eq "sub" ) { +# return \⊂ +# } +# +# if( $method_name eq "mult" ) { +# return \&mult; +# } +# +# if( $method_name eq "div" ) { +# return \÷ +# } + +# return undef; +# +#} + +sub send_request { + + my $method_name = shift; + my @params = @_; + + $log->debug( "Creating a client environment", DEBUG ); + my $session = OpenSRF::AppSession->create( + "dbmath", sysname => 'math', secret => '12345' ); + + $log->debug( "Sending request to math server", INTERNAL ); + + my $method = OpenSRF::DomainObject::oilsMethod->new( method => $method_name ); + + $method->params( @params ); + + + my $req; + my $resp; + + + try { + + for my $nn (0..1) { + my $vv = $session->connect(); + if($vv) { last; } + if( $nn and !$vv ) { + throw OpenSRF::EX::CRITICAL ("DBMath connect attempt timed out"); + } + } + + $req = $session->request( $method ); + $resp = $req->recv(10); + + } catch OpenSRF::DomainObject::oilsAuthException with { + my $e = shift; + $e->throw(); + }; + + if ( defined($resp) and $resp and $resp->class->isa('OpenSRF::DomainObject::oilsResult') ){ + + $log->debug( "Math server returned " . $resp->toString(1), INTERNAL ); + $req->finish; + $session->finish; + return $resp; + + } else { + + if( $resp ) { $log->debug( "Math received \n".$resp->toString(), ERROR ); } + else{ $log->debug( "Math received empty value", ERROR ); } + $req->finish; + $session->finish; + throw OpenSRF::EX::ERROR ("Did not receive expected data from MathDB"); + + } +} + + +sub add_1_action { 1 }; +sub add_1 { + + my $client = shift; + my @args = @_; + return send_request( "add", @args ); +} + +sub sub_1_action { 1 }; +sub sub_1 { + my $client = shift; + my @args = @_; + return send_request( "sub", @args ); +} + +sub mult_1_action { 1 }; +sub mult_1 { + my $client = shift; + my @args = @_; + return send_request( "mult", @args ); +} + +sub div_1_action { 1 }; +sub div_1 { + my $client = shift; + my @args = @_; + return send_request( "div", @args ); +} + + +1; diff --git a/src/perlmods/OpenSRF/Application/Demo/MathDB.pm b/src/perlmods/OpenSRF/Application/Demo/MathDB.pm new file mode 100644 index 0000000..0b40256 --- /dev/null +++ b/src/perlmods/OpenSRF/Application/Demo/MathDB.pm @@ -0,0 +1,94 @@ +package OpenSRF::Application::Demo::MathDB; +use base qw/OpenSRF::Application/; +use OpenSRF::Application; +use OpenSRF::DomainObject::oilsResponse qw/:status/; +use OpenSRF::DomainObject::oilsPrimitive; +use OpenSRF::Utils::Logger qw/:level/; +use strict; +use warnings; +sub DESTROY{} +our $log = 'OpenSRF::Utils::Logger'; + +#sub method_lookup { +# +# my( $class, $method_name, $method_proto ) = @_; +# +# if( $method_name eq "add" ) { +# return \&add; +# } +# +# if( $method_name eq "sub" ) { +# return \⊂ +# } +# +# if( $method_name eq "mult" ) { +# return \&mult; +# } +# +# if( $method_name eq "div" ) { +# return \÷ +# } +# +# return undef; +# +#} + +sub add_1 { + my $client = shift; + my @args = @_; + $log->debug("Adding @args", INTERNAL); + $log->debug("AppRequest is $client", INTERNAL); + my $n1 = shift; my $n2 = shift; + $n1 =~ s/\s+//; $n2 =~ s/\s+//; + my $a = $n1 + $n2; + my $result = new OpenSRF::DomainObject::oilsResult; + $result->content( OpenSRF::DomainObject::oilsScalar->new($a) ); + return $result; + $client->respond($result); + return 1; +} +sub sub_1 { + my $client = shift; + my @args = @_; + $log->debug("Subbing @args", INTERNAL); + $log->debug("AppRequest is $client", INTERNAL); + my $n1 = shift; my $n2 = shift; + $n1 =~ s/\s+//; $n2 =~ s/\s+//; + my $a = $n1 - $n2; + my $result = new OpenSRF::DomainObject::oilsResult; + $result->content( OpenSRF::DomainObject::oilsScalar->new($a) ); + return $result; + $client->respond($result); + return 1; +} + +sub mult_1 { + my $client = shift; + my @args = @_; + $log->debug("Multiplying @args", INTERNAL); + $log->debug("AppRequest is $client", INTERNAL); + my $n1 = shift; my $n2 = shift; + $n1 =~ s/\s+//; $n2 =~ s/\s+//; + my $a = $n1 * $n2; + my $result = new OpenSRF::DomainObject::oilsResult; + $result->content( OpenSRF::DomainObject::oilsScalar->new($a) ); +# $client->respond($result); + return $result; +} + +sub div_1 { + my $client = shift; + my @args = @_; + $log->debug("Dividing @args", INTERNAL); + $log->debug("AppRequest is $client", INTERNAL); + my $n1 = shift; my $n2 = shift; + $n1 =~ s/\s+//; $n2 =~ s/\s+//; + my $a = $n1 / $n2; + my $result = new OpenSRF::DomainObject::oilsResult; + $result->content( OpenSRF::DomainObject::oilsScalar->new($a) ); + return $result; + $client->respond($result); + return 1; +} + +1; diff --git a/src/perlmods/OpenSRF/DOM.pm b/src/perlmods/OpenSRF/DOM.pm new file mode 100644 index 0000000..7e23d9c --- /dev/null +++ b/src/perlmods/OpenSRF/DOM.pm @@ -0,0 +1,289 @@ +use XML::LibXML; +use OpenSRF::Utils::Logger qw(:level); + +package XML::LibXML::Element; +use OpenSRF::EX; + +sub AUTOLOAD { + my $self = shift; + (my $name = $AUTOLOAD) =~ s/.*://; # strip fully-qualified portion + + ### Check for recursion + my $calling_method = (caller(1))[3]; + my @info = caller(1); + + if( @info ) { + if ($info[0] =~ /AUTOLOAD/) { @info = caller(2); } + } + unless( @info ) { @info = caller(); } + if( $calling_method and $calling_method eq "XML::LibXML::Element::AUTOLOAD" ) { + throw OpenSRF::EX::PANIC ( "RECURSION! Caller [ @info ] | Object [ ".ref($self)." ]\n ** Trying to call $name", ERROR ); + } + ### Check for recursion + + #OpenSRF::Utils::Logger->debug( "Autoloading method for DOM: $AUTOLOAD on ".$self->toString, INTERNAL ); + + my $new_node = OpenSRF::DOM::upcast($self); + OpenSRF::Utils::Logger->debug( "Autoloaded to: ".ref($new_node), INTERNAL ); + + return $new_node->$name(@_); +} + + + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM; +use base qw/XML::LibXML OpenSRF/; + +our %_NAMESPACE_MAP = ( + 'http://open-ils.org/xml/namespaces/oils_v1' => 'oils', +); + +our $_one_true_parser; + +sub new { + my $self = shift; + return $_one_true_parser if (defined $_one_true_parser); + $_one_true_parser = $self->SUPER::new(@_); + $_one_true_parser->keep_blanks(0); + $XML::LibXML::skipXMLDeclaration = 0; + return $_one_true_parser = $self->SUPER::new(@_); +} + +sub createDocument { + my $self = shift; + + # DOM API: createDocument(namespaceURI, qualifiedName, doctype?) + my $doc = XML::LibXML::Document->new("1.0", "UTF-8"); + my $el = $doc->createElement('root'); + + $el->setNamespace('http://open-ils.org/xml/namespaces/oils_v1', 'oils', 1); + $doc->setDocumentElement($el); + + return $doc; +} + +my %_loaded_classes; +sub upcast { + my $node = shift; + return undef unless $node; + + my ($ns,$tag) = split ':' => $node->nodeName; + + return $node unless ($ns eq 'oils'); + + my $class = "OpenSRF::DOM::Element::$tag"; + unless (exists $_loaded_classes{$class}) { + eval "use $class;"; + $_loaded_classes{$class} = 1; + } + if ($@) { + OpenSRF::Utils::Logger->error("Couldn't use $class! $@"); + } + + #OpenSRF::Utils::Logger->debug("Upcasting ".$node->toString." to $class", INTERNAL); + + return bless $node => $class; +} + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Node; +use base 'XML::LibXML::Node'; + +sub new { + my $class = shift; + return bless $class->SUPER::new(@_) => $class; +} + +sub childNodes { + my $self = shift; + my @children = $self->_childNodes(); + return wantarray ? @children : OpenSRF::DOM::NodeList->new( @children ); +} + +sub attributes { + my $self = shift; + my @attr = $self->_attributes(); + return wantarray ? @attr : OpenSRF::DOM::NamedNodeMap->new( @attr ); +} + +sub findnodes { + my ($node, $xpath) = @_; + my @nodes = $node->_findnodes($xpath); + if (wantarray) { + return @nodes; + } else { + return OpenSRF::DOM::NodeList->new(@nodes); + } +} + + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::NamedNodeMap; +use base 'XML::LibXML::NamedNodeMap'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::NodeList; +use base 'XML::LibXML::NodeList'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Element; +use base 'XML::LibXML::Element'; + +sub new { + my $class = shift; + + # magically create the element (tag) name, or build a blank element + (my $name = $class) =~ s/^OpenSRF::DOM::Element:://; + if ($name) { + $name = "oils:$name"; + } else { + undef $name; + } + + my $self = $class->SUPER::new($name); + + my %attrs = @_; + for my $aname (keys %attrs) { + $self->setAttribute($aname, $attrs{$aname}); + } + + return $self; +} + +sub getElementsByTagName { + my ( $node , $name ) = @_; + my $xpath = "descendant::$name"; + my @nodes = $node->_findnodes($xpath); + return wantarray ? @nodes : OpenSRF::DOM::NodeList->new(@nodes); +} + +sub getElementsByTagNameNS { + my ( $node, $nsURI, $name ) = @_; + my $xpath = "descendant::*[local-name()='$name' and namespace-uri()='$nsURI']"; + my @nodes = $node->_findnodes($xpath); + return wantarray ? @nodes : OpenSRF::DOM::NodeList->new(@nodes); +} + +sub getElementsByLocalName { + my ( $node,$name ) = @_; + my $xpath = "descendant::*[local-name()='$name']"; + my @nodes = $node->_findnodes($xpath); + return wantarray ? @nodes : OpenSRF::DOM::NodeList->new(@nodes); +} + +sub getChildrenByLocalName { + my ( $node,$name ) = @_; + my $xpath = "./*[local-name()='$name']"; + my @nodes = $node->_findnodes($xpath); + return @nodes; +} + +sub getChildrenByTagName { + my ( $node, $name ) = @_; + my @nodes = grep { $_->nodeName eq $name } $node->childNodes(); + return @nodes; +} + +sub getChildrenByTagNameNS { + my ( $node, $nsURI, $name ) = @_; + my $xpath = "*[local-name()='$name' and namespace-uri()='$nsURI']"; + my @nodes = $node->_findnodes($xpath); + return @nodes; +} + +sub appendWellBalancedChunk { + my ( $self, $chunk ) = @_; + + my $local_parser = OpenSRF::DOM->new(); + my $frag = $local_parser->parse_xml_chunk( $chunk ); + + $self->appendChild( $frag ); +} + +package OpenSRF::DOM::Element::root; +use base 'OpenSRF::DOM::Element'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Text; +use base 'XML::LibXML::Text'; + + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Comment; +use base 'XML::LibXML::Comment'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::CDATASection; +use base 'XML::LibXML::CDATASection'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Document; +use base 'XML::LibXML::Document'; + +sub empty { + my $self = shift; + return undef unless (ref($self)); + $self->documentElement->removeChild($_) for $self->documentElement->childNodes; + return $self; +} + +sub new { + my $class = shift; + return bless $class->SUPER::new(@_) => $class; +} + +sub getElementsByTagName { + my ( $doc , $name ) = @_; + my $xpath = "descendant-or-self::node()/$name"; + my @nodes = $doc->_findnodes($xpath); + return wantarray ? @nodes : OpenSRF::DOM::NodeList->new(@nodes); +} + +sub getElementsByTagNameNS { + my ( $doc, $nsURI, $name ) = @_; + my $xpath = "descendant-or-self::*[local-name()='$name' and namespace-uri()='$nsURI']"; + my @nodes = $doc->_findnodes($xpath); + return wantarray ? @nodes : OpenSRF::DOM::NodeList->new(@nodes); +} + +sub getElementsByLocalName { + my ( $doc,$name ) = @_; + my $xpath = "descendant-or-self::*[local-name()='$name']"; + my @nodes = $doc->_findnodes($xpath); + return wantarray ? @nodes : OpenSRF::DOM::NodeList->new(@nodes); +} + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::DocumentFragment; +use base 'XML::LibXML::DocumentFragment'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Attr; +use base 'XML::LibXML::Attr'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Dtd; +use base 'XML::LibXML::Dtd'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::PI; +use base 'XML::LibXML::PI'; + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Namespace; +use base 'XML::LibXML::Namespace'; + +sub isEqualNode { + my ( $self, $ref ) = @_; + if ( $ref->isa("XML::LibXML::Namespace") ) { + return $self->_isEqual($ref); + } + return 0; +} + +#-------------------------------------------------------------------------------- +package OpenSRF::DOM::Schema; +use base 'XML::LibXML::Schema'; + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/domainObject.pm b/src/perlmods/OpenSRF/DOM/Element/domainObject.pm new file mode 100644 index 0000000..4d9fd2a --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/domainObject.pm @@ -0,0 +1,114 @@ +package OpenSRF::DOM::Element::domainObject; +use strict; use warnings; +use base 'OpenSRF::DOM::Element'; +use OpenSRF::DOM; +use OpenSRF::DOM::Element::domainObjectAttr; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::EX; +use Carp; +#use OpenSRF::DomainObject::oilsPrimitive; +#use OpenSRF::DomainObject::oilsResponse; +use vars qw($AUTOLOAD); + +sub AUTOLOAD { + my $self = shift; + (my $name = $AUTOLOAD) =~ s/.*://; # strip fully-qualified portion + + return class($self) if ($name eq 'class'); + if ($self->can($name)) { + return $self->$name(@_); + } + + if (1) { + ### Check for recursion + my $calling_method = (caller(1))[3]; + my @info = caller(1); + + if( @info ) { + if ($info[0] =~ /AUTOLOAD/) { @info = caller(2); } + } + unless( @info ) { @info = caller(); } + + if( $calling_method and $calling_method eq "OpenSRF::DOM::Element::domainObject::AUTOLOAD" ) { + warn Carp::cluck; + throw OpenSRF::EX::PANIC ( "RECURSION! Caller [ @info[0..2] ] | Object [ ".ref($self)." ]\n ** Trying to call $name", ERROR ); + } + ### Check for recursion + } + + my @args = @_; + my $meth = class($self).'::'.$name; + + try { + return $self->$meth(@args); + } catch Error with { + my $e = shift; + OpenSRF::Utils::Logger->error( $@ . $e); + die $@; + }; + + + my $node = OpenSRF::DOM::Element::domainObject::upcast($self); + OpenSRF::Utils::Logger->debug( "Autoloaded to: ".ref($node), INTERNAL ); + + return $node->$name(@_); +} + +sub downcast { + my $obj = shift; + return bless $obj => 'XML::LibXML::Element'; +} + +sub upcast { + my $self = shift; + return bless $self => class($self); +} + +sub new { + my $class = shift; + my $type = shift; + my $obj = $class->SUPER::new( name => $type ); + while (@_) { + my ($attr,$val) = (shift,shift); + last unless ($attr and $val); + $obj->addAttr( $attr, $val ); + #$obj->appendChild( OpenSRF::DOM::Element::domainObjectAttr->new($attr, $val) ); + } + return $obj; +} + +sub class { + my $self = shift; + return 'OpenSRF::DomainObject::'.$self->getAttribute('name'); +} + +sub base_type { + my $self = shift; + return $self->getAttribute('name'); +} + +sub addAttr { + my $self = shift; + $self->appendChild( $_ ) for OpenSRF::DOM::Element::domainObjectAttr->new(@_); + return $self; +} + +sub attrNode { + my $self = shift; + my $type = shift; + return (grep { $_->getAttribute('name') eq $type } $self->getChildrenByTagName("oils:domainObjectAttr"))[0]; +} + +sub attrHash { + my $self = shift; + my %attrs = map { ( $_->getAttribute('name') => $_->getAttribute('value') ) } $self->getChildrenByTagName('oils:domainObjectAttr'); + + return \%attrs; +} + +sub attrValue { + my $self = shift; + return $self->attrHash->{shift}; +} + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/domainObjectAttr.pm b/src/perlmods/OpenSRF/DOM/Element/domainObjectAttr.pm new file mode 100644 index 0000000..fbdc28e --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/domainObjectAttr.pm @@ -0,0 +1,15 @@ +package OpenSRF::DOM::Element::domainObjectAttr; +use base 'OpenSRF::DOM::Element'; + +sub new { + my $class = shift; + my @nodes; + while (@_) { + my ($name,$val) = (shift,shift); + push @nodes, $class->SUPER::new(name => $name, value => $val); + } + return @nodes if (wantarray); + return $nodes[0]; +} + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/domainObjectCollection.pm b/src/perlmods/OpenSRF/DOM/Element/domainObjectCollection.pm new file mode 100644 index 0000000..264c435 --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/domainObjectCollection.pm @@ -0,0 +1,116 @@ +package OpenSRF::DOM::Element::domainObjectCollection; +use base 'OpenSRF::DOM::Element'; +use OpenSRF::DOM::Element::domainObjectAttr; +use OpenSRF::EX; + +sub AUTOLOAD { + my $self = CORE::shift; + (my $name = $AUTOLOAD) =~ s/.*://; # strip fully-qualified portion + + return class($self) if ($name eq 'class'); + + my @args = @_; + my $meth = class($self).'::'.$name; + + ### Check for recursion + my $calling_method = (caller(1))[3]; + my @info = caller(1); + + if( @info ) { + if ($info[0] =~ /AUTOLOAD/) { @info = caller(2); } + } + unless( @info ) { @info = caller(); } + if( $calling_method and $calling_method eq "OpenSRF::DOM::Element::domainObjectCollection::AUTOLOAD" ) { + throw OpenSRF::EX::PANIC ( "RECURSION! Caller [ @info ] | Object [ ".ref($self)." ]\n ** Trying to call $name", ERROR ); + } + ### Check for recursion + + try { + return $self->$meth(@args);; + } catch Error with { + my $e = shift; + OpenSRF::Utils::Logger->error( $@ . $e); + die $@; + }; + + return upcast($self)->$name(@_); +} + +sub downcast { + my $obj = CORE::shift; + return bless $obj => 'XML::LibXML::Element'; +} + +sub upcast { + my $self = CORE::shift; + return bless $self => class($self); +} + +sub new { + my $class = CORE::shift; + my $type = CORE::shift; + my $obj = $class->SUPER::new( name => $type ); + while ( my $val = shift) { + throw OpenSRF::EX::NotADomainObject + if (ref $val and $val->nodeName !~ /^oils:domainObject/o); + $obj->appendChild( $val ); + } + return $obj; +} + +sub class { + my $self = shift; + return 'OpenSRF::DomainObjectCollection::'.$self->getAttribute('name'); +} + +sub base_type { + my $self = shift; + return $self->getAttribute('name'); +} + +sub pop { + my $self = CORE::shift; + return $self->removeChild( $self->lastChild )->upcast; +} + +sub push { + my $self = CORE::shift; + my @args = @_; + for my $node (@args) { + #throw OpenSRF::EX::NotADomainObject ( "$_ must be a oils:domainOjbect*, it's a ".$_->nodeName ) + # unless ($_->nodeName =~ /^oils:domainObject/o); + + unless ($node->nodeName =~ /^oils:domainObject/o) { + $node = OpenSRF::DomainObject::oilsScalar->new($node); + } + + $self->appendChild( $node ); + } +} + +sub shift { + my $self = CORE::shift; + return $self->removeChild( $self->firstChild )->upcast; +} + +sub unshift { + my $self = CORE::shift; + my @args = @_; + for (reverse @args) { + throw OpenSRF::EX::NotADomainObject + unless ($_->nodeName =~ /^oils:domainObject/o); + $self->insertBefore( $_, $self->firstChild ); + } +} + +sub first { + my $self = CORE::shift; + return $self->firstChild->upcast; +} + +sub list { + my $self = CORE::shift; + return map {(bless($_ => 'OpenSRF::DomainObject::'.$_->getAttribute('name')))} $self->childNodes; +} + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/param.pm b/src/perlmods/OpenSRF/DOM/Element/param.pm new file mode 100644 index 0000000..44993ef --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/param.pm @@ -0,0 +1,4 @@ +package OpenSRF::DOM::Element::param; +use base 'OpenSRF::DOM::Element'; + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/params.pm b/src/perlmods/OpenSRF/DOM/Element/params.pm new file mode 100644 index 0000000..ee3755a --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/params.pm @@ -0,0 +1,4 @@ +package OpenSRF::DOM::Element::params; +use base 'OpenSRF::DOM::Element'; + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/searchCriteria.pm b/src/perlmods/OpenSRF/DOM/Element/searchCriteria.pm new file mode 100644 index 0000000..c8b1eb3 --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/searchCriteria.pm @@ -0,0 +1,48 @@ +package OpenSRF::DOM::Element::searchCriteria; +use base 'OpenSRF::DOM::Element'; +use OpenSRF::DOM; +use OpenSRF::DOM::Element::searchCriterium; + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + if (@_ == 3 and !ref($_[1])) { + my @crit = @_; + @_ = ('AND', \@crit); + } + + my ($joiner,@crits) = @_; + + unless (@crits) { + push @crits, $joiner; + $joiner = 'AND'; + } + + my $collection = $class->SUPER::new(joiner => $joiner); + + for my $crit (@crits) { + if (ref($crit) and ref($crit) =~ /ARRAY/) { + if (ref($$crit[1])) { + $collection->appendChild( $class->new(@$crit) ); + } else { + $collection->appendChild( OpenSRF::DOM::Element::searchCriterium->new( @$crit ) ); + } + } else { + $collection->appendChild($crit); + } + } + return $collection; +} + +sub toSQL { + my $self = shift; + + my @parts = (); + for my $kid ($self->childNodes) { + push @parts, $kid->toSQL; + } + return '(' . join(' '.$self->getAttribute('joiner').' ', @parts) . ')'; +} + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/searchCriterium.pm b/src/perlmods/OpenSRF/DOM/Element/searchCriterium.pm new file mode 100644 index 0000000..9bfa3b4 --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/searchCriterium.pm @@ -0,0 +1,116 @@ +package OpenSRF::DOM::Element::searchCriterium; +use base 'OpenSRF::DOM::Element'; +use OpenSRF::DOM::Element::searchTargetValue; + +sub new { + my $class = shift; + my @nodes; + my @args = @_; + while (scalar(@args)) { + my ($attr,$cmp,$val) = (shift(@args),shift(@args),shift(@args),shift(@args)); + push @nodes, $class->SUPER::new(property => $attr, comparison => $cmp); + $nodes[-1]->appendChild( $_ ) for OpenSRF::DOM::Element::searchTargetValue->new($val); + } + return @nodes if (wantarray); + return $nodes[0]; +} + +sub toSQL { + my $self = shift; + my %args = @_; + + my $column = $self->getAttribute('property'); + my $cmp = lc($self->getAttribute('comparison')); + + my $value = [ map { ($_->getAttribute('value')) } $self->childNodes ]; + + if ($cmp eq '=' || $cmp eq '==' || $cmp eq 'eq' || $cmp eq 'is') { + $cmp = '='; + if (!$value || lc($value) eq 'null') { + $cmp = 'IS'; + $value = 'NULL'; + } + ($value = $value->[0]) =~ s/'/''/gmso; + $value =~ s/\\/\\\\/gmso; + $value = "'$value'" unless $args{no_quote}; + } elsif ($cmp eq '>' || $cmp eq 'gt' || $cmp eq 'over' || $cmp eq 'after') { + $cmp = '>'; + if (!$value || lc($value) eq 'null') { + warn "Can not compare NULL"; + } + ($value = $value->[0]) =~ s/'/''/gmso; + $value =~ s/\\/\\\\/gmso; + $value = "'$value'" unless $args{no_quote}; + } elsif ($cmp eq '<' || $cmp eq 'lt' || $cmp eq 'under' || $cmp eq 'before') { + $cmp = '<'; + if (!$value || lc($value) eq 'null') { + warn "Can not compare NULL"; + } + ($value = $value->[0]) =~ s/'/''/gmso; + $value =~ s/\\/\\\\/gmso; + $value = "'$value'" unless $args{no_quote}; + } elsif ($cmp eq '!=' || $cmp eq '<>' || $cmp eq 'ne' || $cmp eq 'not') { + $cmp = '<>'; + if (!$value || lc($value) eq 'null') { + $cmp = 'IS NOT'; + $value = 'NULL'; + } + ($value = $value->[0]) =~ s/'/''/gmso; + $value =~ s/\\/\\\\/gmso; + $value = "'$value'" unless $args{no_quote}; + } elsif (lc($cmp) eq 'fts' || $cmp eq 'tsearch' || $cmp eq '@@') { + $cmp = '@@'; + if (!$value || lc($value) eq 'null') { + warn "Can not compare NULL"; + } + ($value = $value->[0]) =~ s/'/''/gmso; + $value =~ s/\\/\\\\/gmso; + $value = "to_tsquery('$value')"; + } elsif ($cmp eq 'like' || $cmp eq 'contains' || $cmp eq 'has') { + $cmp = 'LIKE'; + if (!$value || lc($value) eq 'null') { + warn "Can not compare NULL"; + } + ($value = $value->[0]) =~ s/'/''/gmso; + $value =~ s/\\/\\\\/gmso; + $value =~ s/%/\\%/gmso; + $value =~ s/_/\\_/gmso; + $value = "'\%$value\%'"; + } elsif ($cmp eq 'between') { + $cmp = 'BETWEEN'; + if (!ref($value) || lc($value) eq 'null') { + warn "Can not check 'betweenness' of NULL"; + } + if (ref($value) and ref($value) =~ /ARRAY/o) { + $value = "(\$text\$$$value[0]\$text\$ AND \$text\$$$value[-1]\$text\$)"; + } + } elsif ($cmp eq 'not between') { + $cmp = 'NOT BETWEEN'; + if (!ref($value) || lc($value) eq 'null') { + warn "Can not check 'betweenness' of NULL"; + } + if (ref($value) and ref($value) =~ /ARRAY/o) { + $value = "(\$text\$$$value[0]\$text\$ AND \$text\$$$value[-1]\$text\$)"; + } + } elsif ($cmp eq 'in' || $cmp eq 'any' || $cmp eq 'some') { + $cmp = 'IN'; + if (!ref($value) || lc($value) eq 'null') { + warn "Can not check 'inness' of NULL"; + } + if (ref($value) and ref($value) =~ /ARRAY/o) { + $value = '($text$'.join('$text$,$text$', @$value).'$text$)'; + } + } elsif ($cmp eq 'not in' || $cmp eq 'not any' || $cmp eq 'not some') { + $cmp = 'NOT IN'; + if (!ref($value) || lc($value) eq 'null') { + warn "Can not check 'inness' of NULL"; + } + if (ref($value) and ref($value) =~ /ARRAY/o) { + $value = '($text$'.join('$text$,$text$', @$value).'$text$)'; + } + } + + return join(' ', ($column, $cmp, $value)); +} + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/searchTargetValue.pm b/src/perlmods/OpenSRF/DOM/Element/searchTargetValue.pm new file mode 100644 index 0000000..7e6d3c8 --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/searchTargetValue.pm @@ -0,0 +1,23 @@ +package OpenSRF::DOM::Element::searchTargetValue; +use base 'OpenSRF::DOM::Element'; + +sub new { + my $self = shift; + my $class = ref($self) || $self; + my @args = @_; + + my @values = (); + for my $val (@args) { + next unless ($val); + if (ref($val)) { + push @values, $class->new(@$val); + } else { + push @values, $class->SUPER::new( value => $val ); + } + + } + return $values[0] if (!wantarray); + return @values; +} + +1; diff --git a/src/perlmods/OpenSRF/DOM/Element/userAuth.pm b/src/perlmods/OpenSRF/DOM/Element/userAuth.pm new file mode 100644 index 0000000..1072017 --- /dev/null +++ b/src/perlmods/OpenSRF/DOM/Element/userAuth.pm @@ -0,0 +1,178 @@ +package OpenSRF::DOM::Element::userAuth; +use OpenSRF::DOM; +use OpenSRF::Utils::Logger qw/:level/; +use OpenSRF::Utils::Config; +use Digest::MD5 qw/md5_hex/; +use OpenSRF::DomainObject::oilsMethod; +use OpenSRF::DomainObject::oilsResponse; +use OpenSRF::App::Auth; +use OpenSRF::EX qw/:try/; +use OpenSRF::Utils::Cache; + +use base 'OpenSRF::DOM::Element'; + +my $log = 'OpenSRF::Utils::Logger'; + +=head1 NAME + +OpenSRF::DOM::Element::userAuth + +=over 4 + +User authentication data structure for use in oilsMessage objects. + +=back + +=head1 SYNOPSIS + + use OpenSRF::DOM::Element::userAuth; + + %auth_structure = ( userid => '0123456789', secret => 'junko' ); + %auth_structure = ( username => 'miker', secret => 'junko' ); + + my $auth = OpenSRF::DOM::Element::userAuth->new( %auth_structure ); + +... + + my %server_auth = ( sysname => 'OPACServer', + secret => 'deadbeefdeadbeef' ); + + my $auth = OpenSRF::DOM::Element::userAuth->new( %server_auth ); + +=cut + +sub new { + my $class = shift; + $class = ref($class) || $class; + + my %args = @_; + + $args{hashseed} ||= int( rand( $$ ) ); + + $args{secret} = md5_hex($args{secret}); + $args{secret} = md5_hex($args{hashseed}. $args{secret}); + + return $class->SUPER::new( %args ); +} + +sub username { + my $self = shift; + return $self->getAttribute('username'); +} + +sub userid { + my $self = shift; + return $self->getAttribute('userid'); +} + +sub sysname { + my $self = shift; + return $self->getAttribute('sysname'); +} + +sub secret { + my $self = shift; + return $self->getAttribute('secret'); +} + +sub hashseed { + my $self = shift; + return $self->getAttribute('hashseed'); +} + +sub authenticate { + my $self = shift; + my $session = shift; + my $u = $self->username || + $self->userid || + $self->sysname; + $log->debug("Authenticating user [$u]",INFO); + + + # We need to make sure that we are not the auth server. If we are, + # we don't want to send a request to ourselves. Instead just call + # the local auth method. + my @params = ( $u, $self->secret, $self->hashseed ); + my $res; + + # ------------------------------ + # See if we can auth with the cache first + $log->debug( "Attempting cache auth...", INTERNAL ); + my $cache = OpenSRF::Utils::Cache->current("user"); + my $value = $cache->get( $u ); + + if( $value and $value eq $self->secret ) { + $log->debug( "User $u is cached and authenticated", INTERNAL ); + return 1; + } + # ------------------------------ + + if( $session->service eq "auth" ) { + $log->debug( "We are AUTH. calling local auth", DEBUG ); + my $meth = OpenSRF::App::Auth->method_lookup('authenticate', 1); + $log->debug("Meth ref is $meth", INTERNAL); + $res = $meth->run( 1, @params ); + + } else { + $log->debug( "Calling AUTH server", DEBUG ); + $res = _request_remote_auth( $session, @params ); + } + + + if( $res and $res->class->isa('OpenSRF::DomainObject::oilsResult') and + $res->content and ($res->content->value eq "yes") ) { + + $log->debug( "User $u is authenticated", DEBUG ); + $log->debug( "Adding $u to cache", INTERNAL ); + + # Add to the cache ------------------------------ + $cache->set( $u, $self->secret ); + + return 1; + + } else { + return 0; + } + +} + +sub _request_remote_auth { + + my $server_session = shift; + my @params = @_; + + my $service = $server_session->service; + + my @server_auth = (sysname => OpenSRF::Utils::Config->current->$service->sysname, + secret => OpenSRF::Utils::Config->current->$service->secret ); + + my $session = OpenSRF::AppSession->create( "auth", @server_auth ); + + $log->debug( "Sending request to auth server", INTERNAL ); + + my $req; my $res; + + try { + + if( ! $session->connect() ) { + throw OpenSRF::EX::CRITICAL ("Cannot communicate with auth server"); + } + $req = $session->request( authenticate => @params ); + $req->wait_complete( OpenSRF::Utils::Config->current->client->connect_timeout ); + $res = $req->recv(); + + } catch OpenSRF::DomainObject::oilsAuthException with { + return 0; + + } finally { + $req->finish() if $req; + $session->finish() if $session; + }; + + return $res; + +} + + + +1; diff --git a/src/perlmods/OpenSRF/DomainObject.pm b/src/perlmods/OpenSRF/DomainObject.pm new file mode 100644 index 0000000..9e7d960 --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObject.pm @@ -0,0 +1,90 @@ +package OpenSRF::DomainObject; +use base 'OpenSRF::DOM::Element::domainObject'; +use OpenSRF::DOM; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::DomainObject::oilsPrimitive; +my $logger = "OpenSRF::Utils::Logger"; + +=head1 NAME + +OpenSRF::DomainObject + +=head1 SYNOPSIS + +OpenSRF::DomainObject is an abstract base class. It +should not be used directly. See C +for details. + +=cut + +my $tmp_doc; + +sub object_castor { + my $self = shift; + my $node = shift; + + return unless (defined $node); + + if (ref($node) eq 'HASH') { + return new OpenSRF::DomainObject::oilsHash (%$node); + } elsif (ref($node) eq 'ARRAY') { + return new OpenSRF::DomainObject::oilsArray (@$node); + } + + return $node; +} + +sub native_castor { + my $self = shift; + my $node = shift; + + return unless (defined $node); + + if ($node->nodeType == 3) { + return $node->nodeValue; + } elsif ($node->nodeName =~ /domainObject/o) { + return $node->tie_me if ($node->class->can('tie_me')); + } + return $node; +} + +sub new { + my $class = shift; + $class = ref($class) || $class; + + (my $type = $class) =~ s/^.+://o; + + $tmp_doc ||= OpenSRF::DOM->createDocument; + my $dO = OpenSRF::DOM::Element::domainObject->new( $type, @_ ); + + $tmp_doc->documentElement->appendChild($dO); + + return $dO; +} + +sub _attr_get_set { + my $self = shift; + my $part = shift; + + $logger->debug( "DomainObject:_attr_get_set: ". $self->toString, INTERNAL ); + + my $node = $self->attrNode($part); + + $logger->debug( "DomainObject:_attr_get_set " . $node->toString(), INTERNAL ) if ($node); + + + if (defined(my $new_value = shift)) { + if (defined $node) { + my $old_val = $node->getAttribute( "value" ); + $node->setAttribute(value => $new_value); + return $old_val; + } else { + $self->addAttr( $part => $new_value ); + return $new_value; + } + } elsif ( $node ) { + return $node->getAttribute( "value" ); + } +} + +1; diff --git a/src/perlmods/OpenSRF/DomainObject/oilsMessage.pm b/src/perlmods/OpenSRF/DomainObject/oilsMessage.pm new file mode 100644 index 0000000..b7343e6 --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObject/oilsMessage.pm @@ -0,0 +1,344 @@ +package OpenILS::DomainObject::oilsMessage; +use base 'OpenILS::DomainObject'; +use OpenILS::AppSession; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use OpenILS::Utils::Logger qw/:level/; +use warnings; use strict; +use OpenILS::EX qw/:try/; + +=head1 NAME + +OpenILS::DomainObject::oilsMessage + +=head1 + +use OpenILS::DomainObject::oilsMessage; + +my $msg = OpenILS::DomainObject::oilsMessage->new( type => 'CONNECT' ); + +$msg->userAuth( $userAuth_element ); + +$msg->payload( $domain_object ); + +=head1 ABSTRACT + +OpenILS::DomainObject::oilsMessage is used internally to wrap data sent +between client and server. It provides the structure needed to authenticate +session data, and also provides the logic needed to unwrap session data and +pass this information along to the Application Layer. + +=cut + +my $log = 'OpenILS::Utils::Logger'; + +=head1 METHODS + +=head2 OpenILS::DomainObject::oilsMessage->type( [$new_type] ) + +=over 4 + +Used to specify the type of message. One of +B. + +=back + +=cut + +sub type { + my $self = shift; + return $self->_attr_get_set( type => shift ); +} + +=head2 OpenILS::DomainObject::oilsMessage->protocol( [$new_protocol_number] ) + +=over 4 + +Used to specify the protocol of message. Currently, only protocol C<1> is +supported. This will be used to check that messages are well-formed, and as +a hint to the Application as to which version of a method should fulfill a +REQUEST message. + +=back + +=cut + +sub protocol { + my $self = shift; + return $self->_attr_get_set( protocol => shift ); +} + +=head2 OpenILS::DomainObject::oilsMessage->threadTrace( [$new_threadTrace] ); + +=over 4 + +Sets or gets the current message sequence identifier, or thread trace number, +for a message. Useful as a debugging aid, but that's about it. + +=back + +=cut + +sub threadTrace { + my $self = shift; + return $self->_attr_get_set( threadTrace => shift ); +} + +=head2 OpenILS::DomainObject::oilsMessage->update_threadTrace + +=over 4 + +Increments the threadTrace component of a message. This is automatic when +using the normal session processing stack. + +=back + +=cut + +sub update_threadTrace { + my $self = shift; + my $tT = $self->threadTrace; + + $tT ||= 0; + $tT++; + + $log->debug("Setting threadTrace to $tT",DEBUG); + + $self->threadTrace($tT); + + return $tT; +} + +=head2 OpenILS::DomainObject::oilsMessage->payload( [$new_payload] ) + +=over 4 + +Sets or gets the payload of a message. This should be exactly one object +of (sub)type domainObject or domainObjectCollection. + +=back + +=cut + +sub payload { + my $self = shift; + my $new_pl = shift; + + my ($payload) = $self->getChildrenByTagName('oils:domainObjectCollection') || + $self->getChildrenByTagName('oils:domainObject'); + if ($new_pl) { + $payload = $self->removeChild($payload) if ($payload); + $self->appendChild($new_pl); + return $new_pl unless ($payload); + } + + return OpenILS::DOM::upcast($payload)->upcast if ($payload); +} + +=head2 OpenILS::DomainObject::oilsMessage->userAuth( [$new_userAuth_element] ) + +=over 4 + +Sets or gets the userAuth element for this message. This is used internally by the +session object. + +=back + +=cut + +sub userAuth { + my $self = shift; + my $new_ua = shift; + + my ($ua) = $self->getChildrenByTagName('oils:userAuth'); + if ($new_ua) { + $ua = $self->removeChild($ua) if ($ua); + $self->appendChild($new_ua); + return $new_ua unless ($ua); + } + + return $ua; +} + +=head2 OpenILS::DomainObject::oilsMessage->handler( $session_id ) + +=over 4 + +Used by the message processing stack to set session state information from the current +message, and then sends control (via the payload) to the Application layer. + +=back + +=cut + +sub handler { + my $self = shift; + my $session = shift; + + my $mtype = $self->type; + my $protocol = $self->protocol || 1;; + my $tT = $self->threadTrace; + + $session->last_message_type($mtype); + $session->last_message_protocol($protocol); + $session->last_threadTrace($tT); + + $log->debug(" Received protocol => [$protocol], MType => [$mtype], ". + "from [".$session->remote_id."], threadTrace[".$self->threadTrace."]", INFO); + $log->debug("endpoint => [".$session->endpoint."]", DEBUG); + $log->debug("OpenILS::AppSession->SERVER => [".$session->SERVER()."]", DEBUG); + + $log->debug("Before ALL", DEBUG); + + my $val; + if ( $session->endpoint == $session->SERVER() ) { + $val = $self->do_server( $session, $mtype, $protocol, $tT ); + + } elsif ($session->endpoint == $session->CLIENT()) { + $val = $self->do_client( $session, $mtype, $protocol, $tT ); + } + + if( $val ) { + return OpenILS::Application->handler($session, $self->payload); + } + + return 1; + +} + + + +# handle server side message processing + +# !!! Returning 0 means that we don't want to pass ourselves up to the message layer !!! +sub do_server { + my( $self, $session, $mtype, $protocol, $tT ) = @_; + + # A Server should never receive STATUS messages. If so, we drop them. + # This is to keep STATUS's from dead client sessions from creating new server + # sessions which send mangled session exceptions to backends for messages + # that they are not aware of any more. + if( $mtype eq 'STATUS' ) { return 0; } + + + if ($mtype eq 'DISCONNECT') { + $session->state( $session->DISCONNECTED ); + $session->kill_me; + return 0; + } + + if ($session->state == $session->CONNECTING()) { + + # the transport layer thinks this is a new connection. is it? + unless ($mtype eq 'CONNECT') { + $log->error("Connection seems to be mangled: Got $mtype instead of CONNECT"); + + my $res = OpenILS::DomainObject::oilsBrokenSession->new( + status => "Connection seems to be mangled: Got $mtype instead of CONNECT", + ); + + $session->status($res); + $session->kill_me; + return 0; + + } + + #unless ($self->userAuth ) { + # $log->debug( "No Authentication information was provided with the initial packet", ERROR ); + # my $res = OpenILS::DomainObject::oilsConnectException->new( + # status => "No Authentication info was provided with initial message" ); + # $session->status($res); + # $session->kill_me; + # return 0; + #} + + #unless( $self->userAuth->authenticate( $session ) ) { + # my $res = OpenILS::DomainObject::oilsAuthException->new( + # status => "Authentication Failed for " . $self->userAuth->getAttribute('username') ); + # $session->status($res) if $res; + # $session->kill_me; + # return 0; + #} + + #$session->client_auth( $self->userAuth ); + + $log->debug("We're a server and the user is authenticated",DEBUG); + + my $res = OpenILS::DomainObject::oilsConnectStatus->new; + $session->status($res); + $session->state( $session->CONNECTED ); + + return 0; + } + + + $log->debug("Passing to Application::handler()", INFO); + $log->debug($self->toString(1), DEBUG); + + return 1; + +} + + +# Handle client side message processing. Return 1 when the the message should be pushed +# up to the application layer. return 0 otherwise. +sub do_client { + + my( $self, $session , $mtype, $protocol, $tT) = @_; + + + if ($mtype eq 'STATUS') { + + if ($self->payload->statusCode == STATUS_OK) { + $session->state($session->CONNECTED); + $log->debug("We connected successfully to ".$session->app, INFO); + return 0; + } + + if ($self->payload->statusCode == STATUS_TIMEOUT) { + $session->state( $session->DISCONNECTED ); + $session->reset; + $session->push_resend( $session->app_request($self->threadTrace) ); + $log->debug("Disconnected because of timeout", WARN); + return 0; + + } elsif ($self->payload->statusCode == STATUS_REDIRECTED) { + $session->state( $session->DISCONNECTED ); + $session->reset; + $session->push_resend( $session->app_request($self->threadTrace) ); + $log->debug("Disconnected because of redirect", WARN); + return 0; + + } elsif ($self->payload->statusCode == STATUS_EXPFAILED) { + $session->state( $session->DISCONNECTED ); + $log->debug("Disconnected because of mangled session", WARN); + $session->reset; + $session->push_resend( $session->app_request($self->threadTrace) ); + return 0; + + } elsif ($self->payload->statusCode == STATUS_CONTINUE) { + return 0; + + } elsif ($self->payload->statusCode == STATUS_COMPLETE) { + my $req = $session->app_request($self->threadTrace); + $req->complete(1) if ($req); + return 0; + } + + # add more STATUS handling code here (as 'elsif's), for Message layer status stuff + + } elsif ($session->state == $session->CONNECTING()) { + # This should be changed to check the type of response (is it a connectException?, etc.) + } + + if( $self->payload->class->isa( "OpenILS::EX" ) ) { + $self->payload->throw(); + } + + $log->debug("Passing to OpenILS::Application::handler()\n" . $self->payload->toString(1), INTERNAL); + $log->debug("oilsMessage passing to Application: " . $self->type." : ".$session->remote_id, INFO ); + + return 1; + +} + +1; diff --git a/src/perlmods/OpenSRF/DomainObject/oilsMethod.pm b/src/perlmods/OpenSRF/DomainObject/oilsMethod.pm new file mode 100644 index 0000000..6db472b --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObject/oilsMethod.pm @@ -0,0 +1,100 @@ +package OpenILS::DomainObject::oilsMethod; +use OpenILS::DOM::Element::params; +use OpenILS::DOM::Element::param; +use JSON; +use base 'OpenILS::DomainObject'; + +=head1 NAME + +OpenILS::DomainObject::oilsMethod + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsMethod; + +my $method = OpenILS::DomainObject::oilsMethod->new( method => 'search' ); + +$method->return_type( 'mods' ); + +$method->params( 'title:harry potter' ); + +$client->send( 'REQUEST', $method ); + +=head1 METHODS + +=head2 OpenILS::DomainObject::oilsMethod->method( [$new_method_name] ) + +=over 4 + +Sets or gets the method name that will be called on the server. As above, +this can be specified as a build attribute as well as added to a prebuilt +oilsMethod object. + +=back + +=cut + +sub method { + my $self = shift; + return $self->_attr_get_set( method => shift ); +} + +=head2 OpenILS::DomainObject::oilsMethod->return_type( [$new_return_type] ) + +=over 4 + +Sets or gets the return type for this method call. This can also be supplied as +a build attribute. + +This option does not require that the server return the type you request. It is +used as a suggestion when more than one return type or format is possible. + +=back + +=cut + + +sub return_type { + my $self = shift; + return $self->_attr_get_set( return_type => shift ); +} + +=head2 OpenILS::DomainObject::oilsMethod->params( [@new_params] ) + +=over 4 + +Sets or gets the parameters for this method call. Just pass in either text +parameters, or DOM nodes of any type. + +=back + +=cut + + +sub params { + my $self = shift; + my @args = @_; + + my ($old_params) = $self->getChildrenByTagName('oils:params'); + + my $params; + if (@args) { + + $self->removeChild($old_params) if ($old_params); + + my $params = OpenILS::DOM::Element::params->new; + $self->appendChild($params); + $params->appendTextNode( JSON->perl2JSON( \@args ); + + $old_params = $params unless ($old_params); + } + + if ($old_params) { + $params = JSON->JSON2perl( $old_params->textContent ); + return @$params; + } + + return @args; +} + +1; diff --git a/src/perlmods/OpenSRF/DomainObject/oilsMultiSearch.pm b/src/perlmods/OpenSRF/DomainObject/oilsMultiSearch.pm new file mode 100644 index 0000000..340b1b7 --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObject/oilsMultiSearch.pm @@ -0,0 +1,186 @@ +package OpenILS::DomainObjectCollection::oilsMultiSearch; +use OpenILS::DomainObjectCollection; +use OpenILS::DomainObject::oilsPrimitive; +use OpenILS::DomainObject::oilsSearch; +use OpenILS::DOM::Element::searchCriteria; +use OpenILS::DOM::Element::searchCriterium; +use base 'OpenILS::DomainObjectCollection::oilsHash'; + +sub new { + my $class = shift; + my %args = @_; + + $class = ref($class) || $class; + + my $self = $class->SUPER::new; + + tie my %hash, 'OpenILS::DomainObjectCollection::oilsHash', $self; + + $self->set( bind_count => 1 ); + $self->set( searches => new OpenILS::DomainObjectCollection::oilsHash ); + $self->set( relators => new OpenILS::DomainObjectCollection::oilsArray ); + $self->set( fields => new OpenILS::DomainObjectCollection::oilsArray ); + $self->set( group_by => new OpenILS::DomainObjectCollection::oilsArray ); + $self->set( order_by => new OpenILS::DomainObjectCollection::oilsArray ); + + return $self; +} + +sub add_subsearch { + my $self = shift; + my $alias = shift; + my $search = shift; + my $relator = shift; + + $search = OpenILS::DomainObject::oilsSearch->new($search) if (ref($search) eq 'ARRAY'); + + $self->searches->set( $alias => $search ); + + if ($self->searches->size > 1) { + throw OpenILS::EX::InvalidArg ('You need to pass a relator searchCriterium') + unless (defined $relator); + } + + $relator = OpenILS::DOM::Element::searchCriterium->new( @$relator ) + if (ref($relator) eq 'ARRAY'); + + $self->relators->push( $relator ) if (defined $relator); + + return $self; +} + +sub relators { + return $_[0]->_accessor('relators'); +} + +sub searches { + return $_[0]->_accessor('searches'); +} + +sub fields { + my $self = shift; + my @parts = @_; + if (@parts) { + $self->set( fields => OpenILS::DomainObjectCollection::oilsArray->new(@_) ); + } + return $self->_accessor('fields')->list; +} + +sub format { + $_[0]->set( format => $_[1] ) if (defined $_[1]); + return $_[0]->_accessor('format'); +} + +sub limit { + $_[0]->set( limit => $_[1] ) if (defined $_[1]); + return $_[0]->_accessor('limit'); +} + +sub offset { + $_[0]->set( offset => $_[1] ) if (defined $_[1]); + return $_[0]->_accessor('offset'); +} + +sub chunk_key { + $_[0]->set( chunk_key => $_[1] ) if (defined $_[1]); + return $_[0]->_accessor('chunk_key'); +} + +sub order_by { + my $self = shift; + my @parts = @_; + if (@parts) { + $self->set( order_by => OpenILS::DomainObjectCollection::oilsArray->new(@_) ); + } + return $self->_accessor('order_by')->list; +} + +sub group_by { + my $self = shift; + my @parts = @_; + if (@parts) { + $self->set( group_by => OpenILS::DomainObjectCollection::oilsArray->new(@_) ); + } + return $self->_accessor('group_by')->list; +} + +sub SQL_select_list { + my $self = shift; + + if (my $sql = $self->_accessor('sql_select_list')) { + return $sql; + } + + $self->set( sql_select_list => 'SELECT '.join(', ', $self->fields) ) if defined($self->fields); + return $self->_accessor('sql_select_list'); +} + +sub SQL_group_by { + my $self = shift; + + if (my $sql = $self->_accessor('sql_group_by')) { + return $sql; + } + + $self->set( sql_group_by => 'GROUP BY '.join(', ', $self->group_by) ) if defined($self->group_by); + return $self->_accessor('sql_group_by'); +} + +sub SQL_order_by { + my $self = shift; + + if (my $sql = $self->_accessor('sql_order_by')) { + return $sql; + } + + $self->set( sql_order_by => 'ORDER BY '.join(', ', $self->order_by) ) if defined($self->order_by); + return $self->_accessor('sql_order_by'); +} + +sub SQL_offset { + my $self = shift; + + if (my $sql = $self->_accessor('sql_offset')) { + return $sql; + } + + $self->set( sql_offset => 'OFFSET '.$self->offset ) if defined($self->offset); + return $self->_accessor('sql_offset'); +} + +sub SQL_limit { + my $self = shift; + + if (my $sql = $self->_accessor('sql_limit')) { + return $sql; + } + + $self->set( sql_limit => 'LIMIT '.$self->limit ) if defined($self->limit); + return $self->_accessor('sql_limit'); +} + +sub toSQL { + my $self = shift; + + my $SQL = $self->SQL_select_list.' FROM '; + + my @subselects; + for my $search ( $self->searches->keys ) { + push @subselects, '('.$self->searches->_accessor($search)->toSQL.') '.$search; + } + $SQL .= join(', ', @subselects).' WHERE '; + + my @relators; + for my $rel ( $self->relators->list ) { + push @relators, $rel->value->toSQL( no_quote => 1 ); + } + $SQL .= join(' AND ', @relators).' '; + $SQL .= join ' ', ($self->SQL_group_by, $self->SQL_order_by, $self->SQL_limit, $self->SQL_offset); + + return $SQL; +} + +#this is just to allow DomainObject to "upcast" nicely +package OpenILS::DomainObject::oilsMultiSearch; +use base OpenILS::DomainObjectCollection::oilsMultiSearch; +1; diff --git a/src/perlmods/OpenSRF/DomainObject/oilsPrimitive.pm b/src/perlmods/OpenSRF/DomainObject/oilsPrimitive.pm new file mode 100644 index 0000000..0f34624 --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObject/oilsPrimitive.pm @@ -0,0 +1,623 @@ +package OpenILS::DomainObject::oilsScalar; +use base 'OpenILS::DomainObject'; +use OpenILS::DomainObject; + +=head1 NAME + +OpenILS::DomainObject::oilsScalar + +=head1 SYNOPSIS + + use OpenILS::DomainObject::oilsScalar; + + my $text = OpenILS::DomainObject::oilsScalar->new( 'a string or number' ); + $text->value( 'replacement value' ); + print "$text"; # stringify + + ... + + $text->value( 1 ); + if( $text ) { # boolify + + ... + + $text->value( rand() * 1000 ); + print 10 + $text; # numify + + Or, using the TIE interface: + + my $scalar; + my $real_object = tie($scalar, 'OpenILS::DomainObject::oilsScalar', "a string to store..."); + + $scalar = "a new string"; + print $scalar . "\n"; + print $real_object->toString . "\n"; + +=head1 METHODS + +=head2 OpenILS::DomainObject::oilsScalar->value( [$new_value] ) + +=over 4 + +Sets or gets the value of the scalar. As above, this can be specified +as a build attribute as well as added to a prebuilt oilsScalar object. + +=back + +=cut + +use overload '""' => sub { return ''.$_[0]->value }; +use overload '0+' => sub { return int($_[0]->value) }; +use overload '<=>' => sub { return int($_[0]->value) <=> $_[1] }; +use overload 'bool' => sub { return 1 if ($_[0]->value); return 0 }; + +sub new { + my $class = shift; + $class = ref($class) || $class; + + my $value = shift; + + return $value + if ( defined $value and + ref $value and $value->can('base_type') and + UNIVERSAL::isa($value->class, __PACKAGE__) and + !scalar(@_) + ); + + my $self = $class->SUPER::new; + + if (ref($value) and ref($value) eq 'SCALAR') { + $self->value($$value); + tie( $$value, ref($self->upcast), $self); + } else { + $self->value($value) if (defined $value); + } + + return $self; +} + +sub TIESCALAR { + return CORE::shift()->new(@_); +} + +sub value { + my $self = shift; + my $value = shift; + + if ( defined $value ) { + $self->removeChild($_) for ($self->childNodes); + if (ref($value) && $value->isa('XML::LibXML::Node')) { + #throw OpenILS::EX::NotADomainObject + # unless ($value->nodeName =~ /^oils:domainObject/o); + $self->appendChild($value); + } elsif (defined $value) { + $self->appendText( ''.$value ); + } + + return $value + } else { + $value = $self->firstChild; + if ($value) { + if ($value->nodeType == 3) { + return $value->textContent; + } else { + return $value; + } + } + return undef; + } +} + +sub FETCH { $_[0]->value } +sub STORE { $_[0]->value($_[1]) } + +package OpenILS::DomainObject::oilsPair; +use base 'OpenILS::DomainObject::oilsScalar'; + +=head1 NAME + +OpenILS::DomainObject::oilsPair + +=head1 SYNOPSIS + + use OpenILS::DomainObject::oilsPair; + + my $pair = OpenILS::DomainObject::oilsPair->new( 'key_for_pair' => 'a string or number' ); + + $pair->key( 'replacement key' ); + $pair->value( 'replacement value' ); + + print "$pair"; # stringify 'value' + + ... + + $pair->value( 1 ); + + if( $pair ) { # boolify + + ... + + $pair->value( rand() * 1000 ); + + print 10 + $pair; # numify 'value' + +=head1 ABSTRACT + +This class impliments a "named pair" object. This is the basis for +hash-type domain objects. + +=head1 METHODS + +=head2 OpenILS::DomainObject::oilsPair->value( [$new_value] ) + +=over 4 + +Sets or gets the value of the pair. As above, this can be specified +as a build attribute as well as added to a prebuilt oilsPair object. + +=back + +=head2 OpenILS::DomainObject::oilsPair->key( [$new_key] ) + +=over 4 + +Sets or gets the key of the pair. As above, this can be specified +as a build attribute as well as added to a prebuilt oilsPair object. +This must be a perlish scalar; any string or number that is valid as the +attribute on an XML node will work. + +=back + +=cut + +use overload '""' => sub { return ''.$_[0]->value }; +use overload '0+' => sub { return int($_[0]->value) }; +use overload 'bool' => sub { return 1 if ($_[0]->value); return 0 }; + +sub new { + my $class = shift; + my ($key, $value) = @_; + + my $self = $class->SUPER::new($value); + $self->setAttribute( key => $key); + + return $self; +} + +sub key { + my $self = shift; + my $key = shift; + + $self->setAttribute( key => $key) if ($key); + return $self->getAttribute( 'key' ); +} + +package OpenILS::DomainObjectCollection::oilsArray; +use base qw/OpenILS::DomainObjectCollection Tie::Array/; +use OpenILS::DomainObjectCollection; + +=head1 NAME + +OpenILS::DomainObjectCollection::oilsArray + +=head1 SYNOPSIS + + use OpenILS::DomainObject::oilsPrimitive; + + my $collection = OpenILS::DomainObjectCollection::oilsArray->new( $domain_object, $another_domain_object, ...); + + $collection->push( 'appended value' ); + $collection->unshift( 'prepended vaule' ); + my $first = $collection->shift; + my $last = $collection->pop; + + ... + + my @values = $collection->list; + + Or, using the TIE interface: + + my @array; + my $real_object = tie(@array, 'OpenILS::DomainObjectCollection::oilsArray', $domain, $objects, 'to', $store); + + or to tie an existing $collection object + + my @array; + tie(@array, 'OpenILS::DomainObjectCollection::oilsArray', $collection); + + or even.... + + my @array; + tie(@array, ref($collection), $collection); + + + $array[2] = $DomainObject; # replaces 'to' (which is now an OpenILS::DomainObject::oilsScalar) above + delete( $array[3] ); # removes '$store' above. + my $size = scalar( @array ); + + print $real_object->toString; + +=head1 ABSTRACT + +This package impliments array-like domain objects. A full tie interface +is also provided. If elements are passed in as strings (or numbers) they +are turned into oilsScalar objects. Any simple scalar or Domain Object may +be stored in the array. + +=head1 METHODS + +=head2 OpenILS::DomainObjectCollection::oilsArray->list() + +=over 4 + +Returns the array of 'OpenILS::DomainObject's that this collection contains. + +=back + +=cut + +sub tie_me { + my $class = shift; + $class = ref($class) || $class; + my $node = shift; + my @array; + tie @array, $class, $node; + return \@array; +} + +# an existing DomainObjectCollection::oilsArray can now be tied +sub TIEARRAY { + return CORE::shift()->new(@_); +} + +sub new { + my $class = CORE::shift; + $class = ref($class) || $class; + + my $first = CORE::shift; + + return $first + if ( defined $first and + ref $first and $first->can('base_type') and + UNIVERSAL::isa($first->class, __PACKAGE__) and + !scalar(@_) + ); + + my $self = $class->SUPER::new; + + my @args = @_; + if (ref($first) and ref($first) eq 'ARRAY') { + push @args, @$first; + tie( @$first, ref($self->upcast), $self); + } else { + unshift @args, $first if (defined $first); + } + + $self->STORE($self->FETCHSIZE, $_) for (@args); + return $self; +} + +sub STORE { + my $self = CORE::shift; + my ($index, $value) = @_; + + $value = OpenILS::DomainObject::oilsScalar->new($value) + unless ( ref $value and $value->nodeName =~ /^oils:domainObject/o ); + + $self->_expand($index) unless ($self->EXISTS($index)); + + ($self->childNodes)[$index]->replaceNode( $value ); + + return $value->upcast; +} + +sub push { + my $self = CORE::shift; + my @values = @_; + $self->STORE($self->FETCHSIZE, $_) for (@values); +} + +sub pop { + my $self = CORE::shift; + my $node = $self->SUPER::pop; + if ($node) { + if ($node->base_type eq 'oilsScalar') { + return $node->value; + } + return $node->upcast; + } +} + +sub unshift { + my $self = CORE::shift; + my @values = @_; + $self->insertBefore($self->firstChild, $_ ) for (reverse @values); +} + +sub shift { + my $self = CORE::shift; + my $node = $self->SUPER::shift; + if ($node) { + if ($node->base_type eq 'oilsScalar') { + return $node->value; + } + return $node->upcast; + } +} + +sub FETCH { + my $self = CORE::shift; + my $index = CORE::shift; + my $node = ($self->childNodes)[$index]->upcast; + if ($node) { + if ($node->base_type eq 'oilsScalar') { + return $node->value; + } + return $node->upcast; + } +} + +sub size { + my $self = CORE::shift; + scalar($self->FETCHSIZE) +} + +sub FETCHSIZE { + my $self = CORE::shift; + my @a = $self->childNodes; + return scalar(@a); +} + +sub _expand { + my $self = CORE::shift; + my $count = CORE::shift; + my $size = $self->FETCHSIZE; + for ($size..$count) { + $self->SUPER::push( new OpenILS::DomainObject::oilsScalar ); + } +} + +sub STORESIZE { + my $self = CORE::shift; + my $count = CORE::shift; + my $size = $self->FETCHSIZE - 1; + + if (defined $count and $count != $size) { + if ($size < $count) { + $self->_expand($count); + $size = $self->FETCHSIZE - 1; + } else { + while ($size > $count) { + $self->SUPER::pop; + $size = $self->FETCHSIZE - 1; + } + } + } + + return $size +} + +sub EXISTS { + my $self = CORE::shift; + my $index = CORE::shift; + return $self->FETCHSIZE > abs($index) ? 1 : 0; +} + +sub CLEAR { + my $self = CORE::shift; + $self->STORESIZE(0); + return $self; +} + +sub DELETE { + my $self = CORE::shift; + my $index = CORE::shift; + return $self->removeChild( ($self->childNodes)[$index] ); +} + +package OpenILS::DomainObjectCollection::oilsHash; +use base qw/OpenILS::DomainObjectCollection Tie::Hash/; + +=head1 NAME + +OpenILS::DomainObjectCollection::oilsHash + +=head1 SYNOPSIS + + use OpenILS::DomainObject::oilsPrimitive; + + my $collection = OpenILS::DomainObjectCollection::oilsHash->new( key1 => $domain_object, key2 => $another_domain_object, ...); + + $collection->set( key =>'value' ); + my $value = $collection->find( $key ); + my $dead_value = $collection->remove( $key ); + my @keys = $collection->keys; + my @values = $collection->values; + + Or, using the TIE interface: + + my %hash; + my $real_object = tie(%hash, 'OpenILS::DomainObjectCollection::oilsHash', domain => $objects, to => $store); + + or to tie an existing $collection object + + my %hash; + tie(%hash, 'OpenILS::DomainObjectCollection::oilsHash', $collection); + + or even.... + + my %hash; + tie(%hash, ref($collection), $collection); + + or perhaps ... + + my $content = $session->recv->content; # eh? EH?!?! + tie(my %hash, ref($content), $content); + + $hash{domain} = $DomainObject; # replaces value for key 'domain' above + delete( $hash{to} ); # removes 'to => $store' above. + for my $key ( keys %hash ) { + ... do stuff ... + } + + print $real_object->toString; + +=head1 ABSTRACT + +This package impliments hash-like domain objects. A full tie interface +is also provided. If elements are passed in as strings (or numbers) they +are turned into oilsScalar objects. Any simple scalar or Domain Object may +be stored in the hash. + +=back + +=cut + +sub tie_me { + my $class = shift; + $class = ref($class) || $class; + my $node = shift; + my %hash; + tie %hash, $class, $node; + return %hash; +} + + +sub keys { + my $self = shift; + return map { $_->key } $self->childNodes; +} + +sub values { + my $self = shift; + return map { $_->value } $self->childNodes; +} + +# an existing DomainObjectCollection::oilsHash can now be tied +sub TIEHASH { + return shift()->new(@_); +} + +sub new { + my $class = shift; + $class = ref($class) || $class; + my $first = shift; + + return $first + if ( defined $first and + ref $first and $first->can('base_type') and + UNIVERSAL::isa($first->class, __PACKAGE__) and + !scalar(@_) + ); + + my $self = $class->SUPER::new; + + my @args = @_; + if (ref($first) and ref($first) eq 'HASH') { + push @args, %$first; + tie( %$first, ref($self->upcast), $self); + } else { + unshift @args, $first if (defined $first); + } + + my %arg_hash = @args; + while ( my ($key, $value) = each(%arg_hash) ) { + $self->STORE($key => $value); + } + return $self; +} + +sub STORE { + shift()->set(@_); +} + +sub set { + my $self = shift; + my ($key, $value) = @_; + + my $node = $self->find_node($key); + + return $node->value( $value ) if (defined $node); + return $self->appendChild( OpenILS::DomainObject::oilsPair->new($key => $value) ); +} + +sub _accessor { + my $self = shift; + my $key = shift; + my $node = find_node($self, $key); + return $node->value if ($node); +} + +sub find_node { + my $self = shift; + my $key = shift; + return ($self->findnodes("oils:domainObject[\@name=\"oilsPair\" and \@key=\"$key\"]", $self))[0]; +} + +sub find { + my $self = shift; + my $key = shift; + my $node = $self->find_node($key); + my $value = $node->value if (defined $node); + return $value; +} + +sub size { + my $self = CORE::shift; + my @a = $self->childNodes; + return scalar(@a); +} + +sub FETCH { + my $self = shift; + my $key = shift; + return $self->find($key); +} + +sub EXISTS { + my $self = shift; + my $key = shift; + return $self->find_node($key); +} + +sub CLEAR { + my $self = shift; + $self->removeChild for ($self->childNodes); + return $self; +} + +sub DELETE { + shift()->remove(@_); +} + +sub remove { + my $self = shift; + my $key = shift; + return $self->removeChild( $self->find_node($key) ); +} + +sub FIRSTKEY { + my $self = shift; + return $self->firstChild->key; +} + +sub NEXTKEY { + my $self = shift; + my $key = shift; + my ($prev_node) = $self->find_node($key); + my $last_node = $self->lastChild; + + if ($last_node and $last_node->key eq $prev_node->key) { + return undef; + } else { + return $prev_node->nextSibling->key; + } +} + +package OpenILS::DomainObject::oilsHash; +use base qw/OpenILS::DomainObjectCollection::oilsHash/; + +package OpenILS::DomainObject::oilsArray; +use base qw/OpenILS::DomainObjectCollection::oilsArray/; + +1; diff --git a/src/perlmods/OpenSRF/DomainObject/oilsResponse.pm b/src/perlmods/OpenSRF/DomainObject/oilsResponse.pm new file mode 100644 index 0000000..70aa894 --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObject/oilsResponse.pm @@ -0,0 +1,406 @@ +package OpenILS::DomainObject::oilsResponse; +use vars qw/@EXPORT_OK %EXPORT_TAGS/; +use Exporter; +use JSON; +use base qw/OpenILS::DomainObject Exporter/; +use OpenILS::Utils::Logger qw/:level/; + +BEGIN { +@EXPORT_OK = qw/STATUS_CONTINUE STATUS_OK STATUS_ACCEPTED + STATUS_BADREQUEST STATUS_UNAUTHORIZED STATUS_FORBIDDEN + STATUS_NOTFOUND STATUS_NOTALLOWED STATUS_TIMEOUT + STATUS_INTERNALSERVERERROR STATUS_NOTIMPLEMENTED + STATUS_VERSIONNOTSUPPORTED STATUS_REDIRECTED + STATUS_EXPFAILED STATUS_COMPLETE/; + +%EXPORT_TAGS = ( + status => [ qw/STATUS_CONTINUE STATUS_OK STATUS_ACCEPTED + STATUS_BADREQUEST STATUS_UNAUTHORIZED STATUS_FORBIDDEN + STATUS_NOTFOUND STATUS_NOTALLOWED STATUS_TIMEOUT + STATUS_INTERNALSERVERERROR STATUS_NOTIMPLEMENTED + STATUS_VERSIONNOTSUPPORTED STATUS_REDIRECTED + STATUS_EXPFAILED STATUS_COMPLETE/ ], +); + +} + +=head1 NAME + +OpenILS::DomainObject::oilsResponse + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsResponse qw/:status/; + +my $resp = OpenILS::DomainObject::oilsResponse->new; + +$resp->status( 'a status message' ); + +$resp->statusCode( STATUS_CONTINUE ); + +$client->respond( $resp ); + +=head1 ABSTRACT + +OpenILS::DomainObject::oilsResponse implements the base class for all Application +layer messages send between the client and server. + +=cut + +sub STATUS_CONTINUE { return 100 } + +sub STATUS_OK { return 200 } +sub STATUS_ACCEPTED { return 202 } +sub STATUS_COMPLETE { return 205 } + +sub STATUS_REDIRECTED { return 307 } + +sub STATUS_BADREQUEST { return 400 } +sub STATUS_UNAUTHORIZED { return 401 } +sub STATUS_FORBIDDEN { return 403 } +sub STATUS_NOTFOUND { return 404 } +sub STATUS_NOTALLOWED { return 405 } +sub STATUS_TIMEOUT { return 408 } +sub STATUS_EXPFAILED { return 417 } + +sub STATUS_INTERNALSERVERERROR { return 500 } +sub STATUS_NOTIMPLEMENTED { return 501 } +sub STATUS_VERSIONNOTSUPPORTED { return 505 } + +my $log = 'OpenILS::Utils::Logger'; + +sub new { + my $class = shift; + $class = ref($class) || $class; + + my $default_status = eval "\$${class}::status"; + my $default_statusCode = eval "\$${class}::statusCode"; + + my %args = ( status => $default_status, + statusCode => $default_statusCode, + @_ ); + + return $class->SUPER::new( %args ); +} + +sub status { + my $self = shift; + return $self->_attr_get_set( status => shift ); +} + +sub statusCode { + my $self = shift; + return $self->_attr_get_set( statusCode => shift ); +} + + +#------------------------------------------------------------------------------- + + + +package OpenILS::DomainObject::oilsStatus; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use base 'OpenILS::DomainObject::oilsResponse'; +use vars qw/$status $statusCode/; + +=head1 NAME + +OpenILS::DomainObject::oilsException + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsResponse; + +... + +# something happens. + +$client->status( OpenILS::DomainObject::oilsStatus->new ); + +=head1 ABSTRACT + +The base class for Status messages sent between client and server. This +is implemented on top of the C class, and +sets the default B to C and B to C. + +=cut + +$status = 'Status'; +$statusCode = STATUS_OK; + +package OpenILS::DomainObject::oilsConnectStatus; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use base 'OpenILS::DomainObject::oilsStatus'; +use vars qw/$status $statusCode/; + +=head1 NAME + +OpenILS::DomainObject::oilsConnectStatus + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsResponse; + +... + +# something happens. + +$client->status( new OpenILS::DomainObject::oilsConnectStatus ); + +=head1 ABSTRACT + +The class for Stati relating to the connection status of a session. This +is implemented on top of the C class, and +sets the default B to C and B to C. + +=head1 SEE ALSO + +B + +=cut + +$status = 'Connection Successful'; +$statusCode = STATUS_OK; + +1; + + + +#------------------------------------------------------------------------------- + + + +package OpenILS::DomainObject::oilsResult; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use OpenILS::DomainObject::oilsPrimitive; +use base 'OpenILS::DomainObject::oilsResponse'; +use vars qw/$status $statusCode/; + + +$status = 'OK'; +$statusCode = STATUS_OK; + +=head1 NAME + +OpenILS::DomainObject::oilsResult + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsResponse; + + .... do stuff, create $object ... + +my $res = OpenILS::DomainObject::oilsResult->new; + +$res->content($object) + +$session->respond( $res ); + +=head1 ABSTRACT + +This is the base class for encapuslating RESULT messages send from the server +to a client. It is a subclass of B, and +sets B to C and B to C. + +=head1 METHODS + +=head2 OpenILS::DomainObject::oilsMessage->content( [$new_content] ) + +=over 4 + +Sets or gets the content of the response. This should be exactly one object +of (sub)type domainObject or domainObjectCollection. + +=back + +=cut + +sub content { + my $self = shift; + my $new_content = shift; + + my ($content) = $self->getChildrenByTagName('oils:domainObject'); + + if ($new_content) { + $new_content = OpenILS::DomainObject::oilsScalar->new( JSON->perl2JSON( $new_content ) ); + + $self->removeChild($content) if ($content); + $self->appendChild($new_content); + } + + + $new_content = $content if ($content); + + return JSON->JSON2perl($new_content->textContent) if $new_content; +} + +=head1 SEE ALSO + +B + +=cut + +1; + + + +#------------------------------------------------------------------------------- + + + +package OpenILS::DomainObject::oilsException; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use OpenILS::EX; +use base qw/OpenILS::EX OpenILS::DomainObject::oilsResponse/; +use vars qw/$status $statusCode/; +use Error; + +sub message { + my $self = shift; + return '<' . $self->statusCode . '> ' . $self->status; +} + +sub new { + my $class = shift; + return $class->OpenILS::DomainObject::oilsResponse::new( @_ ); +} + + +=head1 NAME + +OpenILS::DomainObject::oilsException + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsResponse; + +... + +# something breaks. + +$client->send( 'ERROR', OpenILS::DomainObject::oilsException->new( status => "ARRRRRRG!" ) ); + +=head1 ABSTRACT + +The base class for Exception messages sent between client and server. This +is implemented on top of the C class, and +sets the default B to C and B to C. + +=cut + +$status = 'Exception occured'; +$statusCode = STATUS_INTERNALSERVERERROR; + +package OpenILS::DomainObject::oilsConnectException; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use OpenILS::EX; +use base qw/OpenILS::DomainObject::oilsException OpenILS::EX::ERROR/; +use vars qw/$status $statusCode/; + +=head1 NAME + +OpenILS::DomainObject::oilsConnectException + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsResponse; + +... + +# something breaks while connecting. + +$client->send( 'ERROR', new OpenILS::DomainObject::oilsConnectException ); + +=head1 ABSTRACT + +The class for Exceptions that occur durring the B phase of a session. This +is implemented on top of the C class, and +sets the default B to C and B to C. + +=head1 SEE ALSO + +B + +=cut + + +$status = 'Connect Request Failed'; +$statusCode = STATUS_FORBIDDEN; + +package OpenILS::DomainObject::oilsMethodException; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use base 'OpenILS::DomainObject::oilsException'; +use vars qw/$status $statusCode/; + +=head1 NAME + +OpenILS::DomainObject::oilsMehtodException + +=head1 SYNOPSIS + +use OpenILS::DomainObject::oilsResponse; + +... + +# something breaks while looking up or starting +# a method call. + +$client->send( 'ERROR', new OpenILS::DomainObject::oilsMethodException ); + +=head1 ABSTRACT + +The class for Exceptions that occur durring the B phase of a session. This +is implemented on top of the C class, and +sets the default B to C and B to C. + +=head1 SEE ALSO + +B + +=cut + + +$status = 'Method not found'; +$statusCode = STATUS_NOTFOUND; + +# ------------------------------------------- + +package OpenILS::DomainObject::oilsServerError; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use base 'OpenILS::DomainObject::oilsException'; +use vars qw/$status $statusCode/; + +$status = 'Internal Server Error'; +$statusCode = STATUS_INTERNALSERVERERROR; + +# ------------------------------------------- + + + + + +package OpenILS::DomainObject::oilsBrokenSession; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use OpenILS::EX; +use base qw/OpenILS::DomainObject::oilsException OpenILS::EX::ERROR/; +use vars qw/$status $statusCode/; +$status = "Request on Disconnected Session"; +$statusCode = STATUS_EXPFAILED; + +package OpenILS::DomainObject::oilsXMLParseError; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use OpenILS::EX; +use base qw/OpenILS::DomainObject::oilsException OpenILS::EX::ERROR/; +use vars qw/$status $statusCode/; +$status = "XML Parse Error"; +$statusCode = STATUS_EXPFAILED; + +package OpenILS::DomainObject::oilsAuthException; +use OpenILS::DomainObject::oilsResponse qw/:status/; +use OpenILS::EX; +use base qw/OpenILS::DomainObject::oilsException OpenILS::EX::ERROR/; +use vars qw/$status $statusCode/; +$status = "Authentication Failure"; +$statusCode = STATUS_FORBIDDEN; + +1; diff --git a/src/perlmods/OpenSRF/DomainObject/oilsSearch.pm b/src/perlmods/OpenSRF/DomainObject/oilsSearch.pm new file mode 100644 index 0000000..2785da3 --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObject/oilsSearch.pm @@ -0,0 +1,106 @@ +package OpenILS::DomainObject::oilsSearch; +use OpenILS::DomainObject; +use OpenILS::DomainObject::oilsPrimitive; +use OpenILS::DOM::Element::searchCriteria; +use base 'OpenILS::DomainObject'; + +sub new { + my $class = shift; + $class = ref($class) || $class; + + unshift @_, 'table' if (@_ == 1); + my %args = @_; + + my $self = $class->SUPER::new; + + for my $part ( keys %args ) { + if ($part ne 'criteria') { + $self->$part( $args{$part} ); + next; + } + $self->criteria( OpenILS::DOM::Element::searchCriteria->new( @{$args{$part}} ) ); + } + return $self; +} + +sub format { + my $self = shift; + return $self->_attr_get_set( format => shift ); +} + +sub table { + my $self = shift; + return $self->_attr_get_set( table => shift ); +} + +sub fields { + my $self = shift; + my $new_fields_ref = shift; + + my ($old_fields) = $self->getChildrenByTagName("oils:domainObjectCollection"); + + if ($new_fields_ref) { + my $do = OpenILS::DomainObjectCollection::oilsArray->new( @$new_fields_ref ); + if (defined $old_fields) { + $old_fields->replaceNode($do); + } else { + $self->appendChild($do); + return $do->list; + } + } + + return $old_fields->list if ($old_fields); +} + +sub limit { + my $self = shift; + return $self->_attr_get_set( limit => shift ); +} + +sub offset { + my $self = shift; + return $self->_attr_get_set( offset => shift ); +} + +sub group_by { + my $self = shift; + return $self->_attr_get_set( group_by => shift ); +} + +sub criteria { + my $self = shift; + my $new_crit = shift; + + if (@_) { + unshift @_, $new_crit; + $new_crit = OpenILS::DOM::Element::searchCriteria->new(@_); + } + + my ($old_crit) = $self->getChildrenByTagName("oils:searchCriteria"); + + if (defined $new_crit) { + if (defined $old_crit) { + $old_crit->replaceNode($new_crit); + } else { + $self->appendChild($new_crit); + return $new_crit; + } + } + + return $old_crit; +} + +sub toSQL { + my $self = shift; + + my $SQL = 'SELECT ' . join(',', $self->fields); + $SQL .= ' FROM ' . $self->table; + $SQL .= ' WHERE ' . $self->criteria->toSQL if ($self->criteria); + $SQL .= ' GROUP BY ' . $self->group_by if ($self->group_by); + $SQL .= ' LIMIT ' . $self->limit if ($self->limit); + $SQL .= ' OFFSET ' . $self->offset if ($self->offset); + + return $SQL; +} + +1; diff --git a/src/perlmods/OpenSRF/DomainObjectCollection.pm b/src/perlmods/OpenSRF/DomainObjectCollection.pm new file mode 100644 index 0000000..7049af7 --- /dev/null +++ b/src/perlmods/OpenSRF/DomainObjectCollection.pm @@ -0,0 +1,35 @@ +package OpenSRF::DomainObjectCollection; +use base 'OpenSRF::DOM::Element::domainObjectCollection'; +use OpenSRF::DOM; +use OpenSRF::Utils::Logger qw(:level); +my $logger = "OpenSRF::Utils::Logger"; + +=head1 NAME + +OpenSRF::DomainObjectCollection + +=head1 SYNOPSIS + +OpenSRF::DomainObjectCollection is an abstract base class. It +should not be used directly. See C +for details. + +=cut + +sub new { + my $class = shift; + $class = ref($class) || $class; + + my @args = shift; + + (my $type = $class) =~ s/^.+://o; + + my $doc = OpenSRF::DOM->createDocument; + my $dO = OpenSRF::DOM::Element::domainObjectCollection->new( $type, @args ); + + $doc->documentElement->appendChild($dO); + + return $dO; +} + +1; diff --git a/src/perlmods/OpenSRF/EX.pm b/src/perlmods/OpenSRF/EX.pm new file mode 100644 index 0000000..7b2cfb0 --- /dev/null +++ b/src/perlmods/OpenSRF/EX.pm @@ -0,0 +1,251 @@ +package OpenSRF::EX; +use Error qw(:try); +use base qw( OpenSRF Error ); +use OpenSRF::Utils::Logger; + +my $log = "OpenSRF::Utils::Logger"; +$Error::Debug = 1; + +sub new { + my( $class, $message ) = @_; + $class = ref( $class ) || $class; + my $self = {}; + $self->{'msg'} = ${$class . '::ex_msg_header'} ." \n$message"; + return bless( $self, $class ); +} + +sub message() { return $_[0]->{'msg'}; } + +sub DESTROY{} + + +=head1 OpenSRF::EX + +Top level exception. This class logs an exception when it is thrown. Exception subclasses +should subclass one of OpenSRF::EX::INFO, NOTICE, WARN, ERROR, CRITICAL, and PANIC and provide +a new() method that takes a message and a message() method that returns that message. + +=cut + +=head2 Synopsis + + + throw OpenSRF::EX::Jabber ("I Am Dying"); + + OpenSRF::EX::InvalidArg->throw( "Another way" ); + + my $je = OpenSRF::EX::Jabber->new( "I Cannot Connect" ); + $je->throw(); + + + See OpenSRF/EX.pm for example subclasses. + +=cut + +# Log myself and throw myself + +#sub message() { shift->alert_abstract(); } + +#sub new() { shift->alert_abstract(); } + +sub throw() { + + my $self = shift; + + if( ! ref( $self ) || scalar( @_ ) ) { + $self = $self->new( @_ ); + } + + if( $self->class->isa( "OpenSRF::EX::INFO" ) || + $self->class->isa( "OpenSRF::EX::NOTICE" ) || + $self->class->isa( "OpenSRF::EX::WARN" ) ) { + + $log->debug( $self->stringify(), $log->DEBUG ); + } + + else{ $log->debug( $self->stringify(), $log->ERROR ); } + + $self->SUPER::throw; +} + + +sub stringify() { + + my $self = shift; + my $ctime = localtime(); + my( $package, $file, $line) = get_caller(); + my $name = ref( $self ); + my $msg = $self->message(); + + $msg =~ s/^/Mess: /mg; + + return " * ! EXCEPTION ! * \nTYPE: $name\n$msg\n". + "Loc.: $line $package \nLoc.: $file \nTime: $ctime\n"; +} + + +# --- determine the originating caller of this exception +sub get_caller() { + + $package = caller(); + my $x = 0; + while( $package->isa( "Error" ) || $package =~ /^Error::/ ) { + $package = caller( ++$x ); + } + return (caller($x)); +} + + + + +# ------------------------------------------------------------------- +# ------------------------------------------------------------------- + +# Top level exception subclasses defining the different exception +# levels. + +# ------------------------------------------------------------------- + +package OpenSRF::EX::INFO; +use base qw(OpenSRF::EX); +our $ex_msg_header = "System INFO"; + +# ------------------------------------------------------------------- + +package OpenSRF::EX::NOTICE; +use base qw(OpenSRF::EX); +our $ex_msg_header = "System NOTICE"; + +# ------------------------------------------------------------------- + +package OpenSRF::EX::WARN; +use base qw(OpenSRF::EX); +our $ex_msg_header = "System WARNING"; + +# ------------------------------------------------------------------- + +package OpenSRF::EX::ERROR; +use base qw(OpenSRF::EX); +our $ex_msg_header = "System ERROR"; + +# ------------------------------------------------------------------- + +package OpenSRF::EX::CRITICAL; +use base qw(OpenSRF::EX); +our $ex_msg_header = "System CRITICAL"; + +# ------------------------------------------------------------------- + +package OpenSRF::EX::PANIC; +use base qw(OpenSRF::EX); +our $ex_msg_header = "System PANIC"; + +# ------------------------------------------------------------------- +# ------------------------------------------------------------------- + +# Some basic exceptions + +# ------------------------------------------------------------------- +package OpenSRF::EX::Jabber; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "Jabber Exception"; + +package OpenSRF::EX::JabberDisconnected; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "JabberDisconnected Exception"; + +=head2 OpenSRF::EX::Jabber + +Thrown when there is a problem using the Jabber service + +=cut + +package OpenSRF::EX::Transport; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "Transport Exception"; + + + +# ------------------------------------------------------------------- +package OpenSRF::EX::InvalidArg; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "Invalid Arg Exception"; + +=head2 OpenSRF::EX::InvalidArg + +Thrown where an argument to a method was invalid or not provided + +=cut + + +# ------------------------------------------------------------------- +package OpenSRF::EX::NotADomainObject; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "Must be a Domain Object"; + +=head2 OpenSRF::EX::NotADomainObject + +Thrown where a OpenSRF::DomainObject::oilsScalar or +OpenSRF::DomainObject::oilsPair was passed a value that +is not a perl scalar or a OpenSRF::DomainObject. + +=cut + + +# ------------------------------------------------------------------- +package OpenSRF::EX::ArrayOutOfBounds; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "Tied array access on a nonexistant index"; + +=head2 OpenSRF::EX::ArrayOutOfBounds + +Thrown where a TIEd array (OpenSRF::DomainObject::oilsArray) was accessed at +a nonexistant index + +=cut + + + +# ------------------------------------------------------------------- +package OpenSRF::EX::Socket; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "Socket Exception"; + +=head2 OpenSRF::EX::Socket + +Thrown when there is a network layer exception + +=cut + + + +# ------------------------------------------------------------------- +package OpenSRF::EX::Config; +use base 'OpenSRF::EX::PANIC'; +our $ex_msg_header = "Config Exception"; + +=head2 OpenSRF::EX::Config + +Thrown when a package requires a config option that it cannot retrieve +or the config file itself cannot be loaded + +=cut + + +# ------------------------------------------------------------------- +package OpenSRF::EX::User; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "User Exception"; + +=head2 OpenSRF::EX::User + +Thrown when an error occurs due to user identification information + +=cut + +package OpenSRF::EX::Session; +use base 'OpenSRF::EX::ERROR'; +our $ex_msg_header = "Session Error"; + + +1; diff --git a/src/perlmods/OpenSRF/System.pm b/src/perlmods/OpenSRF/System.pm new file mode 100644 index 0000000..d78e422 --- /dev/null +++ b/src/perlmods/OpenSRF/System.pm @@ -0,0 +1,364 @@ +package OpenSRF::System; +use strict; use warnings; +use base 'OpenSRF'; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::Transport::Listener; +use OpenSRF::Transport; +use OpenSRF::UnixServer; +use OpenSRF::Utils; +use OpenSRF::Utils::LogServer; +use OpenSRF::DOM; +use OpenSRF::EX qw/:try/; +use POSIX ":sys_wait_h"; +use strict; + +=head2 Name/Description + +OpenSRF::System + +To start the system: OpenSRF::System->bootstrap(); + +Simple system process management and automation. After instantiating the class, simply call +bootstrap() to launch the system. Each launched process is stored as a process-id/method-name +pair in a local hash. When we receive a SIG{CHILD}, we loop through this hash and relaunch +any child processes that may have terminated. + +Currently automated processes include launching the internal Unix Servers, launching the inbound +connections for each application, and starting the system shell. + + +Note: There should be only one instance of this class +alive at any given time. It is designed as a globel process handler and, hence, will cause much +oddness if you call the bootstrap() method twice or attempt to create two of these by trickery. +There is a single instance of the class created on the first call to new(). This same instance is +returned on subsequent calls to new(). + +=cut + +$| = 1; + +sub APPS { return qw( opac ); } #circ cat storage ); } + +sub DESTROY {} + +# ---------------------------------------------- + +$SIG{INT} = sub { instance()->killall(); }; + +$SIG{HUP} = sub{ instance()->hupall(); }; + +#$SIG{CHLD} = \&process_automation; + + +# Go ahead and set the config + +set_config(); + +# ---------------------------------------------- +# Set config options +sub set_config { + + my $config = OpenSRF::Utils::Config->load( + config_file => "/pines/conf/oils.conf" ); + + if( ! $config ) { throw OpenSRF::EX::Config "System could not load config"; } + + my $tran = $config->transport->implementation; + + eval "use $tran;"; + if( $@ ) { + throw OpenSRF::EX::PANIC ("Cannot find transport implementation: $@" ); + } + + OpenSRF::Transport->message_envelope( $tran->get_msg_envelope ); + OpenSRF::Transport::PeerHandle->set_peer_client( $tran->get_peer_client ); + OpenSRF::Transport::Listener->set_listener( $tran->get_listener ); + + +} + + + + + + +# ---------------------------------------------- + +{ + # --- + # put $instance in a closure and return it for requests to new() + # since there should only be one System instance running + # ----- + my $instance; + sub instance { return __PACKAGE__->new(); } + sub new { + my( $class ) = @_; + + if( ! $instance ) { + $class = ref( $class ) || $class; + my $self = {}; + $self->{'pid_hash'} = {}; + bless( $self, $class ); + $instance = $self; + } + return $instance; + } +} + +# ---------------------------------------------- +# Commands to execute at system launch + +sub _unixserver { + my( $app ) = @_; + return "OpenSRF::UnixServer->new( '$app' )->serve()"; +} + +sub _listener { + my( $app ) = @_; + return "OpenSRF::Transport::Listener->new( '$app' )->initialize()->listen()"; +} + +#sub _shell { return "OpenSRF::Shell->listen()"; } + + +# ---------------------------------------------- +# Boot up the system + +sub bootstrap { + + my $self = __PACKAGE__->instance(); + + my $config = OpenSRF::Utils::Config->current; + + my $apps = $config->system->apps; + my $server_type = $config->system->server_type; + $server_type ||= "basic"; + + if( $server_type eq "prefork" ) { + $server_type = "Net::Server::PreForkSimple"; + } else { + $server_type = "Net::Server::Single"; + } + + _log( " * Server type: $server_type", INTERNAL ); + + eval "use $server_type"; + + if( $@ ) { + throw OpenSRF::EX::PANIC ("Cannot set $server_type: $@" ); + } + + push @OpenSRF::UnixServer::ISA, $server_type; + + _log( " * System boostrap" ); + + # Start a process group and make me the captain + setpgrp( 0, 0 ); + + $0 = "System"; + + # --- Boot the Unix servers + $self->launch_unix($apps); + + _sleep(); + + # --- Boot the listeners + $self->launch_listener($apps); + + _sleep(); + + # --- Start the system shell +#if ($config->system->shell) { +# eval " +# use OpenSRF::Shell; +# $self->launch_shell() if ($config->system->shell); +# "; +# +# if ($@) { +# warn "ARRRGGG! Can't start the shell..."; +# } +# } + + # --- Now we wait for our brood to perish + _log( " * System is ready..." ); + while( 1 ) { sleep; } + exit; +} + + + +# ---------------------------------------------- +# Bootstraps a single client connection. + +sub bootstrap_client { + + my $self = __PACKAGE__->instance(); + my $config = OpenSRF::Utils::Config->current; + + my $app = "client"; + + OpenSRF::Transport::PeerHandle->construct( $app ); + +} + +sub bootstrap_logger { + + $0 = "Log Server"; + OpenSRF::Utils::LogServer->serve(); + +} + + +# ---------------------------------------------- +# Cycle through the known processes, reap the dead child +# and put a new child in its place. (MMWWAHAHHAHAAAA!) + +sub process_automation { + + my $self = __PACKAGE__->instance(); + + foreach my $pid ( keys %{$self->pid_hash} ) { + + if( waitpid( $pid, WNOHANG ) == $pid ) { + + my $method = $self->pid_hash->{$pid}; + delete $self->pid_hash->{$pid}; + + my $newpid = OpenSRF::Utils::safe_fork(); + _log( "Relaunching => $method" ); + + if( $newpid ) { + $self->pid_hash( $newpid, $method ); + } + else { $0 = $method; eval $method; exit; } + } + } + + $SIG{CHLD} = \&process_automation; +} + + +# ---------------------------------------------- +# Launch the Unix Servers + +sub launch_unix { + my( $self, $apps ) = @_; + + foreach my $app ( @$apps ) { + + _log( " * Starting UnixServer for $app..." ); + + my $pid = OpenSRF::Utils::safe_fork(); + if( $pid ) { + $self->pid_hash( $pid , _unixserver( $app ) ); + } + else { + my $apname = $app; + $apname =~ tr/[a-z]/[A-Z]/; + $0 = "Unix Server ($apname)"; + eval _unixserver( $app ); + exit; + } + } +} + +# ---------------------------------------------- +# Launch the inbound clients + +sub launch_listener { + + my( $self, $apps ) = @_; + + foreach my $app ( @$apps ) { + + _log( " * Starting Listener for $app..." ); + + my $pid = OpenSRF::Utils::safe_fork(); + if ( $pid ) { + $self->pid_hash( $pid , _listener( $app ) ); + } + else { + my $apname = $app; + $apname =~ tr/[a-z]/[A-Z]/; + $0 = "Listener ($apname)"; + eval _listener( $app ); + exit; + } + } +} + +# ---------------------------------------------- + +=head comment +sub launch_shell { + + my $self = shift; + + my $pid = OpenSRF::Utils::safe_fork(); + + if( $pid ) { $self->pid_hash( $pid , _shell() ); } + else { + $0 = "System Shell"; + for( my $x = 0; $x != 10; $x++ ) { + eval _shell(); + if( ! $@ ) { last; } + } + exit; + } +} +=cut + + +# ---------------------------------------------- + +sub pid_hash { + my( $self, $pid, $method ) = @_; + $self->{'pid_hash'}->{$pid} = $method + if( $pid and $method ); + return $self->{'pid_hash'}; +} + +# ---------------------------------------------- +# If requested, the System can shut down. + +sub killall { + + $SIG{CHLD} = 'IGNORE'; + $SIG{INT} = 'IGNORE'; + kill( 'INT', -$$ ); #kill all in process group + exit; + +} + +# ---------------------------------------------- +# Handle $SIG{HUP} +sub hupall { + + _log( "HUPping brood" ); + $SIG{CHLD} = 'IGNORE'; + $SIG{HUP} = 'IGNORE'; + set_config(); # reload config + kill( 'HUP', -$$ ); +# $SIG{CHLD} = \&process_automation; + $SIG{HUP} = sub{ instance()->hupall(); }; +} + + +# ---------------------------------------------- +# Log to debug, and stdout + +sub _log { + my $string = shift; + OpenSRF::Utils::Logger->debug( $string ); + print $string . "\n"; +} + +# ---------------------------------------------- + +sub _sleep { + select( undef, undef, undef, 0.3 ); +} + +1; + + diff --git a/src/perlmods/OpenSRF/Transport.pm b/src/perlmods/OpenSRF/Transport.pm new file mode 100644 index 0000000..b1a8bbb --- /dev/null +++ b/src/perlmods/OpenSRF/Transport.pm @@ -0,0 +1,189 @@ +package OpenSRF::Transport; +use strict; use warnings; +use base 'OpenSRF'; +use OpenSRF::DOM; +use OpenSRF::AppSession; +use OpenSRF::Utils::Config; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::DomainObject::oilsResponse qw/:status/; +use OpenSRF::EX qw/:try/; + +#------------------ +# --- These must be implemented by all Transport subclasses +# ------------------------------------------- + +=head2 get_listener + +Returns the package name of the package the system will use to +gather incoming requests + +=cut + +sub get_listener { shift()->alert_abstract(); } + +=head2 get_peer_client + +Returns the name of the package responsible for client communication + +=cut + +sub get_peer_client { shift()->alert_abstract(); } + +=head2 get_msg_envelope + +Returns the name of the package responsible for parsing incoming messages + +=cut + +sub get_msg_envelope { shift()->alert_abstract(); } + +# ------------------------------------------- + +our $message_envelope; +my $logger = "OpenSRF::Utils::Logger"; + + + +=head2 message_envelope( [$envelope] ); + +Sets the message envelope class that will allow us to extract +information from the messages we receive from the low +level transport + +=cut + +sub message_envelope { + my( $class, $envelope ) = @_; + if( $envelope ) { + $message_envelope = $envelope; + eval "use $envelope;"; + if( $@ ) { + $logger->error( + "Error loading message_envelope: $envelope -> $@", ERROR); + } + } + return $message_envelope; +} + +=head2 handler( $data ) + +Creates a new MessageWrapper, extracts the remote_id, session_id, and message body +from the message. Then, creates or retrieves the AppSession object with the session_id and remote_id. +Finally, creates the message document from the body of the message and calls +the handler method on the message document. + +=cut + +sub handler { + my( $class, $service, $data ) = @_; + + $logger->transport( "Transport handler() received $data", INTERNAL ); + + # pass data to the message envelope + my $helper = $class->message_envelope->new( $data ); + + # Extract message information + my $remote_id = $helper->get_remote_id(); + my $sess_id = $helper->get_sess_id(); + my $body = $helper->get_body(); + my $type = $helper->get_msg_type(); + + $logger->transport( + "Transport building/retrieving session: $service, $remote_id, $sess_id", DEBUG ); + + # See if the app_session already exists. If so, make + # sure the sender hasn't changed if we're a server + my $app_session = OpenSRF::AppSession->find( $sess_id ); + if( $app_session and $app_session->endpoint == $app_session->SERVER() and + $app_session->remote_id ne $remote_id ) { + $logger->transport( "Backend Gone or invalid sender", INTERNAL ); + my $res = OpenSRF::DomainObject::oilsBrokenSession->new(); + $res->status( "Backend Gone or invalid sender, Reconnect" ); + $app_session->status( $res ); + return 1; + } + + # Retrieve or build the app_session as appropriate (server_build decides which to do) + $logger->transport( "AppSession is valid or does not exist yet", INTERNAL ); + $app_session = OpenSRF::AppSession->server_build( $sess_id, $remote_id, $service ); + + if( ! $app_session ) { + throw OpenSRF::EX::Session ("Transport::handler(): No AppSession object returned from server_build()"); + } + + # Create a document from the XML contained within the message + my $doc; + eval { $doc = OpenSRF::DOM->new->parse_string($body); }; + if( $@ ) { + + $logger->transport( "Received bogus XML", INFO ); + $logger->transport( "Bogus XML data: \n $body \n", INTERNAL ); + my $res = OpenSRF::DomainObject::oilsXMLParseError->new( status => "XML Parse Error --- $body" ); + + $app_session->status($res); + $app_session->kill_me; + return 1; + } + + $logger->transport( "Transport::handler() creating \n$body", INTERNAL ); + + # We need to disconnect the session if we got a jabber error on the client side. For + # server side, we'll just tear down the session and go away. + if (defined($type) and $type eq 'error') { + # If we're a server + if( $app_session->endpoint == $app_session->SERVER() ) { + $app_session->kill_me; + return 1; + } else { + $app_session->reset; + $app_session->state( $app_session->DISCONNECTED ); + $app_session->push_resend( $app_session->app_request( $doc->documentElement->firstChild->threadTrace ) ); + return 1; + } + } + + # cycle through and pass each oilsMessage contained in the message + # up to the message layer for processing. + for my $msg ($doc->documentElement->childNodes) { + + $logger->transport( + "Transport::handler()passing to message handler \n".$msg->toString(1), DEBUG ); + + $logger->transport( + "Transport passing up ".$msg->type." from ". + $app_session->remote_id . " with threadTrace [" . $msg->threadTrace."]", INFO ); + + next unless ( $msg->nodeName eq 'oils:domainObject' && + $msg->getAttribute('name') eq 'oilsMessage' ); + + if( $app_session->endpoint == $app_session->SERVER() ) { + + try { + + if( ! $msg->handler( $app_session ) ) { return 0; } + + } catch Error with { + + my $e = shift; + my $res = OpenSRF::DomainObject::oilsServerError->new(); + $res->status( $res->status . "\n" . $e->text ); + $logger->error($res->stringify); + $app_session->status($res) if $res; + $app_session->kill_me; + return 0; + + }; + + } else { + + if( ! $msg->handler( $app_session ) ) { return 0; } + + } + + } + + + return $app_session; +} + +1; diff --git a/src/perlmods/OpenSRF/Transport/Jabber.pm b/src/perlmods/OpenSRF/Transport/Jabber.pm new file mode 100644 index 0000000..3b45ac5 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/Jabber.pm @@ -0,0 +1,11 @@ +package OpenSRF::Transport::Jabber; +use base qw/OpenSRF::Transport/; + + +sub get_listener { return "OpenSRF::Transport::Jabber::JInbound"; } + +sub get_peer_client { return "OpenSRF::Transport::Jabber::JPeerConnection"; } + +sub get_msg_envelope { return "OpenSRF::Transport::Jabber::JMessageWrapper"; } + +1; diff --git a/src/perlmods/OpenSRF/Transport/Jabber/JInbound.pm b/src/perlmods/OpenSRF/Transport/Jabber/JInbound.pm new file mode 100644 index 0000000..a381274 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/Jabber/JInbound.pm @@ -0,0 +1,101 @@ +package OpenSRF::Transport::Jabber::JInbound; +use strict;use warnings; +use base qw/OpenSRF::Transport::Jabber::JabberClient/; +use OpenSRF::EX; +use OpenSRF::Utils::Config; +use OpenSRF::Utils::Logger qw(:level); + +my $logger = "OpenSRF::Utils::Logger"; + +=head1 Description + +This is the jabber connection where all incoming client requests will be accepted. +This connection takes the data, passes it off to the system then returns to take +more data. Connection params are all taken from the config file and the values +retreived are based on the $app name passed into new(). + +This service should be loaded at system startup. + +=cut + +# XXX This will be overhauled to connect as a component instead of as +# a user. all in good time, though. + +{ + my $unix_sock; + sub unix_sock { return $unix_sock; } + my $instance; + + sub new { + my( $class, $app ) = @_; + $class = ref( $class ) || $class; + if( ! $instance ) { + my $app_state = $app . "_inbound"; + my $config = OpenSRF::Utils::Config->current; + + if( ! $config ) { + throw OpenSRF::EX::Jabber( "No suitable config found" ); + } + + my $host = $config->transport->server->primary; + my $username = $config->transport->users->$app; + my $password = $config->transport->auth->password; + my $debug = $config->transport->llevel->$app_state; + my $log = $config->transport->log->$app_state; + my $resource = "system"; + + + my $self = __PACKAGE__->SUPER::new( + username => $username, + host => $host, + resource => $resource, + password => $password, + log_file => $log, + debug => $debug, + ); + + + my $f = $config->dirs->sock_dir; + $unix_sock = join( "/", $f, $config->unix_sock->$app ); + bless( $self, $class ); + $instance = $self; + } + $instance->SetCallBacks( message => \&handle_message ); + return $instance; + } + +} + +# --- +# All incoming messages are passed untouched to the Unix Server for processing. The +# Unix socket is closed by the Unix Server as soon as it has received all of the +# data. This means we can go back to accepting more incoming connection. +# ----- +sub handle_message { + my $sid = shift; + my $message = shift; + + my $packet = $message->GetXML(); + + $logger->transport( "JInbound $$ received $packet", INTERNAL ); + + # Send the packet to the unix socket for processing. + my $sock = unix_sock(); + my $socket; + my $x = 0; + for( ;$x != 5; $x++ ) { #try 5 times + if( $socket = IO::Socket::UNIX->new( Peer => $sock ) ) { + last; + } + } + if( $x == 5 ) { + throw OpenSRF::EX::Socket( + "Unable to connect to UnixServer: socket-file: $sock \n :=> $! " ); + } + print $socket $packet; + close( $socket ); +} + + +1; + diff --git a/src/perlmods/OpenSRF/Transport/Jabber/JMessageWrapper.pm b/src/perlmods/OpenSRF/Transport/Jabber/JMessageWrapper.pm new file mode 100644 index 0000000..15a6de5 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/Jabber/JMessageWrapper.pm @@ -0,0 +1,91 @@ +package OpenSRF::Transport::Jabber::JMessageWrapper; +use Jabber::NodeFactory; +use Net::Jabber qw(Client); +use Net::Jabber::Message; +use base qw/ Net::Jabber::Message OpenSRF /; +use OpenSRF::Utils::Logger qw(:level); +use strict; use warnings; + +=head1 Description + +OpenSRF::Transport::Jabber::JMessageWrapper + +Provides a means to extract information about a Jabber +message when all you have is the raw XML. The API +implemented here should be implemented by any Transport +helper/MessageWrapper class. + +=cut + +sub DESTROY{} + +my $logger = "OpenSRF::Utils::Logger"; +my $_node_factory = Jabber::NodeFactory->new( fromstr => 1 ); + + +=head2 new( Net::Jabber::Message/$raw_xml ) + +Pass in the raw Jabber message as XML and create a new +JMessageWrapper + +=cut + +sub new { + my( $class, $xml ) = @_; + $class = ref( $class ) || $class; + + return undef unless( $xml ); + + my $self; + + if( ref( $xml ) ) { + $self = $xml; + } else { + $logger->transport( "MWrapper got: " . $xml, INTERNAL ); + my $node = $_node_factory->newNodeFromStr( $xml ); + $self = $class->SUPER::new(); + $self->SetFrom( $node->attr('from') ); + $self->SetThread( $node->getTag('thread')->data ); + $self->SetBody( $node->getTag('body')->data ); + } + + bless( $self, $class ); + + $logger->transport( "MessageWrapper $self after blessing", INTERNAL ); + + return $self; + +} + +=head2 get_remote_id + +Returns the JID (user@host/resource) of the remote user + +=cut +sub get_remote_id { + my( $self ) = @_; + return $self->GetFrom(); +} + +=head2 get_sess_id + +Returns the Jabber thread associated with this message + +=cut +sub get_sess_id { + my( $self ) = @_; + return $self->GetThread(); +} + +=head2 get_body + +Returns the message body of the Jabber message + +=cut +sub get_body { + my( $self ) = @_; + return $self->GetBody(); +} + + +1; diff --git a/src/perlmods/OpenSRF/Transport/Jabber/JPeerConnection.pm b/src/perlmods/OpenSRF/Transport/Jabber/JPeerConnection.pm new file mode 100644 index 0000000..766de42 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/Jabber/JPeerConnection.pm @@ -0,0 +1,80 @@ +package OpenSRF::Transport::Jabber::JPeerConnection; +use strict; +use base qw/OpenSRF::Transport::Jabber::JabberClient/; +use OpenSRF::Utils::Config; +use OpenSRF::Utils::Logger qw(:level); + +=head1 Description + +Represents a single connection to a remote peer. The +Jabber values are loaded from the config file. + +Subclasses OpenSRF::Transport::JabberClient. + +=cut + +=head2 new() + + new( $appname ); + + The $appname parameter tells this class how to find the correct + Jabber username, password, etc to connect to the server. + +=cut + +our $main_instance; +our %apps_hash; + +sub retrieve { + my( $class, $app ) = @_; + my @keys = keys %apps_hash; + OpenSRF::Utils::Logger->transport( + "Requesting peer for $app and we have @keys", INTERNAL ); + return $apps_hash{$app}; +} + + + +sub new { + my( $class, $app ) = @_; + my $config = OpenSRF::Utils::Config->current; + + if( ! $config ) { + throw OpenSRF::EX::Config( "No suitable config found" ); + } + + my $app_stat = $app . "_peer"; + my $host = $config->transport->server->primary; + my $username = $config->transport->users->$app; + my $password = $config->transport->auth->password; + my $debug = $config->transport->llevel->$app_stat; + my $log = $config->transport->log->$app_stat; + my $resource = $config->env->hostname . "_$$"; + + OpenSRF::EX::Config->throw( "JPeer could not load all necesarry values from config" ) + unless ( $host and $username and $password and $resource ); + + + my $self = __PACKAGE__->SUPER::new( + username => $username, + host => $host, + resource => $resource, + password => $password, + log_file => $log, + debug => $debug, + ); + + bless( $self, $class ); + + $self->SetCallBacks( message => sub { + my $msg = $_[1]; + OpenSRF::Utils::Logger->transport( + "JPeer passing \n$msg \n to Transport->handler for $app", INTERNAL ); + OpenSRF::Transport->handler( $app, $msg ); } ); + + $apps_hash{$app} = $self; + return $apps_hash{$app}; +} + +1; + diff --git a/src/perlmods/OpenSRF/Transport/Jabber/JabberClient.pm b/src/perlmods/OpenSRF/Transport/Jabber/JabberClient.pm new file mode 100644 index 0000000..50eb6ae --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/Jabber/JabberClient.pm @@ -0,0 +1,277 @@ +package OpenSRF::Transport::Jabber::JabberClient; +use strict; use warnings; +use OpenSRF::EX; +use Net::Jabber qw( Client ); +use base qw( OpenSRF Net::Jabber::Client ); +use OpenSRF::Utils::Logger qw(:level); + +=head1 Description + +OpenSRF::Transport::Jabber::JabberClient + +Subclasses Net::Jabber::Client and, hence, provides the same +functionality. What it provides in addition is mainly some logging +and exception throwing on the call to 'initialize()', which sets +up the connection and authentication. + +=cut + +my $logger = "OpenSRF::Utils::Logger"; + +sub DESTROY{}; + + +=head2 new() + +Creates a new JabberClient object. The parameters should be self explanatory. +If not, see Net::Jabber::Client for more. + +debug and log_file are not required if you don't care to log the activity, +however all other parameters are. + +%params: + + host + username + resource + password + debug + log_file + +=cut + +sub new { + + my( $class, %params ) = @_; + + $class = ref( $class ) || $class; + + my $host = $params{'host'} || return undef; + my $username = $params{'username'} || return undef; + my $resource = $params{'resource'} || return undef; + my $password = $params{'password'} || return undef; + my $debug = $params{'debug'}; + my $log_file = $params{'log_file'}; + + my $self; + + if( $debug and $log_file ) { + $self = Net::Jabber::Client->new( + debuglevel => $debug, debugfile => $log_file ); + } + else { $self = Net::Jabber::Client->new(); } + + bless( $self, $class ); + + $self->host( $host ); + $self->username( $username ); + $self->resource( $resource ); + $self->password( $password ); + + $logger->transport( "Creating Jabber instance: $host, $username, $resource", + $logger->INFO ); + + $self->SetCallBacks( send => + sub { $logger->transport( "JabberClient in 'send' callback: @_", INTERNAL ); } ); + + + return $self; +} + +# ------------------------------------------------- + +=head2 gather() + +Gathers all Jabber messages sitting in the collection queue +and hands them each to their respective callbacks. This call +does not block (calls Process(0)) + +=cut + +sub gather { my $self = shift; $self->Process( 0 ); } + +# ------------------------------------------------- + +=head2 listen() + +Blocks and gathers incoming messages as they arrive. Does not return +unless an error occurs. + +Throws an OpenSRF::EX::JabberException if the call to Process ever fails. + +=cut +sub listen { + my $self = shift; + while(1) { + my $o = $self->process( -1 ); + if( ! defined( $o ) ) { + throw OpenSRF::EX::Jabber( "Listen Loop failed at 'Process()'" ); + } + } +} + +# ------------------------------------------------- + +sub password { + my( $self, $password ) = @_; + $self->{'oils:password'} = $password if $password; + return $self->{'oils:password'}; +} + +# ------------------------------------------------- + +sub username { + my( $self, $username ) = @_; + $self->{'oils:username'} = $username if $username; + return $self->{'oils:username'}; +} + +# ------------------------------------------------- + +sub resource { + my( $self, $resource ) = @_; + $self->{'oils:resource'} = $resource if $resource; + return $self->{'oils:resource'}; +} + +# ------------------------------------------------- + +sub host { + my( $self, $host ) = @_; + $self->{'oils:host'} = $host if $host; + return $self->{'oils:host'}; +} + +# ------------------------------------------------- + +=head2 send() + + Sends a Jabber message. + + %params: + to - The JID of the recipient + thread - The Jabber thread + body - The body of the message + +=cut + +sub send { + my( $self, %params ) = @_; + + my $to = $params{'to'} || return undef; + my $body = $params{'body'} || return undef; + my $thread = $params{'thread'}; + + my $msg = Net::Jabber::Message->new(); + + $msg->SetTo( $to ); + $msg->SetThread( $thread ) if $thread; + $msg->SetBody( $body ); + + $logger->transport( + "JabberClient Sending message to $to with thread $thread and body: \n$body", INTERNAL ); + + $self->Send( $msg ); +} + + +=head2 inintialize() + +Connect to the server and log in. + +Throws an OpenSRF::EX::JabberException if we cannot connect +to the server or if the authentication fails. + +=cut + +# --- The logging lines have been commented out until we decide +# on which log files we're using. + +sub initialize { + + my $self = shift; + + my $host = $self->host; + my $username = $self->username; + my $resource = $self->resource; + my $password = $self->password; + + my $jid = "$username\@$host\/$resource"; + + # --- 5 tries to connect to the jabber server + my $x = 0; + for( ; $x != 5; $x++ ) { + $logger->transport( "$jid: Attempting to connecto to server...$x", WARN ); + if( $self->Connect( 'hostname' => $host ) ) { + last; + } + else { sleep 3; } + } + + if( $x == 5 ) { + die "could not connect to server $!\n"; + throw OpenSRF::EX::Jabber( " Could not connect to Jabber server" ); + } + + $logger->transport( "Logging into jabber as $jid " . + "from " . ref( $self ), DEBUG ); + + # --- Log in + my @a = $self->AuthSend( 'username' => $username, + 'password' => $password, 'resource' => $resource ); + + if( $a[0] eq "ok" ) { + $logger->transport( " * $jid: Jabber authenticated and connected", DEBUG ); + } + else { + throw OpenSRF::EX::Jabber( " * $jid: Unable to authenticate: @a" ); + } + + return $self; +} + +sub construct { + my( $class, $app ) = @_; + $logger->transport("Constructing new Jabber connection for $app", INTERNAL ); + $class->peer_handle( + $class->new( $app )->initialize() ); +} + +sub process { + + my( $self, $timeout ) = @_; + if( ! $timeout ) { $timeout = 0; } + + unless( $self->Connected() ) { + OpenSRF::EX::Jabber->throw( + "This JabberClient instance is no longer connected to the server", ERROR ); + } + + my $val; + + if( $timeout eq "-1" ) { + $val = $self->Process(); + } + else { $val = $self->Process( $timeout ); } + + if( $timeout eq "-1" ) { $timeout = " "; } + + if( ! defined( $val ) ) { + OpenSRF::EX::Jabber->throw( + "Call to Net::Jabber::Client->Process( $timeout ) failed", ERROR ); + } + elsif( ! $val ) { + $logger->transport( + "Call to Net::Jabber::Client->Process( $timeout ) returned 0 bytes of data", DEBUG ); + } + elsif( $val ) { + $logger->transport( + "Call to Net::Jabber::Client->Process( $timeout ) successfully returned data", INTERNAL ); + } + + return $val; + +} + + +1; diff --git a/src/perlmods/OpenSRF/Transport/Listener.pm b/src/perlmods/OpenSRF/Transport/Listener.pm new file mode 100644 index 0000000..f9de8a4 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/Listener.pm @@ -0,0 +1,43 @@ +package OpenSRF::Transport::Listener; +use base 'OpenSRF'; +use OpenSRF::Utils::Logger qw(:level); + + +=head1 Description + +This is the empty class that acts as the subclass of the transport listener. My API +includes + +new( $app ) + create a new Listener with appname $app + +initialize() + Perform any transport layer connections/authentication. + +listen() + Block, wait for, and process incoming messages + +=cut + +=head2 set_listener() + +Sets my superclass. Pass in a string representing the perl module +(e.g. OpenSRF::Transport::Jabber::JInbound) to be used as the +superclass and it will be pushed onto @ISA. + +=cut + +sub set_listener { + my( $class, $listener ) = @_; + if( $listener ) { + eval "use $listener;"; + if( $@ ) { + OpenSRF::Utils::Logger->error( + "Unable to set transport listener: $@", ERROR ); + } + unshift @ISA, $listener; + } +} + + +1; diff --git a/src/perlmods/OpenSRF/Transport/PeerHandle.pm b/src/perlmods/OpenSRF/Transport/PeerHandle.pm new file mode 100644 index 0000000..e0bd53b --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/PeerHandle.pm @@ -0,0 +1,40 @@ +package OpenSRF::Transport::PeerHandle; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::EX; +use base 'OpenSRF'; +use vars '@ISA'; + +my $peer; + +=head2 peer_handle( $handle ) + +Assigns the object that will act as the peer connection handle. + +=cut +sub peer_handle { + my( $class, $handle ) = @_; + if( $handle ) { $peer = $handle; } + return $peer; +} + + +=head2 set_peer_client( $peer ) + +Sets the class that will act as the superclass of this class. +Pass in a string representing the module to be used as the superclass, +and that module is 'used' and unshifted into @ISA. We now have that +classes capabilities. + +=cut +sub set_peer_client { + my( $class, $peer ) = @_; + if( $peer ) { + eval "use $peer;"; + if( $@ ) { + throw OpenSRF::EX::PANIC ( "Unable to set peer client: $@" ); + } + unshift @ISA, $peer; + } +} + +1; diff --git a/src/perlmods/OpenSRF/Transport/SlimJabber.pm b/src/perlmods/OpenSRF/Transport/SlimJabber.pm new file mode 100644 index 0000000..7963b93 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/SlimJabber.pm @@ -0,0 +1,18 @@ +package OpenSRF::Transport::SlimJabber; +use base qw/OpenSRF::Transport/; + +=head2 OpenSRF::Transport::SlimJabber + +Implements the Transport interface for providing the system with appropriate +classes for handling transport layer messaging + +=cut + + +sub get_listener { return "OpenSRF::Transport::SlimJabber::Inbound"; } + +sub get_peer_client { return "OpenSRF::Transport::SlimJabber::PeerConnection"; } + +sub get_msg_envelope { return "OpenSRF::Transport::SlimJabber::MessageWrapper"; } + +1; diff --git a/src/perlmods/OpenSRF/Transport/SlimJabber/Client.pm b/src/perlmods/OpenSRF/Transport/SlimJabber/Client.pm new file mode 100644 index 0000000..fc1afb8 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/SlimJabber/Client.pm @@ -0,0 +1,541 @@ +package OpenSRF::Transport::SlimJabber::Client; +use strict; use warnings; +use OpenSRF::EX; +#use Net::Jabber qw( Client ); +use base qw( OpenSRF ); +use OpenSRF::Utils::Logger qw(:level); +use Time::HiRes qw(ualarm); + +use Fcntl qw(F_GETFL F_SETFL O_NONBLOCK); +use IO::Socket::INET; + +=head1 Description + +OpenSRF::Transport::SlimJabber::Client + +Home-brewed slimmed down jabber connection agent. Supports SSL connections +with a config file options: + + transport->server->port # the ssl port + transport->server->ssl # is this ssl? + +=cut + +my $logger = "OpenSRF::Utils::Logger"; + +sub DESTROY{ + my $self = shift; + my $socket = $self->{_socket}; + if( $socket and $socket->connected() ) { + print $socket ""; + close( $socket ); + } +}; + + +=head2 new() + +Creates a new Client object. + +debug and log_file are not required if you don't care to log the activity, +however all other parameters are. + +%params: + + username + resource + password + debug + log_file + +=cut + +sub new { + + my( $class, %params ) = @_; + + $class = ref( $class ) || $class; + + my $conf = OpenSRF::Utils::Config->current; + + my $host = $conf->transport->server->primary; + my $port = $conf->transport->server->port; + my $username = $params{'username'} || return undef; + my $resource = $params{'resource'} || return undef; + my $password = $params{'password'} || return undef; + + my $jid = "$username\@$host\/$resource"; + + + my $self = bless {} => $class; + + $self->jid( $jid ); + $self->host( $host ); + $self->port( $port ); + $self->username( $username ); + $self->resource( $resource ); + $self->password( $password ); + $self->{temp_buffer} = ""; + + $logger->transport( "Creating Client instance: $host:$port, $username, $resource", + $logger->INFO ); + + return $self; +} + +# clears the tmp buffer as well as the TCP buffer +sub buffer_reset { + + my $self = shift; + $self->{temp_buffer} = ""; + + my $fh = $self->{_socket}; + set_nonblock( $fh ); + my $t_buf = ""; + while( sysread( $fh, $t_buf, 4096 ) ) {} + set_block( $fh ); +} +# ------------------------------------------------- + +=head2 gather() + +Gathers all Jabber messages sitting in the collection queue +and hands them each to their respective callbacks. This call +does not block (calls Process(0)) + +=cut + +sub gather { my $self = shift; $self->process( 0 ); } + +# ------------------------------------------------- + +=head2 listen() + +Blocks and gathers incoming messages as they arrive. Does not return +unless an error occurs. + +Throws an OpenSRF::EX::JabberException if the call to Process ever fails. + +=cut +sub listen { + my $self = shift; + + my $sock = $self->unix_sock(); + my $socket = IO::Socket::UNIX->new( Peer => $sock ); + $logger->transport( "Unix Socket opened by Listener", INTERNAL ); + + throw OpenSRF::EX::Socket( "Unable to connect to UnixServer: socket-file: $sock \n :=> $! " ) + unless ($socket->connected); + + while(1) { + my $o = $self->process( -1 ); + $logger->transport( "Call to process() in listener returned:\n $o", INTERNAL ); + if( ! defined( $o ) ) { + throw OpenSRF::EX::Jabber( "Listen Loop failed at 'process()'" ); + } + print $socket $o; + + } + throw OpenSRF::EX::Socket( "How did we get here?!?!" ); +} + +sub set_nonblock { + my $fh = shift; + my $flags = fcntl($fh, F_GETFL, 0) + or die "Can't get flags for the socket: $!\n"; + + $logger->transport( "Setting NONBLOCK: original flags: $flags", INTERNAL ); + + fcntl($fh, F_SETFL, $flags | O_NONBLOCK) + or die "Can't set flags for the socket: $!\n"; + + return $flags; +} + +sub reset_fl { + my $fh = shift; + my $flags = shift; + $logger->transport( "Restoring BLOCK: to flags $flags", INTERNAL ); + fcntl($fh, F_SETFL, $flags) if defined $flags; +} + +sub set_block { + my $fh = shift; + + my $flags = fcntl($fh, F_GETFL, 0) + or die "Can't get flags for the socket: $!\n"; + + $flags &= ~O_NONBLOCK; + + fcntl($fh, F_SETFL, $flags) + or die "Can't set flags for the socket: $!\n"; +} + + +sub timed_read { + my ($self, $timeout) = @_; + + $logger->transport( "Temp Buffer Contained: \n". $self->{temp_buffer}, INTERNAL) if $self->{temp_buffer}; + if( $self->can( "app" ) ) { + $logger->transport( "timed_read called for ".$self->app.", I am: ".$self->jid, INTERNAL ); + } + + # See if there is a complete message in the temp_buffer + # that we can return + if( $self->{temp_buffer} ) { + my $buffer = $self->{temp_buffer}; + my $complete = 0; + $self->{temp_buffer} = ''; + + my ($tag) = ($buffer =~ /<([^\s\?\>]+)/o); + $logger->transport("Using tag: $tag ", INTERNAL); + + if ( $buffer =~ /^(.*?<\/$tag>)(.*)/s) { + $buffer = $1; + $self->{temp_buffer} = $2; + $complete++; + $logger->transport( "completed read with $buffer", INTERNAL ); + } elsif ( $buffer =~ /^<$tag[^>]*?\/>(.*)/) { + $self->{temp_buffer} = $1; + $complete++; + $logger->transport( "completed read with $buffer", INTERNAL ); + } else { + $self->{temp_buffer} = $buffer; + } + + if( $buffer and $complete ) { + return $buffer; + } + + } + ############ + + my $fh = $self->{_socket}; + + unless( $fh and $fh->connected ) { + throw OpenSRF::EX::Socket ("Attempted read on closed socket", ERROR ); + } + + $logger->transport( "Temp Buffer After first attempt: \n ".$self->{temp_buffer}, INTERNAL) if $self->{temp_buffer}; + + my $flags; + if (defined($timeout) && !$timeout) { + $flags = set_nonblock( $fh ); + } + + $timeout ||= 0; + $logger->transport( "Calling timed_read with timetout $timeout", INTERNAL ); + + + my $complete = 0; + my $first_read = 1; + my $xml = ''; + eval { + my $tag = ''; + eval { + no warnings; + local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required + + # alarm needs a number greater => 1. + my $alarm_timeout = $timeout; + if( $alarm_timeout > 0 and $alarm_timeout < 1 ) { + $alarm_timeout = 1; + } + alarm $alarm_timeout; + do { + + my $buffer = $self->{temp_buffer}; + $self->{temp_buffer} = ''; + ##### + + my $ff = fcntl($fh, F_GETFL, 0); + if ($ff == ($ff | O_NONBLOCK) and $timeout > 0 ) { + #throw OpenSRF::EX::ERROR ("File flags are set to NONBLOCK but timeout is $timeout", ERROR ); + } + + my $t_buf = ""; + my $read_size = 1024; + my $f = 0; + while( my $n = sysread( $fh, $t_buf, $read_size ) ) { + $buffer .= $t_buf; + if( $n < $read_size ) { + #reset_fl( $fh, $f ) if $f; + set_block( $fh ); + last; + } + # see if there is any more data to grab... + $f = set_nonblock( $fh ); + } + + #sysread($fh, $buffer, 2048, length($buffer) ); + #sysread( $fh, $t_buf, 2048 ); + #$buffer .= $t_buf; + + ##### + $logger->transport(" Got [$buffer] from the socket", INTERNAL); + + if ($first_read) { + $logger->transport(" First read Buffer\n [$buffer]", INTERNAL); + ($tag) = ($buffer =~ /<([^\s\?\>]+){1}/o); + $first_read--; + $logger->transport("Using tag: $tag ", INTERNAL); + } + + if (!$first_read && $buffer =~ /^(.*?<\/$tag>){1}(.*)/s) { + $buffer = $1; + $self->{temp_buffer} = $2; + $complete++; + $logger->transport( "completed read with $buffer", INTERNAL ); + } elsif (!$first_read && $buffer =~ /^<$tag[^>]*?\/>(.*)/) { + $self->{temp_buffer} = $1; + $complete++; + $logger->transport( "completed read with $buffer", INTERNAL ); + } + + $xml .= $buffer; + + } while (!$complete && $xml); + alarm(0); + }; + alarm(0); + }; + + $logger->transport( "XML Read: $xml", INTERNAL ); + #reset_fl( $fh, $flags) if defined $flags; + set_block( $fh ) if defined $flags; + + if ($complete) { + return $xml; + } + if( $@ ) { + return undef; + } + return ""; +} + + +# ------------------------------------------------- + +sub tcp_connected { + + my $self = shift; + return 1 if ($self->{_socket} and $self->{_socket}->connected); + return 0; +} + +sub password { + my( $self, $password ) = @_; + $self->{'oils:password'} = $password if $password; + return $self->{'oils:password'}; +} + +# ------------------------------------------------- + +sub username { + my( $self, $username ) = @_; + $self->{'oils:username'} = $username if $username; + return $self->{'oils:username'}; +} + +# ------------------------------------------------- + +sub resource { + my( $self, $resource ) = @_; + $self->{'oils:resource'} = $resource if $resource; + return $self->{'oils:resource'}; +} + +# ------------------------------------------------- + +sub jid { + my( $self, $jid ) = @_; + $self->{'oils:jid'} = $jid if $jid; + return $self->{'oils:jid'}; +} + +sub port { + my( $self, $port ) = @_; + $self->{'oils:port'} = $port if $port; + return $self->{'oils:port'}; +} + +sub host { + my( $self, $host ) = @_; + $self->{'oils:host'} = $host if $host; + return $self->{'oils:host'}; +} + +# ------------------------------------------------- + +=head2 send() + + Sends a Jabber message. + + %params: + to - The JID of the recipient + thread - The Jabber thread + body - The body of the message + +=cut + +sub send { + my $self = shift; + my %params = @_; + + my $to = $params{'to'} || return undef; + my $body = $params{'body'} || return undef; + my $thread = $params{'thread'} || ""; + my $router_command = $params{'router_command'} || ""; + my $router_class = $params{'router_class'} || ""; + + my $msg = OpenSRF::Transport::SlimJabber::MessageWrapper->new; + + $msg->setTo( $to ); + $msg->setThread( $thread ) if $thread; + $msg->setBody( $body ); + $msg->set_router_command( $router_command ); + $msg->set_router_class( $router_class ); + + + $logger->transport( + "JabberClient Sending message to $to with thread $thread and body: \n$body", INTERNAL ); + + my $soc = $self->{_socket}; + print $soc $msg->toString; +} + + +=head2 inintialize() + +Connect to the server and log in. + +Throws an OpenSRF::EX::JabberException if we cannot connect +to the server or if the authentication fails. + +=cut + +# --- The logging lines have been commented out until we decide +# on which log files we're using. + +sub initialize { + + my $self = shift; + + my $jid = $self->jid; + my $host = $self->host; + my $port = $self->port; + my $username = $self->username; + my $resource = $self->resource; + my $password = $self->password; + + my $stream = <<" XML"; + + XML + + my $auth = <<" XML"; + + +$username +$password +$resource + + + XML + + + # --- 5 tries to connect to the jabber server + my $socket; + for(1..5) { + $logger->transport( "$jid: Attempting to connect to server... (Try # $_)", WARN ); + $socket = IO::Socket::INET->new( PeerHost => $host, + PeerPort => $port, + Proto => 'tcp' ); + last if ( $socket and $socket->connected ); + sleep 3; + } + + unless ( $socket and $socket->connected ) { + throw OpenSRF::EX::Jabber( " Could not connect to Jabber server: $!" ); + } + + $logger->transport( "Logging into jabber as $jid " . + "from " . ref( $self ), DEBUG ); + + print $socket $stream; + + my $buffer; + eval { + eval { + local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required + alarm 3; + sysread($socket, $buffer, 4096); + $logger->transport( "Login buffer 1: $buffer", INTERNAL ); + alarm(0); + }; + alarm(0); + }; + + print $socket $auth; + + if( $socket and $socket->connected() ) { + $self->{_socket} = $socket; + } else { + throw OpenSRF::EX::Jabber( " ** Unable to connect to Jabber server", ERROR ); + } + + + $buffer = $self->timed_read(10); + + if( $buffer ) {$logger->transport( "Login buffer 2: $buffer", INTERNAL );} + + if( $buffer and $buffer =~ /type=["\']result["\']/ ) { + $logger->transport( " * $jid: Jabber authenticated and connected", DEBUG ); + } else { + if( !$buffer ) { $buffer = " "; } + $socket->close; + throw OpenSRF::EX::Jabber( " * $jid: Unable to authenticate: $buffer", ERROR ); + } + + return $self; +} + +sub construct { + my( $class, $app ) = @_; + $logger->transport("Constructing new Jabber connection for $app", INTERNAL ); + $class->peer_handle( + $class->new( $app )->initialize() ); +} + +sub process { + + my( $self, $timeout ) = @_; + + $timeout ||= 0; + undef $timeout if ( $timeout == -1 ); + + unless( $self->{_socket}->connected ) { + OpenSRF::EX::JabberDisconnected->throw( + "This JabberClient instance is no longer connected to the server", ERROR ); + } + + my $val = $self->timed_read( $timeout ); + + $timeout = "FOREVER" unless ( defined $timeout ); + + if ( ! defined( $val ) ) { + OpenSRF::EX::Jabber->throw( + "Call to Client->timed_read( $timeout ) failed", ERROR ); + } elsif ( ! $val ) { + $logger->transport( + "Call to Client->timed_read( $timeout ) returned 0 bytes of data", DEBUG ); + } elsif ( $val ) { + $logger->transport( + "Call to Client->timed_read( $timeout ) successfully returned data", INTERNAL ); + } + + return $val; + +} + + +1; diff --git a/src/perlmods/OpenSRF/Transport/SlimJabber/Inbound.pm b/src/perlmods/OpenSRF/Transport/SlimJabber/Inbound.pm new file mode 100644 index 0000000..9dc5ad0 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/SlimJabber/Inbound.pm @@ -0,0 +1,94 @@ +package OpenSRF::Transport::SlimJabber::Inbound; +use strict;use warnings; +use base qw/OpenSRF::Transport::SlimJabber::Client/; +use OpenSRF::EX; +use OpenSRF::Utils::Config; +use OpenSRF::Utils::Logger qw(:level); + +my $logger = "OpenSRF::Utils::Logger"; + +=head1 Description + +This is the jabber connection where all incoming client requests will be accepted. +This connection takes the data, passes it off to the system then returns to take +more data. Connection params are all taken from the config file and the values +retreived are based on the $app name passed into new(). + +This service should be loaded at system startup. + +=cut + +# XXX This will be overhauled to connect as a component instead of as +# a user. all in good time, though. + +{ + my $unix_sock; + sub unix_sock { return $unix_sock; } + my $instance; + + sub new { + my( $class, $app ) = @_; + $class = ref( $class ) || $class; + if( ! $instance ) { + my $app_state = $app . "_inbound"; + my $config = OpenSRF::Utils::Config->current; + + if( ! $config ) { + throw OpenSRF::EX::Jabber( "No suitable config found" ); + } + + my $username = $config->transport->users->$app; + my $password = $config->transport->auth->password; + my $resource = "system_" . $config->env->hostname . "_$$"; + + + my $self = __PACKAGE__->SUPER::new( + username => $username, + resource => $resource, + password => $password, + ); + + $self->{app} = $app; + + + my $f = $config->dirs->sock_dir; + $unix_sock = join( "/", $f, $config->unix_sock->$app ); + bless( $self, $class ); + $instance = $self; + } + return $instance; + } + +} + +sub listen { + my $self = shift; + + my $config = OpenSRF::Utils::Config->current; + my $router = $config->system->router_target; + $self->send( to => $router, + body => "registering", router_command => "register" , router_class => $self->{app} ); + + while(1) { + my $sock = $self->unix_sock(); + my $socket = IO::Socket::UNIX->new( Peer => $sock ); + + throw OpenSRF::EX::Socket( "Unable to connect to UnixServer: socket-file: $sock \n :=> $! " ) + unless ($socket->connected); + + my $o = $self->process( -1 ); + + if( ! defined( $o ) ) { + throw OpenSRF::EX::Jabber( "Listen Loop failed at 'process()'" ); + } + print $socket $o; + + $socket->close; + + } + + throw OpenSRF::EX::Socket( "How did we get here?!?!" ); +} + +1; + diff --git a/src/perlmods/OpenSRF/Transport/SlimJabber/MessageWrapper.pm b/src/perlmods/OpenSRF/Transport/SlimJabber/MessageWrapper.pm new file mode 100644 index 0000000..7ac8f3a --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/SlimJabber/MessageWrapper.pm @@ -0,0 +1,99 @@ +package OpenSRF::Transport::SlimJabber::MessageWrapper; +use OpenSRF::DOM; + +sub new { + my $class = shift; + $class = ref($class) || $class; + + my $xml = shift; + + my ($doc, $msg); + if ($xml) { + $doc = OpenSRF::DOM->new->parse_string($xml); + $msg = $doc->documentElement; + } else { + $doc = OpenSRF::DOM->createDocument; + $msg = $doc->createElement( 'message' ); + $doc->documentElement->appendChild( $msg ); + } + + + my $self = { msg_node => $msg }; + + return bless $self => $class; +} + +sub toString { + my $self = shift; + return $self->{msg_node}->toString(@_); +} + +sub get_body { + my $self = shift; + my ($t_body) = grep {$_->nodeName eq 'body'} $self->{msg_node}->childNodes; + if( $t_body ) { + my $body = $t_body->textContent; + return $body; + } + return ""; +} + +sub get_sess_id { + my $self = shift; + my ($t_node) = grep {$_->nodeName eq 'thread'} $self->{msg_node}->childNodes; + if( $t_node ) { + return $t_node->textContent; + } + return ""; +} + +sub get_msg_type { + my $self = shift; + $self->{msg_node}->getAttribute( 'type' ); +} + +sub get_remote_id { + my $self = shift; + + # + my $rid = $self->{msg_node}->getAttribute( 'router_from' ); + return $rid if $rid; + + return $self->{msg_node}->getAttribute( 'from' ); +} + +sub setType { + my $self = shift; + $self->{msg_node}->setAttribute( type => shift ); +} + +sub setTo { + my $self = shift; + $self->{msg_node}->setAttribute( to => shift ); +} + +sub setThread { + my $self = shift; + $self->{msg_node}->appendTextChild( thread => shift ); +} + +sub setBody { + my $self = shift; + my $body = shift; + $self->{msg_node}->appendTextChild( body => $body ); +} + +sub set_router_command { + my( $self, $router_command ) = @_; + if( $router_command ) { + $self->{msg_node}->setAttribute( router_command => $router_command ); + } +} +sub set_router_class { + my( $self, $router_class ) = @_; + if( $router_class ) { + $self->{msg_node}->setAttribute( router_class => $router_class ); + } +} + +1; diff --git a/src/perlmods/OpenSRF/Transport/SlimJabber/PeerConnection.pm b/src/perlmods/OpenSRF/Transport/SlimJabber/PeerConnection.pm new file mode 100644 index 0000000..b65ab35 --- /dev/null +++ b/src/perlmods/OpenSRF/Transport/SlimJabber/PeerConnection.pm @@ -0,0 +1,99 @@ +package OpenSRF::Transport::SlimJabber::PeerConnection; +use strict; +use base qw/OpenSRF::Transport::SlimJabber::Client/; +use OpenSRF::Utils::Config; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::EX qw/:try/; + +=head1 Description + +Represents a single connection to a remote peer. The +Jabber values are loaded from the config file. + +Subclasses OpenSRF::Transport::SlimJabber::Client. + +=cut + +=head2 new() + + new( $appname ); + + The $appname parameter tells this class how to find the correct + Jabber username, password, etc to connect to the server. + +=cut + +our %apps_hash; + +sub retrieve { + my( $class, $app ) = @_; + my @keys = keys %apps_hash; + OpenSRF::Utils::Logger->transport( + "Requesting peer for $app and we have @keys", INTERNAL ); + return $apps_hash{$app}; +} + + + +sub new { + my( $class, $app ) = @_; + my $config = OpenSRF::Utils::Config->current; + + if( ! $config ) { + throw OpenSRF::EX::Config( "No suitable config found" ); + } + + my $app_stat = $app . "_peer"; + my $username = $config->transport->users->$app; + my $password = $config->transport->auth->password; + my $resource = $config->env->hostname . "_$$"; + + OpenSRF::EX::Config->throw( "JPeer could not load all necesarry values from config" ) + unless ( $username and $password and $resource ); + + + my $self = __PACKAGE__->SUPER::new( + username => $username, + resource => $resource, + password => $password, + ); + + bless( $self, $class ); + + $self->app($app); + + $apps_hash{$app} = $self; + return $apps_hash{$app}; +} + +sub process { + my $self = shift; + my $val = $self->SUPER::process(@_); + return 0 unless $val; + OpenSRF::Utils::Logger->transport( "Calling transport handler for ".$self->app." with: $val", INTERNAL ); + my $t; +#try { + $t = OpenSRF::Transport->handler($self->app, $val); + +# } catch OpenSRF::EX with { +# my $e = shift; +# $e->throw(); + +# } catch Error with { return undef; } + + return $t; +} + +sub app { + my $self = shift; + my $app = shift; + if( $app ) { + OpenSRF::Utils::Logger->transport( "PEER changing app to $app: ".$self->jid, INTERNAL ); + } + + $self->{app} = $app if ($app); + return $self->{app}; +} + +1; + diff --git a/src/perlmods/OpenSRF/UnixServer.pm b/src/perlmods/OpenSRF/UnixServer.pm new file mode 100644 index 0000000..8164974 --- /dev/null +++ b/src/perlmods/OpenSRF/UnixServer.pm @@ -0,0 +1,160 @@ +package OpenSRF::UnixServer; +use strict; use warnings; +use base qw/OpenSRF/; +use OpenSRF::EX; +use OpenSRF::Utils::Logger qw(:level); +use OpenSRF::Transport::PeerHandle; +use OpenSRF::Application; +use OpenSRF::AppSession; +use OpenSRF::DomainObject::oilsResponse qw/:status/; +use OpenSRF::System; +use vars qw/@ISA/; +use Carp; + +# XXX Need to add actual logging statements in the code +my $logger = "OpenSRF::Utils::Logger"; + +sub DESTROY { confess "Dying $$"; } + +=head1 What am I + +All inbound messages are passed on to the UnixServer for processing. +We take the data, close the Unix socket, and pass the data on to our abstract +'process()' method. + +Our purpose is to 'multiplex' a single TCP connection into multiple 'client' connections. +So when you pass data down the Unix socket to us, we have been preforked and waiting +to disperse new data among us. + +=cut + +{ + my $app; + sub app { return $app; } + + sub new { + my( $class, $app1 ) = @_; + if( ! $app1 ) { + throw OpenSRF::EX::InvalidArg( "UnixServer requires an app name to run" ); + } + $app = $app1; + my $self = bless( {}, $class ); + if( OpenSRF::Utils::Config->current->system->server_type !~ /fork/i ) { + $self->child_init_hook(); + } + return $self; + } + +} + +=head2 process_request() + +Takes the incoming data, closes the Unix socket and hands the data untouched +to the abstract process() method. This method is implemented in our subclasses. + +=cut + +sub process_request { + + my $self = shift; + my $data; my $d; + while( $d = ) { $data .= $d; } + + + if( ! $data or ! defined( $data ) or $data eq "" ) { + throw OpenSRF::EX::Socket( + "Unix child received empty data from socket" ); + } + + if( ! close( $self->{server}->{client} ) ) { + $logger->debug( "Error closing Unix socket: $!", ERROR ); + } + + + my $app = $self->app(); + $logger->transport( "UnixServer for $app received $data", INTERNAL ); + + my $app_session = OpenSRF::Transport->handler( $self->app(), $data ); + my $config = OpenSRF::Utils::Config->current; + + + my $keepalive = OpenSRF::Utils::Config->current->system->keep_alive; + + my $req_counter = 0; + while( $app_session->state and $app_session->state != $app_session->DISCONNECTED() and + $app_session->find( $app_session->session_id ) ) { + + + my $before = time; + $logger->transport( "UnixServer calling queue_wait $keepalive", INTERNAL ); + $app_session->queue_wait( $keepalive ); + my $after = time; + + if( ($after - $before) >= $keepalive ) { + + my $res = OpenSRF::DomainObject::oilsConnectStatus->new( + status => "Disconnected on timeout", + statusCode => STATUS_TIMEOUT); + $app_session->status($res); + $app_session->state( $app_session->DISCONNECTED() ); + last; + } + + } + + my $x = 0; + while( 1 ) { + $logger->transport( "Looping on zombies " . $x++ , DEBUG); + last unless ( $app_session->queue_wait(0)); + } + + $logger->transport( "Timed out, disconnected, or auth failed", INFO ); + $app_session->kill_me; + +} + + +sub serve { + my( $self ) = @_; + my $config = OpenSRF::Utils::Config->current; + my $app = $self->app(); + my $conf_base = $config->dirs->conf_dir; + my $conf = join( "/", $conf_base, $config->unix_conf->$app ); + $logger->transport( + "Running UnixServer as @OpenSRF::UnixServer::ISA for $app with conf file: $conf", INTERNAL ); + $self->run( 'conf_file' => $conf ); +} + +sub configure_hook { + my $self = shift; + my $app = $self->app; + my $config = OpenSRF::Utils::Config->current; + + $logger->debug( "Setting application implementaion for $app", DEBUG ); + + OpenSRF::Application->application_implementation( $config->application_implementation->$app ); + OpenSRF::Application->application_implementation->initialize() + if (OpenSRF::Application->application_implementation->can('initialize')); + return OpenSRF::Application->application_implementation; +} + +sub child_finish_hook { + my $self = shift; + OpenSRF::AppSession->kill_client_session_cache; +} + +sub child_init_hook { + + my $self = shift; + $logger->transport( + "Creating PeerHandle from UnixServer child_init_hook", INTERNAL ); + OpenSRF::Transport::PeerHandle->construct( $self->app() ); + my $peer_handle = OpenSRF::System::bootstrap_client(); + OpenSRF::Application->application_implementation->child_init + if (OpenSRF::Application->application_implementation->can('child_init')); + return $peer_handle; + +} + +1; + diff --git a/src/perlmods/OpenSRF/Utils.pm b/src/perlmods/OpenSRF/Utils.pm new file mode 100644 index 0000000..297cade --- /dev/null +++ b/src/perlmods/OpenSRF/Utils.pm @@ -0,0 +1,390 @@ +package OpenSRF::Utils; + +=head1 NAME + +OpenSRF::Utils + +=head1 DESCRIPTION + +This is a container package for methods that are useful to derived modules. +It has no constructor, and is generally not useful by itself... but this +is where most of the generic methods live. + + +=head1 METHODS + + +=cut + +use vars qw/@ISA $AUTOLOAD %EXPORT_TAGS @EXPORT_OK @EXPORT $VERSION/; +push @ISA, 'Exporter'; + +$VERSION = do { my @r=(q$Revision$=~/\d+/g); sprintf "%d."."%02d"x$#r,@r }; + +use Time::Local; +use Errno; +use POSIX; +use FileHandle; +#use Cache::FileCache; +#use Storable qw(dclone); +use Digest::MD5 qw(md5 md5_hex md5_base64); +use Exporter; + +# This turns errors into warnings, so daemons don't die. +#$Storable::forgive_me = 1; + +%EXPORT_TAGS = (common => [qw(interval_to_seconds seconds_to_interval sendmail)], daemon => [qw(safe_fork set_psname daemonize)]); + +Exporter::export_ok_tags('common','daemon'); # add aa, cc and dd to @EXPORT_OK + +sub AUTOLOAD { + my $self = shift; + my $type = ref($self) or return undef; + + my $name = $AUTOLOAD; + $name =~ s/.*://; # strip fully-qualified portion + + if (defined($_[0])) { + return $self->{$name} = shift; + } + return $self->{$name}; +} + + +sub _sub_builder { + my $self = shift; + my $class = ref($self) || $self; + my $part = shift; + unless ($class->can($part)) { + *{$class.'::'.$part} = + sub { + my $self = shift; + my $new_val = shift; + if ($new_val) { + $$self{$part} = $new_val; + } + return $$self{$part}; + }; + } +} + +#sub standalone_ipc_cache { +# my $self = shift; +# my $class = ref($self) || $self; +# my $uniquifier = shift || return undef; +# my $expires = shift || 3600; + +# return new Cache::FileCache ( { namespace => $class.'::'.$uniquifier, default_expires_in => $expires } ); +#} + +sub sendmail { + my $self = shift; + my $message = shift || $self; + + open SM, '|/usr/sbin/sendmail -U -t' or return 0; + print SM $message; + close SM or return 0; + return 1; +} + +sub __strip_comments { + my $self = shift; + my $config_file = shift; + my ($line, @done); + while (<$config_file>) { + s/^\s*(.*)\s*$/$1/o if (lc($$self{keep_space}) ne 'true'); + /^(.*)$/o; + $line .= $1; + # keep new lines if keep_space is true + if ($line =~ /^$/o && (lc($$self{keep_space}) ne 'true')) { + $line = ''; + next; + } + if (/^([^<]+)\s*<<\s*(\w+)\s*$/o) { + $line = "$1 = "; + my $breaker = $2; + while (<$config_file>) { + chomp; + last if (/^$breaker/); + $line .= $_; + } + } + + if ($line =~ /^#/ && $line !~ /^#\s*include\s+/o) { + $line = ''; + next; + } + if ($line =~ /\\$/o) { + chomp $line; + $line =~ s/^\s*(.*)\s*\\$/$1/o; + next; + } + push @done, $line; + $line = ''; + } + return @done; +} + + +=head2 $thing->encrypt(@stuff) + +Returns a one way hash (MD5) of the values appended together. + +=cut + +sub encrypt { + my $self = shift; + return md5_hex(join('',@_)); +} + +=head2 $utils_obj->es_time('field') OR noo_es_time($timestamp) + +Returns the epoch-second style timestamp for the value stored in +$utils_obj->{field}. Returns B<0> for an empty or invalid date stamp, and +assumes a PostgreSQL style datestamp to be supplied. + +=cut + +sub es_time { + my $self = shift; + my $part = shift; + my $es_part = $part.'_ES'; + return $$self{$es_part} if (exists($$self{$es_part}) && defined($$self{$es_part}) && $$self{$es_part}); + if (!$$self{$part} or $$self{$part} !~ /\d+/) { + return 0; + + } + my @tm = reverse($$self{$part} =~ /([\d\.]+)/og); + if ($tm[5] > 0) { + $tm[5] -= 1; + } + + return $$self{$es_part} = noo_es_time($$self{$part}); +} + +=head2 noo_es_time($timestamp) (non-OO es_time) + +Returns the epoch-second style timestamp for the B<$timestamp> passed +in. Returns B<0> for an empty or invalid date stamp, and +assumes a PostgreSQL style datestamp to be supplied. + +=cut + +sub noo_es_time { + my $timestamp = shift; + + my @tm = reverse($timestamp =~ /([\d\.]+)/og); + if ($tm[5] > 0) { + $tm[5] -= 1; + } + return timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]) || 1, int($tm[5]), int($tm[6]) || 2002 ); +} + + +=head2 $thing->interval_to_seconds('interval') OR interval_to_seconds('interval') + +=head2 $thing->seconds_to_interval($seconds) OR seconds_to_interval($seconds) + +Returns the number of seconds for any interval passed, or the interval for the seconds. +This is the generic version of B listed below. + +The interval must match the regex I, for example +B<2 weeks, 3 d and 1hour + 17 Months> or +B<1 year, 5 Months, 2 weeks, 3 days and 1 hour of seconds> meaning 46148400 seconds. + + my $expire_time = time() + $thing->interval_to_seconds('17h 9m'); + +The time size indicator may be one of + +=over 2 + +=item s[econd[s]] + +for seconds + +=item m[inute[s]] + +for minutes + +=item h[our[s]] + +for hours + +=item d[ay[s]] + +for days + +=item w[eek[s]] + +for weeks + +=item M[onth[s]] + +for months (really (365 * 1d)/12 ... that may get smarter, though) + +=item y[ear[s]] + +for years (this is 365 * 1d) + +=back + +=cut +sub interval_to_seconds { + my $self = shift; + my $interval = shift || $self; + + $interval =~ s/and/,/g; + $interval =~ s/,//g; + + my $amount = 0; + while ($interval =~ /\s*\+?\s*(\d+)\s*(\w{1})\w*\s*/g) { + $amount += $1 if ($2 eq 's'); + $amount += 60 * $1 if ($2 eq 'm'); + $amount += 60 * 60 * $1 if ($2 eq 'h'); + $amount += 60 * 60 * 24 * $1 if ($2 eq 'd'); + $amount += 60 * 60 * 24 * 7 * $1 if ($2 eq 'w'); + $amount += ((60 * 60 * 24 * 365)/12) * $1 if ($2 eq 'M'); + $amount += 60 * 60 * 24 * 365 * $1 if ($2 eq 'y'); + } + return $amount; +} + +sub seconds_to_interval { + my $self = shift; + my $interval = shift || $self; + + my $limit = shift || 's'; + $limit =~ s/^(.)/$1/o; + + my ($y,$ym,$M,$Mm,$w,$wm,$d,$dm,$h,$hm,$m,$mm,$s,$string); + my ($year, $month, $week, $day, $hour, $minute, $second) = + ('year','Month','week','day', 'hour', 'minute', 'second'); + + if ($y = int($interval / (60 * 60 * 24 * 365))) { + $string = "$y $year". ($y > 1 ? 's' : ''); + $ym = $interval % (60 * 60 * 24 * 365); + } else { + $ym = $interval; + } + return $string if ($limit eq 'y'); + + if ($M = int($ym / ((60 * 60 * 24 * 365)/12))) { + $string .= ($string ? ', ':'')."$M $month". ($M > 1 ? 's' : ''); + $Mm = $ym % ((60 * 60 * 24 * 365)/12); + } else { + $Mm = $ym; + } + return $string if ($limit eq 'M'); + + if ($w = int($Mm / 604800)) { + $string .= ($string ? ', ':'')."$w $week". ($w > 1 ? 's' : ''); + $wm = $Mm % 604800; + } else { + $wm = $Mm; + } + return $string if ($limit eq 'w'); + + if ($d = int($wm / 86400)) { + $string .= ($string ? ', ':'')."$d $day". ($d > 1 ? 's' : ''); + $dm = $wm % 86400; + } else { + $dm = $wm; + } + return $string if ($limit eq 'd'); + + if ($h = int($dm / 3600)) { + $string .= ($string ? ', ' : '')."$h $hour". ($h > 1 ? 's' : ''); + $hm = $dm % 3600; + } else { + $hm = $dm; + } + return $string if ($limit eq 'h'); + + if ($m = int($hm / 60)) { + $string .= ($string ? ', ':'')."$m $minute". ($m > 1 ? 's' : ''); + $mm = $hm % 60; + } else { + $mm = $hm; + } + return $string if ($limit eq 'm'); + + if ($s = int($mm)) { + $string .= ($string ? ', ':'')."$s $second". ($s > 1 ? 's' : ''); + } else { + $string = "Brand New!!!" unless ($string); + } + return $string; +} + +sub full { + my $self = shift; + $$self{empty} = 0; +} + +=head2 $utils_obj->set_psname('string') OR set_psname('string') + +Sets the name of this process in a B listing to B. + + +=cut + +sub set_psname { + my $self = shift; + my $PS_NAME = shift || $self; + $0 = $PS_NAME if ($PS_NAME); +} + +=head2 $utils_obj->daemonize('ps_name') OR daemonize('ps_name') + +Turns the current process into a daemon. B is optional, and is used +as the argument to I<< set_psname() >> if passed. + + +=cut + +sub daemonize { + my $self = shift; + my $PS_NAME = shift || $self; + my $pid; + if ($pid = safe_fork($self)) { + exit 0; + } elsif (defined($pid)) { + set_psname($PS_NAME); + chdir '/'; + setsid; + return $$; + } +} + +=head2 $utils_obj->safe_fork('ps_name') OR safe_fork('ps_name'); + +Forks the current process in a retry loop. B is optional, and is used +as the argument to I<< set_psname() >> if passed. + + +=cut + +sub safe_fork { + my $self = shift; + my $pid; + +FORK: + { + if (defined($pid = fork())) { + srand(time ^ ($$ + ($$ << 15))) unless ($pid); + return $pid; + } elsif ($! == EAGAIN) { + $self->error("Can't fork()! $!, taking 5 and trying again.") if (ref $self); + sleep 5; + redo FORK; + } else { + $self->error("Can't fork()! $!") if ($! && ref($self)); + exit $!; + } + } +} + +#------------------------------------------------------------------------------------------------------------------------------------ + + +1; diff --git a/src/perlmods/OpenSRF/Utils/Cache.pm b/src/perlmods/OpenSRF/Utils/Cache.pm new file mode 100644 index 0000000..cb54514 --- /dev/null +++ b/src/perlmods/OpenSRF/Utils/Cache.pm @@ -0,0 +1,63 @@ +package OpenSRF::Utils::Cache; +use strict; use warnings; +use base qw/Cache::Memcached OpenSRF/; +use Cache::Memcached; +use OpenSRF::Utils::Config; + + +=head OpenSRF::Utils::Cache + +This class just subclasses Cache::Memcached. +see Cache::Memcached for more options. + +The value passed to the call to current is the cache type +you wish to access. The below example sets/gets data +from the 'user' cache. + +my $cache = OpenSRF::Utils::Cache->current("user"); +$cache->set( "key1", "value1" [, $expire_secs ] ); +my $val = $cache->get( "key1" ); + + +=cut + +sub DESTROY {} +my %caches; + + +sub current { + + my ( $class, $c_type ) = @_; + return undef unless $c_type; + + return $caches{$c_type} if exists $caches{$c_type}; + return $caches{$c_type} = $class->new( $c_type ); + +} + + +sub new { + + my( $class, $c_type ) = @_; + return undef unless $c_type; + + return $caches{$c_type} if exists $caches{$c_type}; + + $class = ref( $class ) || $class; + + my $config = OpenSRF::Utils::Config->current; + my $cache_servers = $config->mem_cache->$c_type; + + my $instance = Cache::Memcached->new( { servers => $cache_servers } ); + + return bless( $instance, $class ); + +} + + +1; + + + + + diff --git a/src/perlmods/OpenSRF/Utils/Config.pm b/src/perlmods/OpenSRF/Utils/Config.pm new file mode 100755 index 0000000..29d3656 --- /dev/null +++ b/src/perlmods/OpenSRF/Utils/Config.pm @@ -0,0 +1,434 @@ +package OpenSRF::Utils::Config::Section; + +no strict 'refs'; + +use vars qw/@ISA $AUTOLOAD $VERSION/; +push @ISA, qw/OpenSRF::Utils/; + +use OpenSRF::Utils (':common'); + +$VERSION = do { my @r=(q$Revision$=~/\d+/g); sprintf "%d."."%02d"x$#r,@r }; + +my %SECTIONCACHE; +my %SUBSECTION_FIXUP; + +#use overload '""' => \&OpenSRF::Utils::Config::dump_ini; + +sub SECTION { + my $sec = shift; + return $sec->__id(@_); +} + +sub new { + my $self = shift; + my $class = ref($self) || $self; + + $self = bless {}, $class; + + my $lines = shift; + + for my $line (@$lines) { + + #($line) = split(/\s+\/\//, $line); + #($line) = split(/\s+#/, $line); + + if ($line =~ /^\s*\[([^\[\]]+)\]/) { + $self->_sub_builder('__id'); + $self->__id( $1 ); + next; + } + + my ($protokey,$value,$keytype,$key); + if ($line =~ /^([^=\s]+)\s*=\s*(.*)/s) { + ($protokey,$value) = ($1,$2); + ($keytype,$key) = split(/:/,$protokey); + } + + $key = $protokey unless ($key); + + if ($keytype ne $key) { + $keytype = lc $keytype; + if ($keytype eq 'list') { + $value = [split /\s*,\s*/, $value]; + } elsif ($keytype eq 'bool') { + $value = do{ $value =~ /^t|y|1/i ? 1 : 0; }; + } elsif ($keytype eq 'interval') { + $value = interval_to_seconds($value); + } elsif ($keytype eq 'subsection') { + if (exists $SECTIONCACHE{$value}) { + $value = $SECTIONCACHE{$value}; + } else { + $SUBSECTION_FIXUP{$value}{$self->SECTION} = $key ; + next; + } + } + } + + $self->_sub_builder($key); + $self->$key($value); + } + + no warnings; + if (my $parent_def = $SUBSECTION_FIXUP{$self->SECTION}) { + my ($parent_section, $parent_key) = each %$parent_def; + $SECTIONCACHE{$parent_section}->{$parent_key} = $self; + delete $SUBSECTION_FIXUP{$self->SECTION}; + } + + $SECTIONCACHE{$self->SECTION} = $self; + + return $self; +} + +package OpenSRF::Utils::Config; + +use vars qw/@ISA $AUTOLOAD $VERSION $OpenSRF::Utils::ConfigCache/; +push @ISA, qw/OpenSRF::Utils/; + +use FileHandle; +use OpenSRF::Utils (':common'); +use OpenSRF::Utils::Log (':levels'); + +#use overload '""' => \&OpenSRF::Utils::Config::dump_ini; + +sub import { + my $class = shift; + my $config_file = shift; + + return unless $config_file; + + $class->load( config_file => $config_file); +} + +sub dump_ini { + no warnings; + my $self = shift; + my $string; + my $included = 0; + if ($self->isa('OpenSRF::Utils::Config')) { + if (UNIVERSAL::isa(scalar(caller()), 'OpenSRF::Utils::Config' )) { + $included = 1; + } else { + $string = "# Main File: " . $self->FILE . "\n\n" . $string; + } + } + for my $section ( ('__id', grep { $_ ne '__id' } sort keys %$self) ) { + next if ($section eq 'env' && $self->isa('OpenSRF::Utils::Config')); + if ($section eq '__id') { + $string .= '['.$self->SECTION."]\n" if ($self->isa('OpenSRF::Utils::Config::Section')); + } elsif (ref($self->$section)) { + if (ref($self->$section) =~ /ARRAY/o) { + $string .= "list:$section = ". join(', ', @{$self->$section}) . "\n"; + } elsif (UNIVERSAL::isa($self->$section,'OpenSRF::Utils::Config::Section')) { + if ($self->isa('OpenSRF::Utils::Config::Section')) { + $string .= "subsection:$section = " . $self->$section->SECTION . "\n"; + next; + } else { + next if ($self->$section->{__sub} && !$included); + $string .= $self->$section . "\n"; + } + } elsif (UNIVERSAL::isa($self->$section,'OpenSRF::Utils::Config')) { + $string .= $self->$section . "\n"; + } + } else { + next if $section eq '__sub'; + $string .= "$section = " . $self->$section . "\n"; + } + } + if ($included) { + $string =~ s/^/## /gm; + $string = "# Subfile: " . $self->FILE . "\n#" . '-'x79 . "\n".'#include "'.$self->FILE."\"\n". $string; + } + + return $string; +} + +=head1 NAME + +OpenSRF::Utils::Config + + +=head1 SYNOPSIS + + + use OpenSRF::Utils::Config; + + my $config_obj = OpenSRF::Utils::Config->load( config_file => '/config/file.cnf' ); + + my $attrs_href = $config_obj->attributes(); + + $config_obj->attributes->no_db(0); + + open FH, '>'.$config_obj->FILE() . '.new'; + print FH $config_obj; + close FH; + + + +=head1 DESCRIPTION + + +This module is mainly used by other modules to load a configuration file. + + +=head1 NOTES + + +Hashrefs of sections can be returned by calling a method of the object of the same name as the section. +They can be set by passing a hashref back to the same method. Sections will B be autovivicated, though. + +Here be a config file example, HAR!: + + [datasource] + # backend=XMLRPC + backend=DBI + subsection:definition=devel_db + + [devel_db] + dsn=dbi:Pg(RaiseError => 0, AutoCommit => 1):dbname=dcl;host=nsite-dev + user=postgres + pw=postgres + #readonly=1 + + [live_db] + dsn=dbi:Pg(RaiseError => 0, AutoCommit => 1):dbname=dcl + user=n2dcl + pw=dclserver + #readonly=1 + + [devel_xmlrpc] + subsection:definition=devel_rpc + + [logs] + base=/var/log/nsite + debug=debug.log + error=error.log + + [debug] + enabled=1 + level=ALL + + [devel_rpc] + url=https://localhost:9000/ + proto=SSL + SSL_cipher_list=ALL + SSL_verify_mode=5 + SSL_use_cert=1 + SSL_key_file=client-key.pem + SSL_cert_file=client-cert.pem + SSL_ca_file=cacert.pem + log_level=4 + + [dirs] + base_dir=/home/miker/cvs/NOC/monitor_core/ + cert_dir=certs/ + + +=head1 METHODS + + +=cut + + +$VERSION = do { my @r=(q$Revision$=~/\d+/g); sprintf "%d."."%02d"x$#r,@r }; + + +=head2 OpenSRF::Utils::Config->load( config_file => '/some/config/file.cnf' ) + +Returns a OpenSRF::Utils::Config object representing the config file that was loaded. +The most recently loaded config file (hopefully the only one per app) +is stored at $OpenSRF::Utils::ConfigCache. Use OpenSRF::Utils::Config::current() to get at it. + + +=cut + +sub load { + my $pkg = shift; + $pkg = ref($pkg) || $pkg; + + my %args = @_; + + (my $new_pkg = $args{config_file}) =~ s/\W+/_/g; + $new_pkg .= "::$pkg"; + $new_section_pkg .= "${new_pkg}::Section"; + + { eval <<" PERL"; + + package $new_pkg; + use base $pkg; + sub section_pkg { return '$new_section_pkg'; } + + package $new_section_pkg; + use base "${pkg}::Section"; + + PERL + } + + return $new_pkg->_load( %args ); +} + +sub _load { + my $pkg = shift; + $pkg = ref($pkg) || $pkg; + my $self = {@_}; + bless $self, $pkg; + + no warnings; + if ((exists $$self{config_file} and OpenSRF::Utils::Config->current) and (OpenSRF::Utils::Config->current->FILE eq $$self{config_file}) and (!$self->{force})) { + delete $$self{force}; + return OpenSRF::Utils::Config->current(); + } + + $self->_sub_builder('__id'); + $self->FILE($$self{config_file}); + delete $$self{config_file}; + return undef unless ($self->FILE); + + $self->load_config(); + $self->load_env(); + $self->mangle_dirs(); + $self->mangle_logs(); + + $OpenSRF::Utils::ConfigCache = $self unless $self->nocache; + delete $$self{nocache}; + delete $$self{force}; + return $self; +} + +sub sections { + my $self = shift; + my %filters = @_; + + my @parts = (grep { UNIVERSAL::isa($_,'OpenSRF::Utils::Config::Section') } values %$self); + if (keys %filters) { + my $must_match = scalar(keys %filters); + my @ok_parts; + foreach my $part (@parts) { + my $part_count = 0; + for my $fkey (keys %filters) { + $part_count++ if ($part->$key eq $filters{$key}); + } + push @ok_parts, $part if ($part_count == $must_match); + } + return @ok_parts; + } + return @parts; +} + +sub current { + return $OpenSRF::Utils::ConfigCache; +} + +sub FILE { + return shift()->__id(@_); +} + +sub load_env { + my $self = shift; + my $host = `hostname -f` || `uname -n`; + chomp $host; + $$self{env} = $self->section_pkg->new; + $$self{env}{hostname} = $host; +} + +sub mangle_logs { + my $self = shift; + return unless ($self->logs && $self->dirs && $self->dirs->log_dir); + for my $i ( keys %{$self->logs} ) { + next if ($self->logs->$i =~ /^\//); + $self->logs->$i($self->dirs->log_dir."/".$self->logs->$i); + } +} + +sub mangle_dirs { + my $self = shift; + return unless ($self->dirs && $self->dirs->base_dir); + for my $i ( keys %{$self->dirs} ) { + if ( $i ne 'base_dir' ) { + next if ($self->dirs->$i =~ /^\//); + my $dir_tmp = $self->dirs->base_dir."/".$self->dirs->$i; + $dir_tmp =~ s#//#/#go; + $dir_tmp =~ s#/$##go; + $self->dirs->$i($dir_tmp); + } + } +} + +sub load_config { + my $self = shift; + my $config = new FileHandle $self->FILE, 'r'; + unless ($config) { + OpenSRF::Utils::Log->error("Could not open ".$self->FILE.": $!\n"); + die "Could not open ".$self->FILE.": $!\n"; + } + my @stripped_config = $self->__strip_comments($config) if (defined $config); + + my $chunk = []; + for my $line (@stripped_config) { + no warnings; + next unless ($line); + + if ($line =~ /^\s*\[/ and @$chunk) { + my $section = $self->section_pkg->new($chunk); + + my $sub_name = $section->SECTION; + $self->_sub_builder($sub_name); + $self->$sub_name($section); + + #$self->{$section->SECTION} = $section; + + $chunk = []; + push @$chunk,$line; + next; + } + if ($line =~ /^#\s*include\s+"(\S+)"\s*$/o) { + my $included_file = $1; + my $section = OpenSRF::Utils::Config->load(config_file => $included_file, nocache => 1); + + my $sub_name = $section->FILE; + $self->_sub_builder($sub_name); + $self->$sub_name($section); + + for my $subsect (keys %$section) { + next if ($subsect eq '__id'); + + $self->_sub_builder($subsect); + $self->$subsect($$section{$subsect}); + + #$self->$subsect($section->$subsect); + $self->$subsect->{__sub} = 1; + } + next; + } + + push @$chunk,$line; + } + my $section = $self->section_pkg->new($chunk) if (@$chunk); + my $sub_name = $section->SECTION; + $self->_sub_builder($sub_name); + $self->$sub_name($section); + +} + + +#------------------------------------------------------------------------------------------------------------------------------------ + +=head1 SEE ALSO + + OpenSRF::Utils + +=head1 BUGS + +No know bugs, but report any to miker@purplefrog.com. + +=head1 COPYRIGHT AND LICENSING + +Mike Rylander, Copyright 2000-2004 + +The OpenSRF::Utils::Config module is free software. You may distribute under the terms +of the GNU General Public License version 2 or greater. + +=cut + + +1; diff --git a/src/perlmods/OpenSRF/Utils/LogServer.pm b/src/perlmods/OpenSRF/Utils/LogServer.pm new file mode 100644 index 0000000..c27f512 --- /dev/null +++ b/src/perlmods/OpenSRF/Utils/LogServer.pm @@ -0,0 +1,149 @@ +package OpenSRF::Utils::LogServer; +use strict; use warnings; +use base qw(OpenSRF); +use IO::Socket::INET; +use FileHandle; +use OpenSRF::Utils::Config; +use Fcntl; +use Time::HiRes qw(gettimeofday); +use OpenSRF::Utils::Logger; + +=head2 Name + +OpenSRF::Utils::LogServer + +=cut + +=head2 Synopsis + +Networ Logger + +=cut + +=head2 Description + + +=cut + + + +our $config; +our $port; +our $bufsize = 4096; +our $proto; +our @file_info; + + +sub DESTROY { + for my $file (@file_info) { + if( $file->handle ) { + close( $file->handle ); + } + } +} + + +sub serve { + + $config = OpenSRF::Utils::Config->current; + + unless ($config) { throw OpenSRF::EX::Config ("No suitable config found"); } + + $port = $config->system->log_port; + $proto = $config->system->log_proto; + + + my $server = IO::Socket::INET->new( + LocalPort => $port, + Proto => $proto ) + or die "Error creating server socket : $@\n"; + + + + while ( 1 ) { + my $client = <$server>; + process( $client ); + } + + close( $server ); +} + +sub process { + my $client = shift; + my @params = split(/\|/,$client); + my $log = shift @params; + + if( (!$log) || (!@params) ) { + warn "Invalid logging params: $log\n"; + return; + } + + # Put |'s back in since they are stripped + # from the message by 'split' + my $message; + if( @params > 1 ) { + foreach my $param (@params) { + if( $param ne $params[0] ) { + $message .= "|"; + } + $message .= $param; + } + } + else{ $message = "@params"; } + + my @lines = split( "\n", $message ); + my $time = format_time(); + + my $fh; + + my ($f_obj) = grep { $_->name eq $log } @file_info; + + unless( $f_obj and ($fh=$f_obj->handle) ) { + my $file = $config->logs->$log; + + sysopen( $fh, $file, O_WRONLY|O_APPEND|O_CREAT ) + or warn "Cannot sysopen $log: $!"; + $fh->autoflush(1); + + my $obj = new OpenSRF::Utils::NetLogFile( $log, $file, $fh ); + push @file_info, $obj; + } + + foreach my $line (@lines) { + print $fh "$time $line\n" || die "$!"; + } + +} + +sub format_time { + my ($s, $ms) = gettimeofday(); + my @time = localtime( $s ); + $ms = substr( $ms, 0, 3 ); + my $year = $time[5] + 1900; + my $mon = $time[4] + 1; + my $day = $time[3]; + my $hour = $time[2]; + my $min = $time[1]; + my $sec = $time[0]; + $mon = "0" . "$mon" if ( length($mon) == 1 ); + $day = "0" . "$day" if ( length($day) == 1 ); + $hour = "0" . "$hour" if ( length($hour) == 1 ); + $min = "0" . "$min" if (length($min) == 1 ); + $sec = "0" . "$sec" if (length($sec) == 1 ); + + my $proc = $$; + while( length( $proc ) < 5 ) { $proc = "0" . "$proc"; } + return "[$year-$mon-$day $hour:$min:$sec.$ms $proc]"; +} + + +package OpenSRF::Utils::NetLogFile; + +sub new{ return bless( [ $_[1], $_[2], $_[3] ], $_[0] ); } + +sub name { return $_[0]->[0]; } +sub file { return $_[0]->[1]; } +sub handle { return $_[0]->[2]; } + + +1; diff --git a/src/perlmods/OpenSRF/Utils/Logger.pm b/src/perlmods/OpenSRF/Utils/Logger.pm new file mode 100644 index 0000000..8ebf12b --- /dev/null +++ b/src/perlmods/OpenSRF/Utils/Logger.pm @@ -0,0 +1,355 @@ +package OpenSRF::Utils::Logger; +use strict; +use vars qw($AUTOLOAD @EXPORT_OK %EXPORT_TAGS); +use Exporter; +use base qw/OpenSRF Exporter/; +use FileHandle; +use Time::HiRes qw(gettimeofday); +use OpenSRF::Utils::Config; +use Fcntl; + +@EXPORT_OK = qw/ NONE ERROR WARN INFO DEBUG INTERNAL /; + +%EXPORT_TAGS = ( level => [ qw/ NONE ERROR WARN INFO DEBUG INTERNAL / ] ); + +# XXX Update documentation + +=head1 Description + +OpenSRF::Utils::Logger + +General purpose logging package. The logger searches $config->logs->$log_name for the +actual file to log to. Any file in the config may be logged to. If the user attempts to +log to a log file that does not exist within the config, then the messages will to +to STDERR. + +There are also a set of predefined log levels. Currently they are +NONE, ERROR, WARN, INFO, DEBUG, INTERNAL, and ALL. You can select one of these log levels +when you send messages to the logger. The message will be logged if it is of equal or greater +'importance' than the global log level, found at $config->system->debug. If you don't specify +a log level, a defaul will be provided. Current defaults are: + +error -> ERROR + +debug -> DEBUG + +transport -> INTERNAL + +message -> INFO + +method -> INFO + +All others are logged to INFO by default. + +You write to a log by calling the log's method. + +use OpenSRF::Utils::Logger qw(:level); + +my $logger = "OpenSRF::Utils::Logger"; + +$logger->debug( "degug message" ); +$logger->transport( "debug message", DEBUG ); +$logger->blahalb( "I'll likely end up at STDERR with a log level of INFO" ); + +will only write the time, line number, and file that the method was called from. + +Note also that all messages with a log level of ERROR are written to the "error" log +in addition to the intended log file. + +=cut + +# Just set this first and not during every call to the logger +# XXX this should be added to the sig{hup} handler once it exists. + + +############## +# 1. check config, if file exists write to that file locally +# 2. If not in config and set to remote, send to socket. if not remote log to stderr + +my $config; +my $file_hash; +my $trace_active = 0; + +my $port; +my $proto; +my $peer; +my $socket; +my $remote_logging = 0; + +my $LEVEL = "OpenSRF::Utils::LogLevel"; +my $FILE = "OpenSRF::Utils::LogFile"; + +# --- Log levels - values and names + +my $none = $LEVEL->new( 1, "NONE" ); +my $error = $LEVEL->new( 10, "ERRR" ); +my $warn = $LEVEL->new( 20, "WARN" ); +my $info = $LEVEL->new( 30, "INFO" ); +my $debug = $LEVEL->new( 40, "DEBG" ); +my $internal = $LEVEL->new( 50, "INTL" ); +my $all = $LEVEL->new( 100, "ALL " ); + + +sub NONE { return $none; } +sub ERROR { return $error; } +sub WARN { return $warn; } +sub INFO { return $info; } +sub DEBUG { return $debug; } +sub INTERNAL { return $internal; } +sub ALL { return $all; } + +# Known log files and their default log levels +my $known_logs = [ + $FILE->new( "error", &ERROR ), + $FILE->new( "debug", &DEBUG ), + $FILE->new( "transport",&INTERNAL ), + $FILE->new( "message", &INFO ), + $FILE->new( "method", &INFO ), + ]; + + + + +# --------------------------------------------------------- + +{ + my $global_llevel; + sub global_llevel { return $global_llevel; } + + sub set_config { + + $config = OpenSRF::Utils::Config->current; + + if( defined($config) ) { + + $global_llevel = $config->system->debug; + $port = $config->system->log_port; + $proto = $config->system->log_proto; + $peer = $config->system->log_server; + $remote_logging = $config->system->remote_log; + + { + no strict "refs"; + $global_llevel = &{$global_llevel}; + } + #$trace_active = $config->system->trace; + build_file_hash(); + } + + else { + $global_llevel = DEBUG; + warn "*** Logger found no suitable config. Using STDERR ***\n"; + } + } +} + +sub build_file_hash { + $file_hash = {}; + # XXX This breaks Config encapsulation and should be cleaned. + foreach my $log ( grep { !($_ =~ /__id/) } (keys %{$config->logs}) ) { + $file_hash->{$log} = $config->logs->$log; + } +} + +# --------------------------------------------------------- + +sub AUTOLOAD { + + + my( $self, $string, $llevel ) = @_; + my $log = $AUTOLOAD; + $log =~ s/.*://; # strip fully-qualified portion + + unless( defined($config) or global_llevel() ) { + set_config(); + } + + # Build the sub here so we can use the enclosed $log variable. + # This is some weird Perl s*** that only satan could dream up. + # We mangle the symbol table so that future calls to $logger->blah + # will no longer require the autoload. + # The $log variable (above) will contain the name of the log + # log file the user is attempting to log to. This is true, however, + # even though the above code is presumably run only the first time + # the call to $logger->blah is made. + + no strict "refs"; + + *{$log} = sub { + + if( global_llevel()->level == NONE->level ) { return; } + + my( $class, $string, $llevel ) = @_; + + # see if we can return + if( $llevel ) { + # if level is passed in as a string, cast it to a level object + ref( $llevel ) || do{ $llevel = &{$llevel} }; + return if ($llevel->level > global_llevel()->level); + } + + else { # see if there is a default llevel, set to INFO if not. + my $log_obj; + foreach my $l ( @$known_logs ) { + if( $l->name eq $log ) { $log_obj = $l and last; } + } + if( $log_obj ) { $llevel = $log_obj->def_level; } + else { $llevel = INFO; } + } + + + # again, see if we can get out of this + return if ($llevel->level > global_llevel()->level); + + my @caller = caller(); + push( @caller, (caller(1))[3] ); + + # In the absense of a config, we write to STDERR + + if( ! defined($config) ) { + _write_stderr( $string, $llevel->name, @caller); + return; + } + + if( $remote_logging ) { + _write_net( $log, $string, $llevel->name, @caller ); + + } elsif ( my $file = $file_hash->{$log} ) { + _write_local( $file, $string, $llevel->name, @caller ); + + } else { + _write_stderr( $string, $llevel->name, @caller); + } + + + if( $llevel->name eq ERROR->name ) { # send all error to stderr + _write_stderr( $string, $llevel->name, @caller); + } + + if( $llevel->name eq ERROR->name and $log ne "error" ) { + if( my $e_file = $file_hash->{"error"} ) { + if( ! $remote_logging ) { + _write_local( $e_file, $string, $llevel->name, @caller ); + } + } + } + + }; + + $self->$log( $string, $llevel ); +} + + +# write_net expects a log_type_name and not a log_file_name for the first parameter +my $net_buffer = ""; +my $counter = 0; +sub _write_net { + + + my( $log, $string, $llevel, @caller ) = @_; + my( $pack, $file, $line_no ) = @caller; + my @lines = split( "\n", $string ); + + my $message = "$log|"."-" x 33 . + "\n$log|[$0 $llevel] $line_no $pack". + "\n$log|[$0 $llevel] $file"; + + foreach my $line (@lines) { + $message .= "\n$log|[$0 $llevel] $line"; + } + + $net_buffer .= "$message\n"; + + # every 4th load is sent on the socket + if( $counter++ % 4 ) { return; } + + unless( $socket ) { + $socket = IO::Socket::INET->new( + PeerAddr => $peer, + PeerPort => $port, + Proto => $proto ) + or die "Unable to open socket to log server"; + } + + $socket->send( $net_buffer ); + $net_buffer = ""; + +} + +sub _write_local { + + my( $log, $string, $llevel, @caller ) = @_; + my( $pack, $file, $line_no ) = @caller; + my @lines = split( "\n", $string ); + my $time = format_time(); + sysopen( SINK, $log, O_NONBLOCK|O_WRONLY|O_APPEND|O_CREAT ) + or die "Cannot sysopen $log: $!"; + binmode(SINK, ':utf8'); + print SINK "-" x 23 . "\n"; + print SINK "$time [$0 $llevel] $line_no $pack \n"; + print SINK "$time [$0 $llevel] $file\n"; + foreach my $line (@lines) { + print SINK "$time [$0 $llevel] $line\n"; + } + close( SINK ); + +} + +sub _write_stderr { + my( $string, $llevel, @caller ) = @_; + my( $pack, $file, $line_no ) = @caller; + my @lines = split( "\n", $string ); + my $time = format_time(); + print STDERR "-" x 23 . "\n"; + print STDERR "$time [$0 $llevel] $line_no $pack\n"; + print STDERR "$time [$0 $llevel] $file\n"; + foreach my $line (@lines) { + print STDERR "$time [$0 $llevel] $line\n"; + } +} + +sub format_time { + my ($s, $ms) = gettimeofday(); + my @time = localtime( $s ); + $ms = substr( $ms, 0, 3 ); + my $year = $time[5] + 1900; + my $mon = $time[4] + 1; + my $day = $time[3]; + my $hour = $time[2]; + my $min = $time[1]; + my $sec = $time[0]; + $mon = "0" . "$mon" if ( length($mon) == 1 ); + $day = "0" . "$day" if ( length($day) == 1 ); + $hour = "0" . "$hour" if ( length($hour) == 1 ); + $min = "0" . "$min" if (length($min) == 1 ); + $sec = "0" . "$sec" if (length($sec) == 1 ); + + my $proc = $$; + while( length( $proc ) < 5 ) { $proc = "0" . "$proc"; } + return "[$year-$mon-$day $hour:$min:$sec.$ms $proc]"; +} + + +# ---------------------------------------------- +# --- Models a log level +package OpenSRF::Utils::LogLevel; + +sub new { return bless( [ $_[1], $_[2] ], $_[0] ); } + +sub level { return $_[0]->[0]; } +sub name { return $_[0]->[1]; } + +# ---------------------------------------------- + +package OpenSRF::Utils::LogFile; +use OpenSRF::Utils::Config; + +sub new{ return bless( [ $_[1], $_[2] ], $_[0] ); } + +sub name { return $_[0]->[0]; } +sub def_level { return $_[0]->[1]; } + + +# ---------------------------------------------- + +1; diff --git a/src/router/Makefile b/src/router/Makefile new file mode 100644 index 0000000..9a5c0b5 --- /dev/null +++ b/src/router/Makefile @@ -0,0 +1,31 @@ +# set this shell variable prior to calling make to run with malloc_check enabled +#MALLOC_CHECK_=1 # XXX debug only + +CC = gcc +LIB_DIR=../../lib +CC_OPTS = -Wall -O2 -I /usr/include/libxml2 -I /usr/include/libxml2/libxml -I ../../include -I ../../../../cc/libxml2-2.6.16 +LD_OPTS = -lxml2 +EXE_LD_OPTS = -L $(LIB_DIR) -lxml2 -ltransport +LIB_SOURCES = generic_utils.c transport_socket.c transport_session.c transport_message.c transport_client.c + +TARGETS=generic_utils.o transport_socket.o transport_message.o transport_session.o transport_client.o + +all: router basic_client + +basic_client: lib + $(CC) $(CC_OPTS) $(EXE_LD_OPTS) basic_client.c -o $@ + +# --- Libs ----------------------------------------------- + +lib: + $(CC) -c $(CC_OPTS) $(LIB_SOURCES) + $(CC) -shared -W1 $(LD_OPTS) $(TARGETS) -o $(LIB_DIR)/libtransport.so + + +# The router is compiled as a static binary because of some +# necessary #defines that would break the library +router: + $(CC) $(LD_OPTS) -D_ROUTER $(CC_OPTS) $(LIB_SOURCES) transport_router.c -o $@ + +clean: + /bin/rm -f *.o ../../lib/libtransport.so router basic_client diff --git a/src/router/router.c b/src/router/router.c new file mode 100644 index 0000000..24dbdbe --- /dev/null +++ b/src/router/router.c @@ -0,0 +1,729 @@ +#include "transport_router.h" +#include +#include + + +char* router_resource; +transport_router_registrar* routt; + +void sig_hup_handler( int a ) { + router_registrar_free( routt ); + config_reader_free(); + log_free(); + free( router_resource ); + exit(0); +} + + +int main( int argc, char* argv[] ) { + + if( argc < 2 ) { + fatal_handler( "Usage: %s ", argv[0] ); + exit(0); + } + + + config_reader_init( argv[1] ); + if( conf_reader == NULL ) fatal_handler( "main(): Config is NULL" ); + + /* laod the config options */ + char* server = config_value("//router/transport/server"); + char* port = config_value("//router/transport/port"); + char* username = config_value("//router/transport/username"); + char* password = config_value("//router/transport/password"); + router_resource = config_value("//router/transport/resource"); + char* con_timeout = config_value("//router/transport/connect_timeout" ); + char* max_retries = config_value("//router/transport/max_reconnect_attempts" ); + + fprintf(stderr, "%s %s %s %s", server, port, username, password ); + + int iport = atoi( port ); + int con_itimeout = atoi( con_timeout ); + int max_retries_ = atoi(max_retries); + + if( iport < 1 ) { + fatal_handler( "Port is negative or 0" ); + return 99; + } + + + /* build the router_registrar */ + transport_router_registrar* router_registrar = + router_registrar_init( server, iport, username, password, router_resource, 0, con_itimeout ); + + routt = router_registrar; + + free(server); + free(port); + free(username); + free(password); + free(con_timeout); + free(max_retries); + + signal(SIGHUP,sig_hup_handler); + + + int counter = 0; + /* wait for incoming... */ + while( ++counter <= max_retries_ ) { + + /* connect to jabber */ + if( router_registrar_connect( router_registrar ) ) { + info_handler( "Connected..." ); + counter = 0; + listen_loop( router_registrar ); + } else + warning_handler( "Could not connect to Jabber" ); + + /* this should never happen */ + warning_handler( "Jabber server probably went away, attempting reconnect" ); + + sleep(5); + } + + + router_registrar_free( router_registrar ); + config_reader_free(); + return 1; + +} + +transport_router_registrar* router_registrar_init( char* server, + int port, char* username, char* password, + char* resource, int client_timeout, int con_timeout ) { + + if( server == NULL ) { return NULL; } + + /* allocate a new router_registrar object */ + size_t size = sizeof( transport_router_registrar ); + transport_router_registrar* router_registrar = (transport_router_registrar*) safe_malloc( size ); + + router_registrar->client_timeout = client_timeout; + router_registrar->jabber = jabber_connect_init( server, port, username, password, resource, con_timeout ); + return router_registrar; + +} + +jabber_connect* jabber_connect_init( char* server, + int port, char* username, char* password, char* resource, int connect_timeout ) { + + size_t len = sizeof(jabber_connect); + jabber_connect* jabber = (jabber_connect*) safe_malloc( len ); + + jabber->port = port; + jabber->connect_timeout = connect_timeout; + + jabber->server = strdup(server); + jabber->username = strdup(username); + jabber->password = strdup(password); + jabber->resource = strdup(resource); + + if( jabber->server == NULL || jabber->username == NULL || + jabber->password == NULL || jabber->resource == NULL ) { + fatal_handler( "jabber_init(): Out of Memory" ); + return NULL; + } + + /* build the transport client */ + jabber->t_client = client_init( jabber->server, jabber->port ); + + return jabber; +} + +/* connect the router_registrar to jabber */ +int router_registrar_connect( transport_router_registrar* router ) { + return j_connect( router->jabber ); +} + +/* connect a jabber_connect object jabber */ +int j_connect( jabber_connect* jabber ) { + if( jabber == NULL ) { return 0; } + return client_connect( jabber->t_client, + jabber->username, jabber->password, jabber->resource, jabber->connect_timeout ); +} + +int fill_fd_set( transport_router_registrar* router, fd_set* set ) { + + int max_fd; + FD_ZERO(set); + + int router_fd = router->jabber->t_client->session->sock_obj->sock_fd; + max_fd = router_fd; + FD_SET( router_fd, set ); + + server_class_node* cur_node = router->server_class_list; + while( cur_node != NULL ) { + int cur_class_fd = cur_node->jabber->t_client->session->sock_obj->sock_fd; + if( cur_class_fd > max_fd ) + max_fd = cur_class_fd; + FD_SET( cur_class_fd, set ); + cur_node = cur_node->next; + } + + FD_CLR( 0, set ); + return max_fd; +} + + +void listen_loop( transport_router_registrar* router ) { + + if( router == NULL ) + return; + + int select_ret; + int router_fd = router->jabber->t_client->session->sock_obj->sock_fd; + transport_message* cur_msg; + + while(1) { + + fd_set listen_set; + int max_fd = fill_fd_set( router, &listen_set ); + + if( max_fd < 1 ) + fatal_handler( "fill_fd_set return bogus max_fd: %d", max_fd ); + + int num_handled = 0; + info_handler( "Going into select" ); + + if( (select_ret=select(max_fd+ 1, &listen_set, NULL, NULL, NULL)) < 0 ) { + + warning_handler( "Select returned error %d", select_ret ); + warning_handler( "Select Error %d on fd %d", errno ); + perror( "Select Error" ); + warning_handler( "Errors: EBADF %d, EINTR %d, EINVAL %d, ENOMEM %d", + EBADF, EINTR, EINVAL, ENOMEM ); + continue; + + } else { + + info_handler( "Select returned %d", select_ret ); + + if( FD_ISSET( router_fd, &listen_set ) ) { + cur_msg = client_recv( router->jabber->t_client, 1 ); + router_registrar_handle_msg( router, cur_msg ); + message_free( cur_msg ); + if( ++num_handled == select_ret ) + continue; + } + + /* cycle through the children and find any whose fd's are ready for reading */ + server_class_node* cur_node = router->server_class_list; + while( cur_node != NULL ) { + info_handler("searching child activity" ); + int cur_fd = cur_node->jabber->t_client->session->sock_obj->sock_fd; + + if( FD_ISSET(cur_fd, &listen_set) ) { + ++num_handled; + FD_CLR(cur_fd,&listen_set); + info_handler( "found active child %s", cur_node->server_class ); + + cur_msg = client_recv( cur_node->jabber->t_client, 1 ); + info_handler( "%s received from %s", cur_node->server_class, cur_msg->sender ); + int handle_ret = server_class_handle_msg( router, cur_node, cur_msg ); + + if( handle_ret == -1 ) { + warning_handler( "server_class_handle_msg() returned -1" ); + cur_node = router->server_class_list; /*start over*/ + continue; + + } else if( handle_ret == 0 ) { + /* delete and continue */ + warning_handler( "server_class_handle_msg() returned 0" ); + server_class_node* tmp_node = cur_node->next; + remove_server_class( router, cur_node ); + cur_node = tmp_node; + continue; + } + + info_handler( "%s handled message successfully", cur_node->server_class ); + /* dont free message here */ + if( num_handled == select_ret ) + break; + } + if( num_handled == select_ret ) + break; + cur_node = cur_node->next; + + } /* cycling through the server_class list */ + + } /* no select errors */ + } +} + + +/* determine where to route top level messages */ +int router_registrar_handle_msg( transport_router_registrar* router_registrar, transport_message* msg ) { + + info_handler( "Received class: %s : command %s: body: %s", msg->router_class, msg->router_command, msg->body ); + + if( router_registrar == NULL || msg == NULL ) { return 0; } + + info_handler("Looking for server_class_node %s...",msg->router_class); + server_class_node* active_class_node = find_server_class( router_registrar, msg->router_class ); + + if( active_class_node == NULL ) { + info_handler("Could not find server_class_node %s, creating one.",msg->router_class); + + /* there is no server_class for msg->router_class so we build it here */ + if( strcmp( msg->router_command, "register") == 0 ) { + + info_handler("Adding server_class_node for %s",msg->router_class); + active_class_node = + init_server_class( router_registrar, msg->sender, msg->router_class ); + + if( active_class_node == NULL ) { + fatal_handler( "router_listen(): active_class_node == NULL for %s", msg->sender ); + return 0; + } + + if (router_registrar->server_class_list != NULL) { + active_class_node->next = router_registrar->server_class_list; + router_registrar->server_class_list->prev = active_class_node; + } + router_registrar->server_class_list = active_class_node; + + //spawn_server_class( (void*) active_class_node ); + + } else { + warning_handler( "router_register_handler_msg(): Bad Command [%s] for class [%s]", + msg->router_command, msg->router_class ); + } + + } else if( strcmp( msg->router_command, "register") == 0 ) { + /* there is a server_class for msg->router_class so we + need to either add a new server_node or update the existing one */ + + + server_node* s_node = find_server_node( active_class_node, msg->sender ); + + if( s_node != NULL ) { + s_node->available = 1; + s_node->upd_time = time(NULL); + info_handler( "Found matching registered server: %s. Updating.", + s_node->remote_id ); + } else { + s_node = init_server_node( msg->sender ); + + info_handler( "Adding server_node for: %s.", s_node->remote_id ); + + if (s_node == NULL ) { + warning_handler( " Could not create new xerver_node for %s.", + msg->sender ); + return 0; + } + + s_node->next = active_class_node->current_server_node->next; + s_node->prev = active_class_node->current_server_node; + + active_class_node->current_server_node->next->prev = s_node; + active_class_node->current_server_node->next = s_node; + } + + + } else if( strcmp( msg->router_command, "unregister") == 0 ) { + + if( ! unregister_server_node( active_class_node, msg->sender ) ) + remove_server_class( router_registrar, active_class_node ); + + } else { + warning_handler( "router_register_handler_msg(): Bad Command [%s] for class [%s]", + msg->router_command, msg->router_class ); + } + + return 1; +} + + +/* removes a server class node from the top level router_registrar */ +int unregister_server_node( server_class_node* active_class_node, char* remote_id ) { + + server_node* d_node = find_server_node( active_class_node, remote_id ); + + if ( d_node != NULL ) { + + info_handler( "Removing server_node for: %s.", d_node->remote_id ); + + if ( d_node->next == NULL ) { + warning_handler( "NEXT is NULL in ring [%s] -- " + "THIS SHOULD NEVER HAPPEN", + d_node->remote_id ); + + } + + if ( d_node->prev == NULL ) { + warning_handler( "PREV is NULL in a ring [%s] -- " + "THIS SHOULD NEVER HAPPEN", + d_node->remote_id ); + + } + + if ( d_node->next == d_node && d_node->prev == d_node) { + info_handler( "Last node, setting ring to NULL: %s.", + d_node->remote_id ); + + active_class_node->current_server_node = NULL; + + server_node_free( d_node ); + return 0; + + } else { + info_handler( "Nodes remain, splicing: %s, %s", + d_node->prev->remote_id, + d_node->next->remote_id); + + info_handler( "d_node => %x, next => %x, prev => %x", + d_node, d_node->next, d_node->prev ); + + + d_node->prev->next = d_node->next; + d_node->next->prev = d_node->prev; + + info_handler( "prev => %x, prev->next => %x, prev->prev => %x", + d_node->prev, d_node->prev->next, d_node->prev->prev ); + + info_handler( "next => %x, next->next => %x, next->prev => %x", + d_node->next, d_node->next->next, d_node->next->prev ); + + if (active_class_node->current_server_node == d_node) + active_class_node->current_server_node = d_node->next; + + + server_node_free( d_node ); + } + } + + return 1; +} + +server_node * find_server_node ( server_class_node * class, const char * remote_id ) { + + if ( class == NULL ) { + warning_handler(" find_server_node(): bad arg!"); + return NULL; + } + + server_node * start_node = class->current_server_node; + server_node * node = class->current_server_node; + + do { + if (node == NULL) + return NULL; + + if ( strcmp(node->remote_id, remote_id) == 0 ) + return node; + + node = node->next; + + } while ( node != start_node ); + + return NULL; +} + +/* if we return -1, then we just deleted the server_class you were looking for + if we return 0, then some other error has occured + we return 1 otherwise */ +int remove_server_class( transport_router_registrar* router, server_class_node* class ) { + if( class == NULL ) + return 0; + + transport_message * msg = NULL; + while ( (msg = client_recv(class->jabber->t_client, 0)) != NULL ) { + server_class_handle_msg(router, class, msg); + message_free(msg); + } + + free( class->server_class ); + class->server_class = NULL; + + find_server_class( router, router_resource ); /* find deletes for us */ + + if( router->server_class_list == NULL ) + return 0; + return 1; +} + +server_class_node * find_server_class ( transport_router_registrar * router, const char * class_id ) { + + if ( router == NULL ) { + warning_handler(" find_server_class(): bad arg!"); + return NULL; + } + + info_handler( "Finding server class for %s", class_id ); + server_class_node * class = router->server_class_list; + server_class_node * dead_class = NULL; + + while ( class != NULL ) { + + if ( class->server_class == NULL ) { + info_handler( "Found an empty server class" ); + + if ( class->prev != NULL ) { + class->prev->next = class->next; + if( class->next != NULL ) { + class->next->prev = class->prev; + } + + } else { + info_handler( "Empty class is the first on the list" ); + if( class->next != NULL ) + router->server_class_list = class->next; + + else { /* we're the last class node in the class node list */ + info_handler( "Empty class is the last on the list" ); + server_class_node_free( router->server_class_list ); + router->server_class_list = NULL; + break; + } + + } + + dead_class = class; + class = class->next; + + info_handler( "Tossing our dead class" ); + server_class_node_free( dead_class ); + + if ( class == NULL ) + return NULL; + } + + if ( strcmp(class->server_class, class_id) == 0 ) + return class; + info_handler( "%s != %s", class->server_class, class_id ); + + class = class->next; + } + + return NULL; +} + +/* builds a new server class and connects to the jabber server with the new resource */ +server_class_node* init_server_class( + transport_router_registrar* router, char* remote_id, char* server_class ) { + + size_t len = sizeof( server_class_node ); + server_class_node* node = (server_class_node*) safe_malloc( len ); + + node->jabber = jabber_connect_init( router->jabber->server, + router->jabber->port, router->jabber->username, + router->jabber->password, server_class, router->jabber->connect_timeout ); + + + + node->server_class = strdup( server_class ); + if( server_class == NULL ) { + fatal_handler( "imit_server_class(): out of memory for %s", server_class ); + return NULL; + } + + info_handler( "Received class to init_server_class: %s", server_class ); + node->current_server_node = init_server_node( remote_id ); + if( node->current_server_node == NULL ) { + fatal_handler( "init_server_class(): NULL server_node for %s", remote_id ); + return NULL; + } + + + if( ! j_connect( node->jabber ) ) { + fatal_handler( "Unable to init server class %s", node->server_class ); + return NULL; + } + + info_handler( "Jabber address in init for %s : address %x : username %s : resource %s", + node->server_class, node->jabber->t_client->session->sock_obj->sock_fd, + node->jabber->username, node->jabber->resource ); + + return node; + +} + +/* builds a new server_node to be added to the ring of server_nodes */ +server_node* init_server_node( char* remote_id ) { + + info_handler( "Initing server node for %s", remote_id ); + server_node* current_server_node; + size_t size = sizeof( server_node); + current_server_node = (server_node*) safe_malloc( size ); + + current_server_node->remote_id = strdup(remote_id); + if( current_server_node->remote_id == NULL ) { + fatal_handler("init_server_class(): Out of Memory for %s", remote_id ); + return NULL; + } + + current_server_node->reg_time = time(NULL); + current_server_node->available = 1; + current_server_node->next = current_server_node; + current_server_node->prev = current_server_node; + + + return current_server_node; + +} + +int server_class_handle_msg( transport_router_registrar* router, + server_class_node* s_node, transport_message* msg ) { + + if( s_node->current_server_node == NULL ) { + /* return error to client ??!*/ + /* WE have no one to send the message to */ + warning_handler( "We no longer have any servers for %s : " + "no one to send the message to. Sending error message to %s", s_node->server_class, msg->sender ); + free( msg->recipient ); + + char* rec = strdup( msg->sender ); + if( rec == NULL ) { + fatal_handler( "class msg_handler: out of memory"); + return 0; + } + + info_handler( "Building error message to return for %s", s_node->server_class); + msg->recipient = rec; + set_msg_error(msg, "cancel", 501); + + client_send_message( s_node->jabber->t_client, msg ); + message_free( msg ); + + remove_server_class( router, s_node ); + + return -1; + } + + info_handler( "[%s] Received from %s to \n%s", + s_node->server_class, msg->sender, msg->recipient ); + + if( msg->is_error ) { + warning_handler( "We've received an error message type: %s : code: %d", + msg->error_type, msg->error_code ); + + if( strcmp( msg->error_type, "cancel" ) == 0 ) { + warning_handler( "Looks like we've lost a server!" ); + server_node* dead_node = find_server_node( s_node, msg->sender ); + + if( dead_node != NULL ) { + //message_free( msg ); + transport_message* tmp = dead_node->last_sent; + + /* copy over last sent, it will be freed in the unregister function */ + transport_message* tmp2 = message_init( tmp->body, tmp->subject, tmp->thread, + tmp->recipient, tmp->sender ); + + message_set_router_info( tmp2, tmp->router_from, + tmp->router_to, tmp->router_class, tmp->router_command, tmp->broadcast ); + + if( ! unregister_server_node( s_node, dead_node->remote_id ) ) { + /* WE have no one to send the message to */ + warning_handler( "We no longer have any servers for %s : " + "no one to send the message to.", s_node->server_class ); + free( msg->recipient ); + + char* rec = strdup( msg->router_from ); + if( rec == NULL ) { + fatal_handler( "class msg_handler: out of memory"); + return 0; + } + + info_handler( "Building error message to return for %s", s_node->server_class); + msg->recipient = rec; + client_send_message( s_node->jabber->t_client, msg ); + message_free( tmp2 ); + message_free( msg ); + return 0; + + } else { + msg = tmp2; + } + } + } + } + + + server_node* c_node = s_node->current_server_node->next; + + /* not implemented yet */ + while( ! c_node->available ) { + if( c_node == s_node->current_server_node ) { + warning_handler("No server_node's are available for %s", s_node->server_class ); + /* XXX send error message to client */ + return 0; + } + c_node = c_node->next; + } + s_node->current_server_node = c_node; + + transport_message * new_msg = + message_init( msg->body, msg->subject, msg->thread, + s_node->current_server_node->remote_id, msg->sender ); + + message_set_router_info( new_msg, NULL, msg->sender, NULL, NULL, 0 ); + + info_handler( "[%s] Routing message from [%s]\nto [%s]", s_node->server_class, new_msg->sender, new_msg->recipient ); + + message_free( s_node->current_server_node->last_sent ); + s_node->current_server_node->last_sent = msg; + + if ( new_msg != NULL && client_send_message( s_node->jabber->t_client, new_msg ) ) { + s_node->current_server_node->serve_count++; + s_node->current_server_node->la_time = time(NULL); + message_free( new_msg ); // XXX + return 1; + } + info_handler( "message sent" ); + message_free( new_msg ); // XXX + + return 0; +} + +int router_registrar_free( transport_router_registrar* router_registrar ) { + if( router_registrar == NULL ) return 0; + jabber_connect_free( router_registrar->jabber ); + + /* free the server_class list XXX */ + while( router_registrar->server_class_list != NULL ) { + remove_server_class(router_registrar, router_registrar->server_class_list); + } + + free( router_registrar ); + + + + return 1; +} + + +int server_class_node_free( server_class_node* node ) { + if( node == NULL ) { return 0; } + if( node->server_class != NULL ) + free( node->server_class ); + + jabber_connect_free( node->jabber ); + + /* just in case, free the list */ + while( node->current_server_node != NULL ) { + unregister_server_node( node, node->current_server_node->remote_id ); + } + free( node ); + return 1; +} + +int server_node_free( server_node* node ) { + if( node == NULL ) { return 0; } + message_free( node->last_sent ); + free( node->remote_id ); + free( node ); + return 1; +} + +int jabber_connect_free( jabber_connect* jabber ) { + if( jabber == NULL ) { return 0; } + client_free( jabber->t_client ); + free( jabber->username ); + free( jabber->password ); + free( jabber->resource ); + free( jabber->server ); + free( jabber ); + return 1; +} + + diff --git a/src/router/router.h b/src/router/router.h new file mode 100644 index 0000000..4cdbfbd --- /dev/null +++ b/src/router/router.h @@ -0,0 +1,234 @@ +#include "transport_client.h" +#include "transport_message.h" +#include +#include + +#ifndef TRANSPORT_ROUTER_H +#define TRANSPORT_ROUTER_H + +// ---------------------------------------------------------------------- +// Jabber router_registrar/load balancer. There is a top level linked list of +// server_class_nodes. A server class represents the a cluster of Jabber +// clients that define a single logical routing endpoint. Each of these +// server_class_nodes maintains a list of connected server_nodes, which +// represents the pool of connected server endpoints. A request +// directed at a particular class is routed to the next available +// server endpoint. +// +// ---------------------------------------------------------------------- + + +// ---------------------------------------------------------------------- +// Defines an element in a server list. The server list is a circular +// doubly linked list. User is responsible for freeing a server_node with +// server_node_free() +// ---------------------------------------------------------------------- +struct server_node_struct { + + struct server_node_struct* next; + struct server_node_struct* prev; + + time_t la_time; /* last time we sent a message to a server */ + time_t reg_time; /* time we originally registered */ + time_t upd_time; /* last re-register time */ + int available; /* true if we may be used */ + + int serve_count; /* how many messages we've sent */ + + /* jabber remote id for this server node*/ + char* remote_id; + + /* we cache the last sent message in case our remote + endpoint has gone away. If it has, the next server + node in the list will re-send our last message */ + transport_message* last_sent; + +}; +typedef struct server_node_struct server_node; + + +// ---------------------------------------------------------------------- +// Models a basic jabber connection structure. Any component that +// connects to jabber will have one of these. +// ---------------------------------------------------------------------- +struct jabber_connect_struct { + + char* server; + int port; + char* username; + char* password; + char* resource; + int connect_timeout; + + transport_client* t_client; +}; +typedef struct jabber_connect_struct jabber_connect; + + + +// ---------------------------------------------------------------------- +// Defines an element in the list of server classes. User is +// responsible for freeing a server_class_node with +// server_class_node_free(). +// The server_node_list is a doubly (not circular) linked list +// ---------------------------------------------------------------------- +struct server_class_node_struct { + + /* the name of our class. This will be used as the jabber + resource when we create a class level connection*/ + char* server_class; + + /* the current node in the ring of available server nodes */ + server_node* current_server_node; + + /* next and prev class_node pointers */ + struct server_class_node_struct* next; + struct server_class_node_struct* prev; + + /* our jabber connection struct */ + jabber_connect* jabber; + +}; +typedef struct server_class_node_struct server_class_node; + +// ---------------------------------------------------------------------- +// Top level router_registrar object. Maintains the list of +// server_class_nodes and the top level router jabber connection. +// ---------------------------------------------------------------------- +struct transport_router_registrar_struct { + + /* the list of server class nodes */ + server_class_node* server_class_list; + + /* if we don't hear from the client in this amount of time + we consider them dead... */ + /* not currently used */ + int client_timeout; /* seconds */ + + /* our top level connection to the jabber server */ + jabber_connect* jabber; + + +}; +typedef struct transport_router_registrar_struct transport_router_registrar; + + + + +// ---------------------------------------------------------------------- +// Returns an allocated transport_router_registrar. The user is responsible for +// freeing the allocated memory with router_registrar_free() +// client_timeout is unused at this time. +// connect_timeout is how long we will wait for a failed jabber connect +// attempt for the top level connection. +// ---------------------------------------------------------------------- +transport_router_registrar* router_registrar_init( char* server, + int port, char* username, char* password, char* resource, + int client_timeout, int connect_timeout ); + +// ---------------------------------------------------------------------- +// Connects the top level router_registrar object to the Jabber server. +// ---------------------------------------------------------------------- +int router_registrar_connect( transport_router_registrar* router ); + +// ---------------------------------------------------------------------- +// Connects the given jabber_connect object to the Jabber server +// ---------------------------------------------------------------------- +int j_connect( jabber_connect* jabber ); + + +// ---------------------------------------------------------------------- +// Builds and initializes a jabber_connect object. User is responsible +// for freeing the memory with jabber_connect_free(); +// ---------------------------------------------------------------------- +jabber_connect* jabber_connect_init( char* server, + int port, char* username, char* password, + char* resource, int connect_timeout ); + +// ---------------------------------------------------------------------- +// Allocates and initializes a server class instance. This will be +// called when a new class message arrives. It will connect to Jabber +// as router_registrar->username@router_registrar->server/new_class +// ---------------------------------------------------------------------- +server_class_node* init_server_class( + transport_router_registrar* router_registrar, char* remote_id, char* server_class ); + +// ---------------------------------------------------------------------- +// Allocates and initializes a server_node object. The object must +// be freed with server_node_free(). +// remote_id is the full jabber login for the remote server connection +// I.e. where we send messages when we want to send them to this +// server. +// ---------------------------------------------------------------------- +server_node* init_server_node( char* remote_id ); + + +// ---------------------------------------------------------------------- +// Routes messages sent to the provided server_class_node's class +// ---------------------------------------------------------------------- +int server_class_handle_msg( transport_router_registrar* router, + server_class_node* s_node, transport_message* msg ); + +// ---------------------------------------------------------------------- +// Determines what to do with an inbound register/unregister message. +// ---------------------------------------------------------------------- +int router_registrar_handle_msg( transport_router_registrar*, transport_message* msg ); + +// ---------------------------------------------------------------------- +// Deallocates the memory occupied by the given server_node +// ---------------------------------------------------------------------- +int server_node_free( server_node* node ); + +// ---------------------------------------------------------------------- +// Deallocates the memory used by the given server_class_node. This +// will also free any attached server_node's. +// ---------------------------------------------------------------------- +int server_class_node_free( server_class_node* node ); + +// ---------------------------------------------------------------------- +// Deallocates the memory used by a server_node +// ---------------------------------------------------------------------- +int server_node_free( server_node* node ); + + +// ---------------------------------------------------------------------- +// Deallocates a jabber_connect node +// ---------------------------------------------------------------------- +int jabber_connect_free( jabber_connect* jabber ); + +// ---------------------------------------------------------------------- +// Deallocates the memory used by the router_registrar. This will also call +// server_class_node_free on any attached server_class_nodes. +// ---------------------------------------------------------------------- +int router_registrar_free( transport_router_registrar* router_registrar ); + + +// ---------------------------------------------------------------------- +// Returns the server_node with the given Jabber remote_id +// ---------------------------------------------------------------------- +server_node * find_server_node ( server_class_node * class, const char * remote_id ); + + +// ---------------------------------------------------------------------- +// Returns the server_class_node object with the given class_name +// ---------------------------------------------------------------------- +server_class_node * find_server_class ( transport_router_registrar * router, const char * class_id ); + +// ---------------------------------------------------------------------- +// Removes a server class from the top level router_registrar +// ---------------------------------------------------------------------- +int unregister_server_node( server_class_node* active_class_node, char* remote_id ); + +int fill_fd_set( transport_router_registrar* router, fd_set* set ); + +void listen_loop( transport_router_registrar* router ); + + +int remove_server_class( transport_router_registrar* router, server_class_node* class ); +// ---------------------------------------------------------------------- +// Adds a handler for the SIGUSR1 that we send to wake all the +// listening threads. +// ---------------------------------------------------------------------- +//void sig_handler( int sig ); + +#endif -- 2.11.0