]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/Stomp.php
Several fixes to make RabbitMQ a player.
[quix0rs-gnu-social.git] / extlib / Stomp.php
1 <?php
2 /**
3  *
4  * Copyright 2005-2006 The Apache Software Foundation
5  *
6  * Licensed under the Apache License, Version 2.0 (the "License");
7  * you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at
9  *
10  * http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing, software
13  * distributed under the License is distributed on an "AS IS" BASIS,
14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15  * See the License for the specific language governing permissions and
16  * limitations under the License.
17  */
18
19 /* vim: set expandtab tabstop=3 shiftwidth=3: */
20
21 require_once 'Stomp/Frame.php';
22
23 /**
24  * A Stomp Connection
25  *
26  *
27  * @package Stomp
28  * @author Hiram Chirino <hiram@hiramchirino.com>
29  * @author Dejan Bosanac <dejan@nighttale.net>
30  * @author Michael Caplan <mcaplan@labnet.net>
31  * @version $Revision: 43 $
32  */
33 class Stomp
34 {
35     /**
36      * Perform request synchronously
37      *
38      * @var boolean
39      */
40     public $sync = false;
41
42     /**
43      * Default prefetch size
44      *
45      * @var int
46      */
47     public $prefetchSize = 1;
48
49     /**
50      * Client id used for durable subscriptions
51      *
52      * @var string
53      */
54     public $clientId = null;
55
56     protected $_brokerUri = null;
57     protected $_socket = null;
58     protected $_hosts = array();
59     protected $_params = array();
60     protected $_subscriptions = array();
61     protected $_defaultPort = 61613;
62     protected $_currentHost = - 1;
63     protected $_attempts = 10;
64     protected $_username = '';
65     protected $_password = '';
66     protected $_sessionId;
67     protected $_read_timeout_seconds = 60;
68     protected $_read_timeout_milliseconds = 0;
69
70     /**
71      * Constructor
72      *
73      * @param string $brokerUri Broker URL
74      * @throws Stomp_Exception
75      */
76     public function __construct ($brokerUri)
77     {
78         $this->_brokerUri = $brokerUri;
79         $this->_init();
80     }
81     /**
82      * Initialize connection
83      *
84      * @throws Stomp_Exception
85      */
86     protected function _init ()
87     {
88         $pattern = "|^(([a-zA-Z]+)://)+\(*([a-zA-Z0-9\.:/i,-]+)\)*\??([a-zA-Z0-9=]*)$|i";
89         if (preg_match($pattern, $this->_brokerUri, $regs)) {
90             $scheme = $regs[2];
91             $hosts = $regs[3];
92             $params = $regs[4];
93             if ($scheme != "failover") {
94                 $this->_processUrl($this->_brokerUri);
95             } else {
96                 $urls = explode(",", $hosts);
97                 foreach ($urls as $url) {
98                     $this->_processUrl($url);
99                 }
100             }
101             if ($params != null) {
102                 parse_str($params, $this->_params);
103             }
104         } else {
105             require_once 'Stomp/Exception.php';
106             throw new Stomp_Exception("Bad Broker URL {$this->_brokerUri}");
107         }
108     }
109     /**
110      * Process broker URL
111      *
112      * @param string $url Broker URL
113      * @throws Stomp_Exception
114      * @return boolean
115      */
116     protected function _processUrl ($url)
117     {
118         $parsed = parse_url($url);
119         if ($parsed) {
120             array_push($this->_hosts, array($parsed['host'] , $parsed['port'] , $parsed['scheme']));
121         } else {
122             require_once 'Stomp/Exception.php';
123             throw new Stomp_Exception("Bad Broker URL $url");
124         }
125     }
126     /**
127      * Make socket connection to the server
128      *
129      * @throws Stomp_Exception
130      */
131     protected function _makeConnection ()
132     {
133         if (count($this->_hosts) == 0) {
134             require_once 'Stomp/Exception.php';
135             throw new Stomp_Exception("No broker defined");
136         }
137
138         // force disconnect, if previous established connection exists
139         $this->disconnect();
140
141         $i = $this->_currentHost;
142         $att = 0;
143         $connected = false;
144         while (! $connected && $att ++ < $this->_attempts) {
145             if (isset($this->_params['randomize']) && $this->_params['randomize'] == 'true') {
146                 $i = rand(0, count($this->_hosts) - 1);
147             } else {
148                 $i = ($i + 1) % count($this->_hosts);
149             }
150             $broker = $this->_hosts[$i];
151             $host = $broker[0];
152             $port = $broker[1];
153             $scheme = $broker[2];
154             if ($port == null) {
155                 $port = $this->_defaultPort;
156             }
157             if ($this->_socket != null) {
158                 fclose($this->_socket);
159                 $this->_socket = null;
160             }
161             $this->_socket = @fsockopen($scheme . '://' . $host, $port);
162             if (!is_resource($this->_socket) && $att >= $this->_attempts && !array_key_exists($i + 1, $this->_hosts)) {
163                 require_once 'Stomp/Exception.php';
164                 throw new Stomp_Exception("Could not connect to $host:$port ($att/{$this->_attempts})");
165             } else if (is_resource($this->_socket)) {
166                 $connected = true;
167                 $this->_currentHost = $i;
168                 break;
169             }
170         }
171         if (! $connected) {
172             require_once 'Stomp/Exception.php';
173             throw new Stomp_Exception("Could not connect to a broker");
174         }
175     }
176     /**
177      * Connect to server
178      *
179      * @param string $username
180      * @param string $password
181      * @return boolean
182      * @throws Stomp_Exception
183      */
184     public function connect ($username = '', $password = '')
185     {
186         $this->_makeConnection();
187         if ($username != '') {
188             $this->_username = $username;
189         }
190         if ($password != '') {
191             $this->_password = $password;
192         }
193         $headers = array('login' => $this->_username , 'passcode' => $this->_password);
194         if ($this->clientId != null) {
195             $headers["client-id"] = $this->clientId;
196         }
197         $frame = new Stomp_Frame("CONNECT", $headers);
198         $this->_writeFrame($frame);
199         $frame = $this->readFrame();
200         if ($frame instanceof Stomp_Frame && $frame->command == 'CONNECTED') {
201             $this->_sessionId = $frame->headers["session"];
202             return true;
203         } else {
204             require_once 'Stomp/Exception.php';
205             if ($frame instanceof Stomp_Frame) {
206                 throw new Stomp_Exception("Unexpected command: {$frame->command}", 0, $frame->body);
207             } else {
208                 throw new Stomp_Exception("Connection not acknowledged");
209             }
210         }
211     }
212
213     /**
214      * Check if client session has ben established
215      *
216      * @return boolean
217      */
218     public function isConnected ()
219     {
220         return !empty($this->_sessionId) && is_resource($this->_socket);
221     }
222     /**
223      * Current stomp session ID
224      *
225      * @return string
226      */
227     public function getSessionId()
228     {
229         return $this->_sessionId;
230     }
231     /**
232      * Send a message to a destination in the messaging system
233      *
234      * @param string $destination Destination queue
235      * @param string|Stomp_Frame $msg Message
236      * @param array $properties
237      * @param boolean $sync Perform request synchronously
238      * @return boolean
239      */
240     public function send ($destination, $msg, $properties = array(), $sync = null)
241     {
242         if ($msg instanceof Stomp_Frame) {
243             $msg->headers['destination'] = $destination;
244             $msg->headers = array_merge($msg->headers, $properties);
245             $frame = $msg;
246         } else {
247             $headers = $properties;
248             $headers['destination'] = $destination;
249             $frame = new Stomp_Frame('SEND', $headers, $msg);
250         }
251         $this->_prepareReceipt($frame, $sync);
252         $this->_writeFrame($frame);
253         return $this->_waitForReceipt($frame, $sync);
254     }
255     /**
256      * Prepair frame receipt
257      *
258      * @param Stomp_Frame $frame
259      * @param boolean $sync
260      */
261     protected function _prepareReceipt (Stomp_Frame $frame, $sync)
262     {
263         $receive = $this->sync;
264         if ($sync !== null) {
265             $receive = $sync;
266         }
267         if ($receive == true) {
268             $frame->headers['receipt'] = md5(microtime());
269         }
270     }
271     /**
272      * Wait for receipt
273      *
274      * @param Stomp_Frame $frame
275      * @param boolean $sync
276      * @return boolean
277      * @throws Stomp_Exception
278      */
279     protected function _waitForReceipt (Stomp_Frame $frame, $sync)
280     {
281
282         $receive = $this->sync;
283         if ($sync !== null) {
284             $receive = $sync;
285         }
286         if ($receive == true) {
287             $id = (isset($frame->headers['receipt'])) ? $frame->headers['receipt'] : null;
288             if ($id == null) {
289                 return true;
290             }
291             $frame = $this->readFrame();
292             if ($frame instanceof Stomp_Frame && $frame->command == 'RECEIPT') {
293                 if ($frame->headers['receipt-id'] == $id) {
294                     return true;
295                 } else {
296                     require_once 'Stomp/Exception.php';
297                     throw new Stomp_Exception("Unexpected receipt id {$frame->headers['receipt-id']}", 0, $frame->body);
298                 }
299             } else {
300                 require_once 'Stomp/Exception.php';
301                 if ($frame instanceof Stomp_Frame) {
302                     throw new Stomp_Exception("Unexpected command {$frame->command}", 0, $frame->body);
303                 } else {
304                     throw new Stomp_Exception("Receipt not received");
305                 }
306             }
307         }
308         return true;
309     }
310     /**
311      * Register to listen to a given destination
312      *
313      * @param string $destination Destination queue
314      * @param array $properties
315      * @param boolean $sync Perform request synchronously
316      * @return boolean
317      * @throws Stomp_Exception
318      */
319     public function subscribe ($destination, $properties = null, $sync = null)
320     {
321         $headers = array('ack' => 'client');
322         // FIXME: this seems to be activemq specific, but not hurting rabbitmq?
323         $headers['activemq.prefetchSize'] = $this->prefetchSize;
324         if ($this->clientId != null) {
325             // FIXME: this seems to be activemq specific, but not hurting rabbitmq?
326             $headers["activemq.subcriptionName"] = $this->clientId;
327         }
328         if (isset($properties)) {
329             foreach ($properties as $name => $value) {
330                 $headers[$name] = $value;
331             }
332         }
333         $headers['destination'] = $destination;
334         $frame = new Stomp_Frame('SUBSCRIBE', $headers);
335         $this->_prepareReceipt($frame, $sync);
336         $this->_writeFrame($frame);
337         if ($this->_waitForReceipt($frame, $sync) == true) {
338             $this->_subscriptions[$destination] = $properties;
339             return true;
340         } else {
341             return false;
342         }
343     }
344     /**
345      * Remove an existing subscription
346      *
347      * @param string $destination
348      * @param array $properties
349      * @param boolean $sync Perform request synchronously
350      * @return boolean
351      * @throws Stomp_Exception
352      */
353     public function unsubscribe ($destination, $properties = null, $sync = null)
354     {
355         $headers = array();
356         if (isset($properties)) {
357             foreach ($properties as $name => $value) {
358                 $headers[$name] = $value;
359             }
360         }
361         $headers['destination'] = $destination;
362         $frame = new Stomp_Frame('UNSUBSCRIBE', $headers);
363         $this->_prepareReceipt($frame, $sync);
364         $this->_writeFrame($frame);
365         if ($this->_waitForReceipt($frame, $sync) == true) {
366             unset($this->_subscriptions[$destination]);
367             return true;
368         } else {
369             return false;
370         }
371     }
372     /**
373      * Start a transaction
374      *
375      * @param string $transactionId
376      * @param boolean $sync Perform request synchronously
377      * @return boolean
378      * @throws Stomp_Exception
379      */
380     public function begin ($transactionId = null, $sync = null)
381     {
382         $headers = array();
383         if (isset($transactionId)) {
384             $headers['transaction'] = $transactionId;
385         }
386         $frame = new Stomp_Frame('BEGIN', $headers);
387         $this->_prepareReceipt($frame, $sync);
388         $this->_writeFrame($frame);
389         return $this->_waitForReceipt($frame, $sync);
390     }
391     /**
392      * Commit a transaction in progress
393      *
394      * @param string $transactionId
395      * @param boolean $sync Perform request synchronously
396      * @return boolean
397      * @throws Stomp_Exception
398      */
399     public function commit ($transactionId = null, $sync = null)
400     {
401         $headers = array();
402         if (isset($transactionId)) {
403             $headers['transaction'] = $transactionId;
404         }
405         $frame = new Stomp_Frame('COMMIT', $headers);
406         $this->_prepareReceipt($frame, $sync);
407         $this->_writeFrame($frame);
408         return $this->_waitForReceipt($frame, $sync);
409     }
410     /**
411      * Roll back a transaction in progress
412      *
413      * @param string $transactionId
414      * @param boolean $sync Perform request synchronously
415      */
416     public function abort ($transactionId = null, $sync = null)
417     {
418         $headers = array();
419         if (isset($transactionId)) {
420             $headers['transaction'] = $transactionId;
421         }
422         $frame = new Stomp_Frame('ABORT', $headers);
423         $this->_prepareReceipt($frame, $sync);
424         $this->_writeFrame($frame);
425         return $this->_waitForReceipt($frame, $sync);
426     }
427     /**
428      * Acknowledge consumption of a message from a subscription
429      * Note: This operation is always asynchronous
430      *
431      * @param string|Stomp_Frame $messageMessage ID
432      * @param string $transactionId
433      * @return boolean
434      * @throws Stomp_Exception
435      */
436     public function ack ($message, $transactionId = null)
437     {
438         // Handle the headers,
439         $headers = array();
440
441         if ($message instanceof Stomp_Frame) {
442             // Copy headers from the object
443             // FIXME: at least content-length can be wrong here (set to 3 sometimes).
444             $headers = $message->headers;
445         } else {
446             if (isset($transactionId)) {
447                 $headers['transaction'] = $transactionId;
448             }
449             $headers['message-id'] = $message;
450         }
451         // An ACK has no content
452         $headers['content-length'] = 0;
453
454         // Create it and write it out
455         $frame = new Stomp_Frame('ACK', $headers);
456         $this->_writeFrame($frame);
457         return true;
458     }
459     /**
460      * Graceful disconnect from the server
461      *
462      */
463     public function disconnect ()
464     {
465         $headers = array();
466
467         if ($this->clientId != null) {
468             $headers["client-id"] = $this->clientId;
469         }
470
471         if (is_resource($this->_socket)) {
472             $this->_writeFrame(new Stomp_Frame('DISCONNECT', $headers));
473             fclose($this->_socket);
474         }
475         $this->_socket = null;
476         $this->_sessionId = null;
477         $this->_currentHost = -1;
478         $this->_subscriptions = array();
479         $this->_username = '';
480         $this->_password = '';
481     }
482     /**
483      * Write frame to server
484      *
485      * @param Stomp_Frame $stompFrame
486      */
487     protected function _writeFrame (Stomp_Frame $stompFrame)
488     {
489         if (!is_resource($this->_socket)) {
490             require_once 'Stomp/Exception.php';
491             throw new Stomp_Exception('Socket connection hasn\'t been established');
492         }
493
494         $data = $stompFrame->__toString();
495         $r = fwrite($this->_socket, $data, strlen($data));
496         if ($r === false || $r == 0) {
497             $this->_reconnect();
498             $this->_writeFrame($stompFrame);
499         }
500     }
501
502     /**
503      * Set timeout to wait for content to read
504      *
505      * @param int $seconds_to_wait  Seconds to wait for a frame
506      * @param int $milliseconds Milliseconds to wait for a frame
507      */
508     public function setReadTimeout($seconds, $milliseconds = 0)
509     {
510         $this->_read_timeout_seconds = $seconds;
511         $this->_read_timeout_milliseconds = $milliseconds;
512     }
513
514     /**
515      * Read responce frame from server
516      *
517      * @return Stomp_Frame|Stomp_Message_Map|boolean False when no frame to read
518      */
519     public function readFrame ()
520     {
521         if (!$this->hasFrameToRead()) {
522             return false;
523         }
524
525         $rb = 1024;
526         $data = '';
527          do {
528              $read = fread($this->_socket, $rb);
529              if ($read === false) {
530                  $this->_reconnect();
531                  return $this->readFrame();
532              }
533              $data .= $read;
534              $len = strlen($data);
535
536              $continue = true;
537              // ActiveMq apparently add \n after 0 char
538              if($data[$len - 2] == "\x00" && $data[$len - 1] == "\n")  {
539                $continue = false;
540              }
541
542              // RabbitMq does not
543              if($data[$len - 1] == "\x00") {
544                 $continue = false;
545              }
546         } while ( $continue );
547         list ($header, $body) = explode("\n\n", $data, 2);
548         $header = explode("\n", $header);
549         $headers = array();
550         $command = null;
551         foreach ($header as $v) {
552             if (isset($command)) {
553                 list ($name, $value) = explode(':', $v, 2);
554                 $headers[$name] = $value;
555             } else {
556                 $command = $v;
557             }
558         }
559         $frame = new Stomp_Frame($command, $headers, trim($body));
560         if (isset($frame->headers['amq-msg-type']) && $frame->headers['amq-msg-type'] == 'MapMessage') {
561             require_once 'Stomp/Message/Map.php';
562             return new Stomp_Message_Map($frame);
563         } else {
564             return $frame;
565         }
566     }
567
568     /**
569      * Check if there is a frame to read
570      *
571      * @return boolean
572      */
573     public function hasFrameToRead()
574     {
575         $read = array($this->_socket);
576         $write = null;
577         $except = null;
578
579         $has_frame_to_read = stream_select($read, $write, $except, $this->_read_timeout_seconds, $this->_read_timeout_milliseconds);
580
581         if ($has_frame_to_read === false) {
582             throw new Stomp_Exception('Check failed to determin if the socket is readable');
583         } else if ($has_frame_to_read > 0) {
584             return true;
585         } else {
586             return false;
587         }
588     }
589
590     /**
591      * Reconnects and renews subscriptions (if there were any)
592      * Call this method when you detect connection problems
593      */
594     protected function _reconnect ()
595     {
596         $subscriptions = $this->_subscriptions;
597
598         $this->connect($this->_username, $this->_password);
599         foreach ($subscriptions as $dest => $properties) {
600             $this->subscribe($dest, $properties);
601         }
602     }
603     /**
604      * Graceful object desruction
605      *
606      */
607     public function __destruct()
608     {
609         $this->disconnect();
610     }
611 }
612 ?>