From 2ac684f53df3f76aacb4d1be79d7db64567f1964 Mon Sep 17 00:00:00 2001 From: Federico Marani Date: Fri, 20 Feb 2009 18:57:20 +0000 Subject: * implemented STOMP support for external message queueing systems * (ticket-724) * included PHP STOMP libraries (http://code.google.com/p/stompcli/) --- extlib/Stomp.php | 594 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 extlib/Stomp.php (limited to 'extlib/Stomp.php') diff --git a/extlib/Stomp.php b/extlib/Stomp.php new file mode 100644 index 000000000..9e1c97b3b --- /dev/null +++ b/extlib/Stomp.php @@ -0,0 +1,594 @@ + + * @author Dejan Bosanac + * @author Michael Caplan + * @version $Revision: 43 $ + */ +class Stomp +{ + /** + * Perform request synchronously + * + * @var boolean + */ + public $sync = false; + + /** + * Default prefetch size + * + * @var int + */ + public $prefetchSize = 1; + + /** + * Client id used for durable subscriptions + * + * @var string + */ + public $clientId = null; + + protected $_brokerUri = null; + protected $_socket = null; + protected $_hosts = array(); + protected $_params = array(); + protected $_subscriptions = array(); + protected $_defaultPort = 61613; + protected $_currentHost = - 1; + protected $_attempts = 10; + protected $_username = ''; + protected $_password = ''; + protected $_sessionId; + protected $_read_timeout_seconds = 60; + protected $_read_timeout_milliseconds = 0; + + /** + * Constructor + * + * @param string $brokerUri Broker URL + * @throws Stomp_Exception + */ + public function __construct ($brokerUri) + { + $this->_brokerUri = $brokerUri; + $this->_init(); + } + /** + * Initialize connection + * + * @throws Stomp_Exception + */ + protected function _init () + { + $pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i"; + if (preg_match($pattern, $this->_brokerUri, $regs)) { + $scheme = $regs[2]; + $hosts = $regs[3]; + $params = $regs[4]; + if ($scheme != "failover") { + $this->_processUrl($this->_brokerUri); + } else { + $urls = explode(",", $hosts); + foreach ($urls as $url) { + $this->_processUrl($url); + } + } + if ($params != null) { + parse_str($params, $this->_params); + } + } else { + require_once 'Stomp/Exception.php'; + throw new Stomp_Exception("Bad Broker URL {$this->_brokerUri}"); + } + } + /** + * Process broker URL + * + * @param string $url Broker URL + * @throws Stomp_Exception + * @return boolean + */ + protected function _processUrl ($url) + { + $parsed = parse_url($url); + if ($parsed) { + array_push($this->_hosts, array($parsed['host'] , $parsed['port'] , $parsed['scheme'])); + } else { + require_once 'Stomp/Exception.php'; + throw new Stomp_Exception("Bad Broker URL $url"); + } + } + /** + * Make socket connection to the server + * + * @throws Stomp_Exception + */ + protected function _makeConnection () + { + if (count($this->_hosts) == 0) { + require_once 'Stomp/Exception.php'; + throw new Stomp_Exception("No broker defined"); + } + + // force disconnect, if previous established connection exists + $this->disconnect(); + + $i = $this->_currentHost; + $att = 0; + $connected = false; + while (! $connected && $att ++ < $this->_attempts) { + if (isset($this->_params['randomize']) && $this->_params['randomize'] == 'true') { + $i = rand(0, count($this->_hosts) - 1); + } else { + $i = ($i + 1) % count($this->_hosts); + } + $broker = $this->_hosts[$i]; + $host = $broker[0]; + $port = $broker[1]; + $scheme = $broker[2]; + if ($port == null) { + $port = $this->_defaultPort; + } + if ($this->_socket != null) { + fclose($this->_socket); + $this->_socket = null; + } + $this->_socket = @fsockopen($scheme . '://' . $host, $port); + if (!is_resource($this->_socket) && $att >= $this->_attempts && !array_key_exists($i + 1, $this->_hosts)) { + require_once 'Stomp/Exception.php'; + throw new Stomp_Exception("Could not connect to $host:$port ($att/{$this->_attempts})"); + } else if (is_resource($this->_socket)) { + $connected = true; + $this->_currentHost = $i; + break; + } + } + if (! $connected) { + require_once 'Stomp/Exception.php'; + throw new Stomp_Exception("Could not connect to a broker"); + } + } + /** + * Connect to server + * + * @param string $username + * @param string $password + * @return boolean + * @throws Stomp_Exception + */ + public function connect ($username = '', $password = '') + { + $this->_makeConnection(); + if ($username != '') { + $this->_username = $username; + } + if ($password != '') { + $this->_password = $password; + } + $headers = array('login' => $this->_username , 'passcode' => $this->_password); + if ($this->clientId != null) { + $headers["client-id"] = $this->clientId; + } + $frame = new Stomp_Frame("CONNECT", $headers); + $this->_writeFrame($frame); + $frame = $this->readFrame(); + if ($frame instanceof Stomp_Frame && $frame->command == 'CONNECTED') { + $this->_sessionId = $frame->headers["session"]; + return true; + } else { + require_once 'Stomp/Exception.php'; + if ($frame instanceof Stomp_Frame) { + throw new Stomp_Exception("Unexpected command: {$frame->command}", 0, $frame->body); + } else { + throw new Stomp_Exception("Connection not acknowledged"); + } + } + } + + /** + * Check if client session has ben established + * + * @return boolean + */ + public function isConnected () + { + return !empty($this->_sessionId) && is_resource($this->_socket); + } + /** + * Current stomp session ID + * + * @return string + */ + public function getSessionId() + { + return $this->_sessionId; + } + /** + * Send a message to a destination in the messaging system + * + * @param string $destination Destination queue + * @param string|Stomp_Frame $msg Message + * @param array $properties + * @param boolean $sync Perform request synchronously + * @return boolean + */ + public function send ($destination, $msg, $properties = null, $sync = null) + { + if ($msg instanceof Stomp_Frame) { + $msg->headers['destination'] = $destination; + $msg->headers = array_merge($msg->headers, $properties); + $frame = $msg; + } else { + $headers = $properties; + $headers['destination'] = $destination; + $frame = new Stomp_Frame('SEND', $headers, $msg); + } + $this->_prepareReceipt($frame, $sync); + $this->_writeFrame($frame); + return $this->_waitForReceipt($frame, $sync); + } + /** + * Prepair frame receipt + * + * @param Stomp_Frame $frame + * @param boolean $sync + */ + protected function _prepareReceipt (Stomp_Frame $frame, $sync) + { + $receive = $this->sync; + if ($sync !== null) { + $receive = $sync; + } + if ($receive == true) { + $frame->headers['receipt'] = md5(microtime()); + } + } + /** + * Wait for receipt + * + * @param Stomp_Frame $frame + * @param boolean $sync + * @return boolean + * @throws Stomp_Exception + */ + protected function _waitForReceipt (Stomp_Frame $frame, $sync) + { + + $receive = $this->sync; + if ($sync !== null) { + $receive = $sync; + } + if ($receive == true) { + $id = (isset($frame->headers['receipt'])) ? $frame->headers['receipt'] : null; + if ($id == null) { + return true; + } + $frame = $this->readFrame(); + if ($frame instanceof Stomp_Frame && $frame->command == 'RECEIPT') { + if ($frame->headers['receipt-id'] == $id) { + return true; + } else { + require_once 'Stomp/Exception.php'; + throw new Stomp_Exception("Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body); + } + } else { + require_once 'Stomp/Exception.php'; + if ($frame instanceof Stomp_Frame) { + throw new Stomp_Exception("Unexpected command {$frame->command}", 0, $frame->body); + } else { + throw new Stomp_Exception("Receipt not received"); + } + } + } + return true; + } + /** + * Register to listen to a given destination + * + * @param string $destination Destination queue + * @param array $properties + * @param boolean $sync Perform request synchronously + * @return boolean + * @throws Stomp_Exception + */ + public function subscribe ($destination, $properties = null, $sync = null) + { + $headers = array('ack' => 'client'); + $headers['activemq.prefetchSize'] = $this->prefetchSize; + if ($this->clientId != null) { + $headers["activemq.subcriptionName"] = $this->clientId; + } + if (isset($properties)) { + foreach ($properties as $name => $value) { + $headers[$name] = $value; + } + } + $headers['destination'] = $destination; + $frame = new Stomp_Frame('SUBSCRIBE', $headers); + $this->_prepareReceipt($frame, $sync); + $this->_writeFrame($frame); + if ($this->_waitForReceipt($frame, $sync) == true) { + $this->_subscriptions[$destination] = $properties; + return true; + } else { + return false; + } + } + /** + * Remove an existing subscription + * + * @param string $destination + * @param array $properties + * @param boolean $sync Perform request synchronously + * @return boolean + * @throws Stomp_Exception + */ + public function unsubscribe ($destination, $properties = null, $sync = null) + { + $headers = array(); + if (isset($properties)) { + foreach ($properties as $name => $value) { + $headers[$name] = $value; + } + } + $headers['destination'] = $destination; + $frame = new Stomp_Frame('UNSUBSCRIBE', $headers); + $this->_prepareReceipt($frame, $sync); + $this->_writeFrame($frame); + if ($this->_waitForReceipt($frame, $sync) == true) { + unset($this->_subscriptions[$destination]); + return true; + } else { + return false; + } + } + /** + * Start a transaction + * + * @param string $transactionId + * @param boolean $sync Perform request synchronously + * @return boolean + * @throws Stomp_Exception + */ + public function begin ($transactionId = null, $sync = null) + { + $headers = array(); + if (isset($transactionId)) { + $headers['transaction'] = $transactionId; + } + $frame = new Stomp_Frame('BEGIN', $headers); + $this->_prepareReceipt($frame, $sync); + $this->_writeFrame($frame); + return $this->_waitForReceipt($frame, $sync); + } + /** + * Commit a transaction in progress + * + * @param string $transactionId + * @param boolean $sync Perform request synchronously + * @return boolean + * @throws Stomp_Exception + */ + public function commit ($transactionId = null, $sync = null) + { + $headers = array(); + if (isset($transactionId)) { + $headers['transaction'] = $transactionId; + } + $frame = new Stomp_Frame('COMMIT', $headers); + $this->_prepareReceipt($frame, $sync); + $this->_writeFrame($frame); + return $this->_waitForReceipt($frame, $sync); + } + /** + * Roll back a transaction in progress + * + * @param string $transactionId + * @param boolean $sync Perform request synchronously + */ + public function abort ($transactionId = null, $sync = null) + { + $headers = array(); + if (isset($transactionId)) { + $headers['transaction'] = $transactionId; + } + $frame = new Stomp_Frame('ABORT', $headers); + $this->_prepareReceipt($frame, $sync); + $this->_writeFrame($frame); + return $this->_waitForReceipt($frame, $sync); + } + /** + * Acknowledge consumption of a message from a subscription + * Note: This operation is always asynchronous + * + * @param string|Stomp_Frame $messageMessage ID + * @param string $transactionId + * @return boolean + * @throws Stomp_Exception + */ + public function ack ($message, $transactionId = null) + { + if ($message instanceof Stomp_Frame) { + $frame = new Stomp_Frame('ACK', $message->headers); + $this->_writeFrame($frame); + return true; + } else { + $headers = array(); + if (isset($transactionId)) { + $headers['transaction'] = $transactionId; + } + $headers['message-id'] = $message; + $frame = new Stomp_Frame('ACK', $headers); + $this->_writeFrame($frame); + return true; + } + } + /** + * Graceful disconnect from the server + * + */ + public function disconnect () + { + $header = array(); + + if ($this->clientId != null) { + $headers["client-id"] = $this->clientId; + } + + if (is_resource($this->_socket)) { + $this->_writeFrame(new Stomp_Frame('DISCONNECT', $headers)); + fclose($this->_socket); + } + $this->_socket = null; + $this->_sessionId = null; + $this->_currentHost = -1; + $this->_subscriptions = array(); + $this->_username = ''; + $this->_password = ''; + } + /** + * Write frame to server + * + * @param Stomp_Frame $stompFrame + */ + protected function _writeFrame (Stomp_Frame $stompFrame) + { + if (!is_resource($this->_socket)) { + require_once 'Stomp/Exception.php'; + throw new Stomp_Exception('Socket connection hasn\'t been established'); + } + + $data = $stompFrame->__toString(); + $r = fwrite($this->_socket, $data, strlen($data)); + if ($r === false || $r == 0) { + $this->_reconnect(); + $this->_writeFrame($stompFrame); + } + } + + /** + * Set timeout to wait for content to read + * + * @param int $seconds_to_wait Seconds to wait for a frame + * @param int $milliseconds Milliseconds to wait for a frame + */ + public function setReadTimeout($seconds, $milliseconds = 0) + { + $this->_read_timeout_seconds = $seconds; + $this->_read_timeout_milliseconds = $milliseconds; + } + + /** + * Read responce frame from server + * + * @return Stomp_Frame|Stomp_Message_Map|boolean False when no frame to read + */ + public function readFrame () + { + if (!$this->hasFrameToRead()) { + return false; + } + + $rb = 1024; + $data = ''; + do { + $read = fgets($this->_socket, $rb); + if ($read === false) { + $this->_reconnect(); + return $this->readFrame(); + } + $data .= $read; + $len = strlen($data); + } while (($len < 2 || ! ($data[$len - 2] == "\x00" && $data[$len - 1] == "\n"))); + + list ($header, $body) = explode("\n\n", $data, 2); + $header = explode("\n", $header); + $headers = array(); + $command = null; + foreach ($header as $v) { + if (isset($command)) { + list ($name, $value) = explode(':', $v, 2); + $headers[$name] = $value; + } else { + $command = $v; + } + } + $frame = new Stomp_Frame($command, $headers, trim($body)); + if (isset($frame->headers['amq-msg-type']) && $frame->headers['amq-msg-type'] == 'MapMessage') { + require_once 'Stomp/Message/Map.php'; + return new Stomp_Message_Map($frame); + } else { + return $frame; + } + } + + /** + * Check if there is a frame to read + * + * @return boolean + */ + public function hasFrameToRead() + { + $read = array($this->_socket); + $write = null; + $except = null; + + $has_frame_to_read = stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds); + + if ($has_frame_to_read === false) { + throw new Stomp_Exception('Check failed to determin if the socket is readable'); + } else if ($has_frame_to_read > 0) { + return true; + } else { + return false; + } + } + + /** + * Reconnects and renews subscriptions (if there were any) + * Call this method when you detect connection problems + */ + protected function _reconnect () + { + $subscriptions = $this->_subscriptions; + + $this->connect($this->_username, $this->_password); + foreach ($subscriptions as $dest => $properties) { + $this->subscribe($dest, $properties); + } + } + /** + * Graceful object desruction + * + */ + public function __destruct() + { + $this->disconnect(); + } +} +?> -- cgit v1.2.3-54-g00ecf