Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

init atmosphere integration

  • Loading branch information...
commit 499f36765419a3426ad67ded1620f22ed8d329a1 1 parent 68b1e75
@smaldini authored
View
82 EventsPushGrailsPlugin.groovy
@@ -1,6 +1,8 @@
+import org.grails.plugin.platform.events.push.EventsPushHandler
+
class EventsPushGrailsPlugin {
// the plugin version
- def version = "0.1"
+ def version = "1.0.M1"
// the version or versions of Grails the plugin is designed for
def grailsVersion = "2.0 > *"
// the other plugins this plugin depends on
@@ -10,12 +12,23 @@ class EventsPushGrailsPlugin {
"grails-app/views/error.gsp"
]
- // TODO Fill in these fields
def title = "Events Push Plugin" // Headline display name of the plugin
- def author = "Your name"
- def authorEmail = ""
+ def author = "Stephane Maldini"
+ def authorEmail = "stephane.maldini@gmail.com"
def description = '''\
-Brief summary/description of the plugin.
+Events-push is a client-side events bus based on the portable push library Atmosphere and Grails platform-core plugin for events
+propagation/listening. It simply allows your client to listen to server-side events and push data. It uses WebSockets by default
+and failbacks to Comet method if required (server not compliant, browser too old...).
+Events-push is a white-list broadcaster (triggered scope is 'browser', where your default listener scope with platform-core plugin is 'app'). You
+will need to define which events that can be propagated to server-side by using Events DSL to override 'browser' scope. Ie:
+
+MyEvents.groovy >
+events = {
+ 'saveTodo' scope:'browser' //change 'saveTodo' listeners scope for browser, hence receiving client data.
+}
+
+someView.html >
+grailsEvents.push('saveTodo', data);
'''
// URL to the plugin's documentation
@@ -24,7 +37,7 @@ Brief summary/description of the plugin.
// Extra (optional) plugin metadata
// License: one of 'APACHE', 'GPL2', 'GPL3'
-// def license = "APACHE"
+ def license = "APACHE"
// Details of company behind the plugin (if there is one)
// def organization = [ name: "My Company", url: "http://www.my-company.com/" ]
@@ -36,22 +49,46 @@ Brief summary/description of the plugin.
// def issueManagement = [ system: "JIRA", url: "http://jira.grails.org/browse/GPMYPLUGIN" ]
// Online location of the plugin's browseable source code.
-// def scm = [ url: "http://svn.grails-plugins.codehaus.org/browse/grails-plugins/" ]
+ def scm = [url: "https://github.com/smaldini/grails-events-push"]
def doWithWebDescriptor = { xml ->
- // TODO Implement additions to web.xml (optional), this event occurs before
- }
-
- def doWithSpring = {
- // TODO Implement runtime spring config (optional)
- }
+ def servlets = xml.'servlet'
+ def config = application.config?.events?.push
- def doWithDynamicMethods = { ctx ->
- // TODO Implement registering dynamic methods to classes (optional)
- }
+ servlets[servlets.size() - 1] + {
+ 'servlet' {
+ 'description'('MeteorServlet')
+ 'servlet-name'('MeteorServlet')
+ 'servlet-class'('org.atmosphere.cpr.MeteorServlet')
+ if (!config?.servlet?.initParams?.'org.atmosphere.servlet') {
+ 'init-param' {
+ 'param-name'('org.atmosphere.servlet')
+ 'param-value'(EventsPushHandler.name)
+ }
+ }
+ if (!config?.servlet?.initParams?.'org.atmosphere.useWebSocket') {
+ 'init-param' {
+ 'param-name'('org.atmosphere.useWebSocket')
+ 'param-value'(true)
+ }
+ }
+ config?.servlet?.initParams.each { initParam ->
+ 'init-param' {
+ 'param-name'(initParam.key)
+ 'param-value'(initParam.value)
+ }
+ }
+ 'load-on-startup'('0')
+ }
+ }
- def doWithApplicationContext = { applicationContext ->
- // TODO Implement post initialization spring config (optional)
+ def mappings = xml.'servlet-mapping'
+ mappings[mappings.size() - 1] + {
+ 'servlet-mapping' {
+ 'servlet-name'('MeteorServlet')
+ 'url-pattern'(config?.servlet?.urlPattern ?: '/g-eventsbus/*')
+ }
+ }
}
def onChange = { event ->
@@ -59,13 +96,4 @@ Brief summary/description of the plugin.
// watching is modified and reloaded. The event contains: event.source,
// event.application, event.manager, event.ctx, and event.plugin.
}
-
- def onConfigChange = { event ->
- // TODO Implement code that is executed when the project configuration changes.
- // The event is the same as for 'onChange'.
- }
-
- def onShutdown = { event ->
- // TODO Implement code that is executed when the application shuts down (optional)
- }
}
View
201 LICENSE.txt
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
View
4 application.properties
@@ -1,4 +1,6 @@
#Grails Metadata file
-#Wed May 16 15:57:49 CEST 2012
+#Wed May 16 17:17:08 CEST 2012
app.grails.version=2.0.3
app.name=events-push
+plugins.rest-client-builder=1.0.2
+plugins.svn=1.0.2
View
33 grails-app/conf/BuildConfig.groovy
@@ -3,35 +3,48 @@ grails.project.test.class.dir = "target/test-classes"
grails.project.test.reports.dir = "target/test-reports"
grails.project.target.level = 1.6
//grails.project.war.file = "target/${appName}-${appVersion}.war"
+grails.tomcat.nio = true
+
+if (appName == 'events-push') {
+ grails.plugin.location.'pluginPlatform' = '../../platform-core'
+}
grails.project.dependency.resolution = {
// inherit Grails' default dependencies
inherits("global") {
- // uncomment to disable ehcache
- // excludes 'ehcache'
+ excludes("xml-apis", "commons-digester")
}
+
log "warn" // log level of Ivy resolver, either 'error', 'warn', 'info', 'debug' or 'verbose'
repositories {
grailsCentral()
- // uncomment the below to enable remote dependency resolution
- // from public Maven repositories
- //mavenCentral()
- //mavenLocal()
+ mavenCentral()
+ mavenLocal()
//mavenRepo "http://snapshots.repository.codehaus.org"
//mavenRepo "http://repository.codehaus.org"
//mavenRepo "http://download.java.net/maven/2/"
//mavenRepo "http://repository.jboss.com/maven2/"
+ mavenRepo "https://oss.sonatype.org/content/repositories/snapshots"
}
dependencies {
- // specify dependencies here under either 'build', 'compile', 'runtime', 'test' or 'provided' scopes eg.
-
- // runtime 'mysql:mysql-connector-java:5.1.5'
+ compile('org.atmosphere:atmosphere-runtime:1.0.0-SNAPSHOT') {
+ excludes 'slf4j-api', 'atmosphere-ping'
+ }
}
plugins {
+ runtime(":jquery:1.7.1"){
+ export = false
+ }
+
build(":tomcat:$grailsVersion",
- ":release:1.0.0") {
+ ":release:2.0.1",
+ ":hibernate:$grailsVersion"
+ ) {
export = false
}
+ if (appName != 'events-push') {
+ compile ':platform-core:1.0.M2d-SNAPSHOT'
+ }
}
}
View
36 grails-app/conf/Config.groovy
@@ -8,17 +8,29 @@ log4j = {
// console name:'stdout', layout:pattern(conversionPattern: '%c{2} %m%n')
//}
- error 'org.codehaus.groovy.grails.web.servlet', // controllers
- 'org.codehaus.groovy.grails.web.pages', // GSP
- 'org.codehaus.groovy.grails.web.sitemesh', // layouts
- 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
- 'org.codehaus.groovy.grails.web.mapping', // URL mapping
- 'org.codehaus.groovy.grails.commons', // core / classloading
- 'org.codehaus.groovy.grails.plugins', // plugins
- 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
- 'org.springframework',
- 'org.hibernate',
- 'net.sf.ehcache.hibernate'
+ appenders {
+ console name: 'stdout'
+ }
- warn 'org.mortbay.log'
+ root {
+ info 'stdout'
+ additivity = true
+ }
+
+ debug 'org.atmosphere'
+
+
+ error 'org.codehaus.groovy.grails.web.servlet', // controllers
+ 'org.codehaus.groovy.grails.web.pages', // GSP
+ 'org.codehaus.groovy.grails.web.sitemesh', // layouts
+ 'org.codehaus.groovy.grails.web.mapping.filter', // URL mapping
+ 'org.codehaus.groovy.grails.web.mapping', // URL mapping
+ 'org.codehaus.groovy.grails.commons', // core / classloading
+ 'org.codehaus.groovy.grails.plugins', // plugins
+ 'org.codehaus.groovy.grails.orm.hibernate', // hibernate integration
+ 'org.springframework',
+ 'org.hibernate',
+ 'net.sf.ehcache.hibernate'
+
+ warn 'org.mortbay.log'
}
View
13 grails-app/conf/EventsPushResources.groovy
@@ -0,0 +1,13 @@
+modules = {
+ 'atmosphere' {
+ dependsOn 'jquery'
+ resource id:'js', url:[plugin: 'events-push', dir:'js/jquery', file:"jquery.atmosphere.js"],
+ disposition:'head'
+ }
+
+ 'grailsEvents' {
+ dependsOn 'atmosphere'
+ resource id:'js', url:[plugin: 'events-push', dir:'js/grails', file:"grailsEvents.js"]
+ }
+
+}
View
190 grails-app/views/index.gsp
@@ -0,0 +1,190 @@
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+ <r:require modules="grailsEvents"/>
+ <meta name='layout' content='main'/>
+ <r:script>
+ $(document).ready(function () {
+ var detectedTransport = null;
+ var socket = $.atmosphere;
+ var subSocket;
+
+ function getKeyCode(ev) {
+ if (window.event) return window.event.keyCode;
+ return ev.keyCode;
+ }
+
+ function getElementById() {
+ return document.getElementById(arguments[0]);
+ }
+
+ function getTransport(t) {
+ transport = t.options[t.selectedIndex].value;
+ if (transport == 'autodetect') {
+ transport = 'websocket';
+ }
+
+ return false;
+ }
+
+ function getElementByIdValue() {
+ detectedTransport = null;
+ return document.getElementById(arguments[0]).value;
+ }
+
+ function subscribe() {
+ var request = { url:'/events-push/g-eventsbus/' + getElementByIdValue('topic'),
+ transport:getElementByIdValue('transport')};
+
+ request.onMessage = function (response) {
+ detectedTransport = response.transport;
+ if (response.status == 200) {
+ var data = response.responseBody;
+ if (data.length > 0) {
+ $('ul').prepend($('<li></li>').text(" Message Received: " + data + " but detected transport is " + detectedTransport));
+ }
+ }
+ };
+
+ subSocket = socket.subscribe(request);
+ }
+
+ function unsubscribe() {
+ callbackAdded = false;
+ socket.unsubscribe();
+ }
+
+ function connect() {
+ unsubscribe();
+ getElementById('phrase').value = '';
+ getElementById('sendMessage').className = '';
+ getElementById('phrase').focus();
+ subscribe();
+ getElementById('connect').value = "Switch transport";
+ }
+
+ getElementById('connect').onclick = function (event) {
+ if (getElementById('topic').value == '') {
+ alert("Please enter a PubSub topic to subscribe");
+ return;
+ }
+ connect();
+ }
+
+ getElementById('topic').onkeyup = function (event) {
+ getElementById('sendMessage').className = 'hidden';
+ var keyc = getKeyCode(event);
+ if (keyc == 13 || keyc == 10) {
+ connect();
+ return false;
+ }
+ }
+
+ getElementById('phrase').setAttribute('autocomplete', 'OFF');
+ getElementById('phrase').onkeyup = function (event) {
+ var keyc = getKeyCode(event);
+ if (keyc == 13 || keyc == 10) {
+
+ var m = " sent using " + detectedTransport;
+ if (detectedTransport == null) {
+ detectedTransport = getElementByIdValue('transport');
+ m = " sent trying to use " + detectedTransport;
+ }
+
+ subSocket.push({data:'message=' + getElementByIdValue('phrase') + m});
+
+ getElementById('phrase').value = '';
+ return false;
+ }
+ return true;
+ };
+
+ getElementById('send_message').onclick = function (event) {
+ if (getElementById('topic').value == '') {
+ alert("Please enter a message to publish");
+ return;
+ }
+
+ var m = " sent using " + detectedTransport;
+ if (detectedTransport == null) {
+ detectedTransport = getElementByIdValue('transport');
+ m = " sent trying to use " + detectedTransport;
+ }
+
+ subSocket.push({data:'message=' + getElementByIdValue('phrase') + m});
+
+ getElementById('phrase').value = '';
+ return false;
+ };
+
+ getElementById('topic').focus();
+ });
+ </r:script>
+ <style type='text/css'>
+ div {
+ border: 0px solid black;
+ }
+
+ input#phrase {
+ width: 30em;
+ background-color: #e0f0f0;
+ }
+
+ input#topic {
+ width: 14em;
+ background-color: #e0f0f0;
+ }
+
+ div.hidden {
+ display: none;
+ }
+
+ span.from {
+ font-weight: bold;
+ }
+
+ span.alert {
+ font-style: italic;
+ }
+ </style>
+</head>
+
+<body>
+<h1>PubSub Sample using Atmosphere's JQuery Plug In. By default the sample use the DefaultBroadcaster.</h1>
+
+<h2>To enable Redis, XMPP, Hazelcast of ActiveMQ (JMS), uncomments in pom.xml the associated dependency or put atmosphere-{hazelcast|jms|xmpp|redis}.jar under WEB-INF/lib</h2>
+
+<h2>Select PubSub topic to subscribe</h2>
+
+<div id='pubsub'>
+ <input id='topic' type='text'/>
+</div>
+
+<h2>Select transport to use for subscribing</h2>
+
+<h3>You can change the transport any time.</h3>
+
+<div id='select_transport'>
+ <select id="transport">
+ <option id="autodetect" value="websocket">autodetect</option>
+ <option id="jsonp" value="jsonp">jsonp</option>
+ <option id="long-polling" value="long-polling">long-polling</option>
+ <option id="streaming" value="streaming">http streaming</option>
+ <option id="websocket" value="websocket">websocket</option>
+ </select>
+ <input id='connect' class='button' type='submit' name='connect' value='Connect'/>
+</div>
+<br/>
+<br/>
+
+<h2 id="s_h" class='hidden'>Publish Topic</h2>
+
+<div id='sendMessage' class='hidden'>
+ <input id='phrase' type='text'/>
+ <input id='send_message' class='button' type='submit' name='Publish' value='Publish Message'/>
+</div>
+<br/>
+
+<h2>Real Time PubSub Update</h2>
+<ul></ul>
+</body>
+</html>
View
19 grails-app/views/layouts/main.gsp
@@ -0,0 +1,19 @@
+<!doctype html>
+<!--[if lt IE 7 ]> <html lang="en" class="no-js ie6"> <![endif]-->
+<!--[if IE 7 ]> <html lang="en" class="no-js ie7"> <![endif]-->
+<!--[if IE 8 ]> <html lang="en" class="no-js ie8"> <![endif]-->
+<!--[if IE 9 ]> <html lang="en" class="no-js ie9"> <![endif]-->
+<!--[if (gt IE 9)|!(IE)]><!--> <html lang="en" class="no-js"><!--<![endif]-->
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+ <title><g:layoutTitle default="Kanban"/></title>
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <g:layoutHead/>
+ <r:layoutResources />
+</head>
+<body>
+ <g:layoutBody/>
+ <r:layoutResources />
+</body>
+</html>
View
38 scripts/_Events.groovy
@@ -0,0 +1,38 @@
+import groovy.xml.StreamingMarkupBuilder
+import org.codehaus.groovy.grails.commons.ConfigurationHolder
+
+eventCompileEnd = {
+ if (!isPluginProject) {
+ grailsConsole.echo 'checking sitemesh decorators for events-bus'
+ buildConfiguration(basedir)
+ }
+}
+
+
+def buildConfiguration(basedir) {
+ def config = ConfigurationHolder.config?.events?.push
+ def urlPattern = config?.servlet?.urlPattern ?: '/g-eventsbus/*'
+ // Write the atmosphere-decorators.xml file in WEB-INF
+ def decoratorsDotXml = """\
+<decorators>
+ <excludes>
+ <pattern>$urlPattern</pattern>
+ </excludes>
+</decorators>"""
+ new File("$basedir/web-app/WEB-INF/atmosphere-decorators.xml").write decoratorsDotXml

Factor out the base directory, since you use it twice. I also prefer to use the grailsSettings variable:

def webInfDir = new File(grailsSettings.baseDir, "web-app/WEB-INF")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+
+ // Modify if necessary the sitemesh.xml file that is in WEB-INF?
+ def file = new File("$basedir/web-app/WEB-INF/sitemesh.xml")
+ def doc = new XmlSlurper().parse(file)

Better to give these variables slightly more descriptive names, e.g. "sitemeshConfFile" and "sitemeshConfDoc"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ if (!doc.excludes.find { it.@file == '/WEB-INF/atmosphere-decorators.xml' }.size()) {

.find() does not return a collection and the .size() is redundant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ doc.appendNode({ excludes(file: '/WEB-INF/atmosphere-decorators.xml') })
+ // Save the XML document with pretty print
+ def xml = new StreamingMarkupBuilder().bind {
+ mkp.yield(doc)
+ }
+ def node = new XmlParser().parseText(xml.toString())
+ file.withWriter {
+ new XmlNodePrinter(new PrintWriter(it)).print(node)
+ }
+ }
+}
View
18 scripts/_Install.groovy
@@ -1,10 +1,8 @@
-//
-// This script is executed by Grails after plugin was installed to project.
-// This script is a Gant script so you can use all special variables provided
-// by Gant (such as 'baseDir' which points on project base dir). You can
-// use 'ant' to access a global instance of AntBuilder
-//
-// For example you can create directory under project tree:
-//
-// ant.mkdir(dir:"${basedir}/grails-app/jobs")
-//
+// Write the context.xml file in META-INF and WEB-INF
+def contextDotXml = """\
+<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+<Context>
+ <Loader delegate=\"true\"/>
+</Context>"""
+new File("$basedir/web-app/META-INF/context.xml").write contextDotXml
+new File("$basedir/web-app/WEB-INF/context.xml").write contextDotXml
View
5 scripts/_Uninstall.groovy
@@ -1,5 +0,0 @@
-//
-// This script is executed by Grails when the plugin is uninstalled from project.
-// Use this script if you intend to do any additional clean-up on uninstall, but
-// beware of messing up SVN directories!
-//
View
10 scripts/_Upgrade.groovy
@@ -1,10 +0,0 @@
-//
-// This script is executed by Grails during application upgrade ('grails upgrade'
-// command). This script is a Gant script so you can use all special variables
-// provided by Gant (such as 'baseDir' which points on project base dir). You can
-// use 'ant' to access a global instance of AntBuilder
-//
-// For example you can create directory under project tree:
-//
-// ant.mkdir(dir:"${basedir}/grails-app/jobs")
-//
View
59 src/java/org/grails/plugin/platform/events/push/EventsPushHandler.java
@@ -0,0 +1,59 @@
+package org.grails.plugin.platform.events.push;
+
+import org.atmosphere.cpr.*;
+import org.atmosphere.websocket.WebSocketEventListenerAdapter;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * @author Stephane Maldini <smaldini@doc4web.com>
+ * @version 1.0
+ * @file
+ * @date 16/05/12
+ * @section DESCRIPTION
+ * <p/>
+ * [Does stuff]
+ */
+public class EventsPushHandler extends HttpServlet {
+ @Override
+ public void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ // Create a Meteor
+ Meteor m = Meteor.build(req);
+
+ // Log all events on the console, including WebSocket events.
+ m.addListener(new WebSocketEventListenerAdapter());
+
+ //res.setContentType("text/html;charset=ISO-8859-1");
+
+ Broadcaster b = lookupBroadcaster(req.getPathInfo());
+ m.setBroadcaster(b);
+
+ String header = req.getHeader(HeaderConfig.X_ATMOSPHERE_TRANSPORT);
+ if (header != null && header.equalsIgnoreCase(HeaderConfig.LONG_POLLING_TRANSPORT)) {
+ req.setAttribute(ApplicationConfig.RESUME_ON_BROADCAST, Boolean.TRUE);
+ m.suspend(-1, false);
+ } else {
+ m.suspend(-1);
+ }
+
+ }
+
+ public void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
+ Broadcaster b = lookupBroadcaster(req.getPathInfo());
+
+ String message = req.getReader().readLine();
+
+ if (message != null && message.indexOf("message") != -1) {
+ b.broadcast(message.substring("message=".length()));
+ }
+ }
+
+ Broadcaster lookupBroadcaster(String pathInfo) {
+ String[] decodedPath = pathInfo.split("/");
+ Broadcaster b = BroadcasterFactory.getDefault().lookup(decodedPath[decodedPath.length - 1], true);
+ return b;
+ }
+}
View
19 web-app/js/grails/grailsEvents.js
@@ -0,0 +1,19 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * Copyright 2012, Stephane Maldini
+ * Licensed under the Apache License, Version 2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ */
View
1,742 web-app/js/jquery/jquery.atmosphere.js
@@ -0,0 +1,1742 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+/*
+ * Part of this code has been taked from
+ *
+ * jQuery Stream @VERSION
+ * Comet Streaming JavaScript Library
+ * http://code.google.com/p/jquery-stream/
+ *
+ * Copyright 2011, Donghwan Kim
+ * Licensed under the Apache License, Version 2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Compatible with jQuery 1.5+
+ */
+jQuery.atmosphere = function() {
+ jQuery(window).unload(function() {
+ jQuery.atmosphere.unsubscribe();
+ });
+
+ var parseHeaders = function(headerString) {
+ var match, rheaders = /^(.*?):[ \t]*([^\r\n]*)\r?$/mg, headers = {};
+ while (match = rheaders.exec(headerString)) {
+ headers[match[1]] = match[2];
+ }
+ return headers;
+ };
+
+ return {
+ version : 0.9,
+ requests : [],
+ callbacks : [],
+
+ onError : function(response) {},
+ onClose : function(response) {},
+ onOpen : function(response) {},
+ onMessage : function(response) {},
+ onReconnect : function(request, response) {},
+ onMessagePublished : function(response) {},
+
+ AtmosphereRequest : function(options) {
+
+ /**
+ * {Object} Request parameters.
+ * @private
+ */
+ var _request = {
+ timeout: 300000,
+ method: 'GET',
+ headers: {},
+ contentType : '',
+ cache: true,
+ async: true,
+ ifModified: false,
+ callback: null,
+ dataType: '',
+ url : '',
+ data : '',
+ suspend : true,
+ maxRequest : 60,
+ maxStreamingLength : 10000000,
+ lastIndex : 0,
+ logLevel : 'info',
+ requestCount : 0,
+ fallbackMethod: 'GET',
+ fallbackTransport : 'streaming',
+ transport : 'long-polling',
+ webSocketImpl: null,
+ webSocketUrl: null,
+ webSocketPathDelimiter: "@@",
+ enableXDR : false,
+ rewriteURL : false,
+ attachHeadersAsQueryString : true,
+ executeCallbackBeforeReconnect : false,
+ readyState : 0,
+ lastTimestamp : 0,
+ withCredentials : false,
+ trackMessageLength : false ,
+ messageDelimiter : '|',
+ connectTimeout : -1,
+ onError : function(response) {},
+ onClose : function(response) {},
+ onOpen : function(response) {},
+ onMessage : function(response) {},
+ onReconnect : function(request, response) {},
+ onMessagePublished : function(response) {}
+ };
+
+ /**
+ * {Object} Request's last response.
+ * @private
+ */
+ var _response = {
+ status: 200,
+ responseBody : '',
+ expectedBodySize : -1,
+ headers : [],
+ state : "messageReceived",
+ transport : "polling",
+ error: null,
+ id : 0
+ };
+
+ /**
+ * {number} Request id.
+ *
+ * @private
+ */
+ var _uuid = 0;
+
+ /**
+ * {websocket} Opened web socket.
+ *
+ * @private
+ */
+ var _websocket = null;
+
+ /**
+ * {XMLHttpRequest, ActiveXObject} Opened ajax request (in case of
+ * http-streaming or long-polling)
+ *
+ * @private
+ */
+ var _activeRequest = null;
+
+ /**
+ * {Object} Object use for streaming with IE.
+ *
+ * @private
+ */
+ var _ieStream = null;
+
+ /**
+ * {Object} Object use for jsonp transport.
+ *
+ * @private
+ */
+ var _jqxhr = null;
+
+ /**
+ * {boolean} If request has been subscribed or not.
+ *
+ * @private
+ */
+ var _subscribed = true;
+
+ /**
+ * {number} Number of test reconnection.
+ *
+ * @private
+ */
+ var _requestCount = 0;
+
+ /**
+ * {boolean} If request is currently aborded.
+ *
+ * @private
+ */
+ var _abordingConnection = false;
+
+ // Automatic call to subscribe
+ _subscribe(options);
+
+ /**
+ * Initialize atmosphere request object.
+ *
+ * @private
+ */
+ function _init() {
+ _uuid = 0;
+ _subscribed = true;
+ _abordingConnection = false;
+ _requestCount = 0;
+
+ _websocket = null;
+ _activeRequest = null;
+ _ieStream = null;
+ }
+
+ /**
+ * Re-initialize atmosphere object.
+ * @private
+ */
+ function _reinit() {
+ _close();
+ _init();
+ }
+
+ /**
+ * Subscribe request using request transport. <br>
+ * If request is currently opened, this one will be closed.
+ *
+ * @param {Object}
+ * Request parameters.
+ * @private
+ */
+ function _subscribe(options) {
+ _reinit();
+
+ _request = jQuery.extend(_request, options);
+ _uuid = jQuery.atmosphere.guid();
+
+ _execute();
+ }
+
+ /**
+ * Check if web socket is supported (check for custom implementation
+ * provided by request object or browser implementation).
+ *
+ * @returns {boolean} True if web socket is supported, false
+ * otherwise.
+ * @private
+ */
+ function _supportWebsocket() {
+ return _request.webSocketImpl != null || window.WebSocket || window.MozWebSocket;
+ }
+
+ /**
+ * Open request using request transport. <br>
+ * If request transport is 'websocket' but websocket can't be
+ * opened, request will automatically reconnect using fallback
+ * transport.
+ *
+ * @private
+ */
+ function _execute() {
+ if (_request.transport != 'websocket') {
+ _open('opening',_request.transport);
+ _executeRequest();
+
+ } else if (_request.transport == 'websocket') {
+ if (!_supportWebsocket()) {
+ jQuery.atmosphere.log(_request.logLevel, ["Websocket is not supported, using request.fallbackTransport (" + _request.fallbackTransport + ")"]);
+ _open('opening', _request.fallbackTransport);
+ _reconnectWithFallbackTransport();
+ } else {
+ _executeWebSocket();
+ }
+ }
+ }
+
+ /**
+ * @private
+ */
+ function _open(state, transport) {
+ var prevState = _response.state;
+ _response.state = state;
+ _response.status = 200;
+ var prevTransport = _response.transport;
+ _response.transport = transport;
+ _response.responseBody = "";
+ _invokeCallback();
+ _response.state = prevState;
+ _response.transport = prevTransport;
+ }
+
+ /**
+ * Execute request using jsonp transport.
+ *
+ * @param request
+ * {Object} request Request parameters, if
+ * undefined _request object will be used.
+ * @private
+ */
+ function _jsonp(request) {
+ var rq = _request;
+ if ((request != null) && (typeof(request) != 'undefined')) {
+ rq = request;
+ }
+
+ var url = rq.url;
+ var data = rq.data;
+ if (rq.attachHeadersAsQueryString) {
+ url = _attachHeaders(rq);
+ if (data != '') {
+ url += "&X-Atmosphere-Post-Body=" + data;
+ }
+ data = '';
+ }
+
+ _jqxhr = jQuery.ajax({
+ url : url,
+ type : rq.method,
+ dataType: "jsonp",
+ error : function(jqXHR, textStatus, errorThrown) {
+ if (jqXHR.status < 300) {
+ _reconnect(_jqxhr, rq);
+ } else {
+ _prepareCallback(textStatus, "error", jqXHR.status, rq.transport);
+ }
+ },
+ jsonp : "jsonpTransport",
+ success: function(json) {
+ if (rq.executeCallbackBeforeReconnect) {
+ _reconnect(_jqxhr, rq);
+ }
+
+ var msg = json.message;
+ if (msg != null && typeof msg != 'string') {
+ try {
+ msg = jQuery.stringifyJSON(msg);
+ } catch (err) {
+ // The message was partial
+ }
+ }
+
+ _prepareCallback(msg, "messageReceived", 200, rq.transport);
+
+ if (!rq.executeCallbackBeforeReconnect) {
+ _reconnect(_jqxhr, rq);
+ }
+ },
+ data : rq.data,
+ beforeSend : function(jqXHR) {
+ _doRequest(jqXHR, rq, false);
+ }
+ });
+ }
+
+ /**
+ * Build websocket object.
+ *
+ * @param location
+ * {string} Web socket url.
+ * @returns {websocket} Web socket object.
+ * @private
+ */
+ function _getWebSocket(location) {
+ if (_request.webSocketImpl != null) {
+ return _request.webSocketImpl;
+ } else {
+ if (window.WebSocket) {
+ return new WebSocket(location);
+ } else {
+ return new MozWebSocket(location);
+ }
+ }
+ }
+
+ /**
+ * Build web socket url from request url.
+ *
+ * @return {string} Web socket url (start with "ws" or "wss" for
+ * secure web socket).
+ * @private
+ */
+ function _buildWebSocketUrl() {
+ var url = _request.url;
+ url = _attachHeaders();
+ if (url.indexOf("http") == -1 && url.indexOf("ws") == -1) {
+ url = jQuery.atmosphere.parseUri(document.location, url);
+ }
+ return url.replace('http:', 'ws:').replace('https:', 'wss:');
+ }
+
+ /**
+ * Open web socket. <br>
+ * Automatically use fallback transport if web socket can't be
+ * opened.
+ *
+ * @private
+ */
+ function _executeWebSocket() {
+ var webSocketOpened = false;
+
+ _response.transport = "websocket";
+
+ var location = _buildWebSocketUrl(_request.url);
+
+ jQuery.atmosphere.log(_request.logLevel, ["Invoking executeWebSocket"]);
+ if (_request.logLevel == 'debug') {
+ jQuery.atmosphere.debug("Using URL: " + location);
+ }
+
+ _websocket = _getWebSocket(location);
+
+ if (_request.connectTimeout > 0) {
+ setTimeout(function() {
+ if (!webSocketOpened) {
+ var _message = {
+ code : 1002,
+ reason : "",
+ wasClean : false
+ };
+ _websocket.onclose(_message);
+ }
+ }, _request.connectTimeout);
+ }
+
+ _websocket.onopen = function(message) {
+ if (_request.logLevel == 'debug') {
+ jQuery.atmosphere.debug("Websocket successfully opened");
+ }
+
+ _subscribed = true;
+ _open(webSocketOpened ? 're-opening' : 'opening', "websocket");
+
+ webSocketOpened = true;
+
+ if (_request.method == 'POST') {
+ _response.state = "messageReceived";
+ _websocket.send(_request.data);
+ }
+ };
+
+ _websocket.onmessage = function(message) {
+ if (message.data.indexOf("parent.callback") != -1) {
+ jQuery.atmosphere.log(_request.logLevel, ["parent.callback no longer supported with 0.8 version and up. Please upgrade"]);
+ }
+
+ _response.state = 'messageReceived';
+ _response.status = 200;
+
+ var message = message.data;
+ var skipCallbackInvocation = _trackMessageSize(message, _request, _response);
+
+ if (!skipCallbackInvocation) {
+ _invokeCallback();
+ _response.responseBody = '';
+ }
+ };
+
+ _websocket.onerror = function(message) {
+ jQuery.atmosphere.warn("Websocket error, reason: " + message.reason);
+
+ _response.state = 'error';
+ _response.responseBody = "";
+ _response.status = 500;
+ _invokeCallback();
+ };
+
+ _websocket.onclose = function(message) {
+ var reason = message.reason;
+ if (reason === "") {
+ switch (message.code) {
+ case 1000:
+ reason = "Normal closure; the connection successfully completed whatever purpose for which " +
+ "it was created.";
+ break;
+ case 1001:
+ reason = "The endpoint is going away, either because of a server failure or because the " +
+ "browser is navigating away from the page that opened the connection.";
+ break;
+ case 1002:
+ reason = "The endpoint is terminating the connection due to a protocol error.";
+ break;
+ case 1003:
+ reason = "The connection is being terminated because the endpoint received data of a type it " +
+ "cannot accept (for example, a text-only endpoint received binary data).";
+ break;
+ case 1004:
+ reason = "The endpoint is terminating the connection because a data frame was received that " +
+ "is too large.";
+ break;
+ case 1005:
+ reason = "Unknown: no status code was provided even though one was expected.";
+ break;
+ case 1006:
+ reason = "Connection was closed abnormally (that is, with no close frame being sent).";
+ break;
+ }
+ }
+
+ jQuery.atmosphere.warn("Websocket closed, reason: " + reason);
+ jQuery.atmosphere.warn("Websocket closed, wasClean: " + message.wasClean);
+
+ _response.state = 'closed';
+ _response.responseBody = "";
+ _response.status = 200;
+ _invokeCallback();
+
+ if (_abordingConnection) {
+ _abordingConnection = false;
+ jQuery.atmosphere.log(_request.logLevel, ["Websocket closed normally"]);
+
+ } else if (!webSocketOpened) {
+ jQuery.atmosphere.log(_request.logLevel, ["Websocket failed. Downgrading to Comet and resending"]);
+ _open('opening', _request.fallbackTransport);
+ _reconnectWithFallbackTransport();
+
+ } else if ((_subscribed) && (_response.transport == 'websocket')) {
+ if (_requestCount++ < _request.maxRequest) {
+ _request.requestCount = _requestCount;
+ _response.responseBody = "";
+ _executeWebSocket();
+ } else {
+ jQuery.atmosphere.log(_request.logLevel, ["Websocket reconnect maximum try reached " + _request.requestCount]);
+ }
+ }
+ };
+ }
+
+ /**
+ * Track received message and make sure callbacks/functions are only invoked when the complete message
+ * has been received.
+ *
+ * @param message
+ * @param request
+ * @param response
+ */
+ function _trackMessageSize(message, request, response) {
+ if (request.trackMessageLength) {
+ // The message length is the included within the message
+ var messageStart = message.indexOf(request.messageDelimiter);
+
+ var length = response.expectedBodySize;
+ if (messageStart != -1) {
+ length = message.substring(0, messageStart);
+ message = message.substring(messageStart + 1);
+ response.expectedBodySize = length;
+ }
+
+ if (messageStart != -1) {
+ response.responseBody = message;
+ } else {
+ response.responseBody += message;
+ }
+
+ if (response.responseBody.length != length) {
+ return true;
+ }
+ } else {
+ response.responseBody = message;
+ }
+ return false;
+ }
+
+ /**
+ * Reconnect request with fallback transport. <br>
+ * Used in case websocket can't be opened.
+ *
+ * @private
+ */
+ function _reconnectWithFallbackTransport() {
+ _request.transport = _request.fallbackTransport;
+ _request.method = _request.fallbackMethod;
+ _response.transport = _request.fallbackTransport;
+ _executeRequest();
+ }
+
+ /**
+ * Get url from request and attach headers to it.
+ *
+ * @param request
+ * {Object} request Request parameters, if
+ * undefined _request object will be used.
+ *
+ * @returns {Object} Request object, if undefined,
+ * _request object will be used.
+ * @private
+ */
+ function _attachHeaders(request) {
+ var rq = _request;
+ if ((request != null) && (typeof(request) != 'undefined')) {
+ rq = request;
+ }
+
+ var url = rq.url;
+
+ // If not enabled
+ if (!rq.attachHeadersAsQueryString) return url;
+
+ // If already added
+ if (url.indexOf("X-Atmosphere-Framework") != -1) {
+ return url;
+ }
+
+ url += (url.indexOf('?') != -1) ? '&' : '?';
+ url += "X-Atmosphere-tracking-id=" + _uuid;
+ url += "&X-Atmosphere-Framework=" + jQuery.atmosphere.version;
+ url += "&X-Atmosphere-Transport=" + rq.transport;
+
+ if (rq.trackMessageLength) {
+ url += "&X-Atmosphere-TrackMessageSize=" + "true";
+ }
+
+ if (rq.lastTimestamp != undefined) {
+ url += "&X-Cache-Date=" + rq.lastTimestamp;
+ } else {
+ url += "&X-Cache-Date=" + 0;
+ }
+
+ if (rq.contentType != '') {
+ url += "&Content-Type=" + rq.contentType;
+ }
+
+ jQuery.each(rq.headers, function(name, value) {
+ var h = jQuery.isFunction(value) ? value.call(this, ajaxRequest, request, create) : value;
+ if (h) {
+ url += "&" + encodeURIComponent(name) + "=" + encodeURIComponent(h);
+ }
+ });
+
+ return url;
+ }
+
+ /**
+ * Build ajax request. <br>
+ * Ajax Request is an XMLHttpRequest object, except for IE6 where
+ * ajax request is an ActiveXObject.
+ *
+ * @return {XMLHttpRequest, ActiveXObject} Ajax request.
+ * @private
+ */
+ function _buildAjaxRequest() {
+ var ajaxRequest;
+ if (jQuery.browser.msie) {
+ var activexmodes = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP"];
+ for (var i = 0; i < activexmodes.length; i++) {
+ try {
+ ajaxRequest = new ActiveXObject(activexmodes[i]);
+ } catch(e) { }
+ }
+
+ } else if (window.XMLHttpRequest) {
+ ajaxRequest = new XMLHttpRequest();
+ }
+ return ajaxRequest;
+ }
+
+ /**
+ * Execute ajax request. <br>
+ *
+ * @param request
+ * {Object} request Request parameters, if
+ * undefined _request object will be used.
+ * @private
+ */
+ function _executeRequest(request) {
+ var rq = _request;
+ if ((request != null) || (typeof(request) != 'undefined')) {
+ rq = request;
+ }
+
+ // CORS fake using JSONP
+ if ((rq.transport == 'jsonp') || ((rq.enableXDR) && (jQuery.atmosphere.checkCORSSupport()))) {
+ _jsonp(rq);
+ return;
+ }
+
+ if ((rq.transport == 'streaming') && (jQuery.browser.msie)) {
+ rq.enableXDR && window.XDomainRequest ? _ieXDR(rq) : _ieStreaming(rq);
+ return;
+ }
+
+ if ((rq.enableXDR) && (window.XDomainRequest)) {
+ _ieXDR(rq);
+ return;
+ }
+
+ if (rq.requestCount++ < rq.maxRequest) {
+ var ajaxRequest = _buildAjaxRequest();
+ _doRequest(ajaxRequest, rq, true);
+
+ if (rq.suspend) {
+ _activeRequest = ajaxRequest;
+ }
+
+ if (rq.transport != 'polling') {
+ _response.transport = rq.transport;
+ }
+
+ var error = false;
+ if (!jQuery.browser.msie) {
+ ajaxRequest.onerror = function() {
+ error = true;
+ try {
+ _response.status = XMLHttpRequest.status;
+ } catch(e) {
+ _response.status = 404;
+ }
+
+ _response.state = "error";
+ _invokeCallback();
+ ajaxRequest.abort();
+ _activeRequest = null;
+ };
+ }
+
+ ajaxRequest.onreadystatechange = function() {
+ if (_abordingConnection) {
+ return;
+ }
+
+ var skipCallbackInvocation = false;
+ var update = false;
+
+ // Remote server disconnected us, reconnect.
+ if (rq.transport == 'streaming'
+ && (rq.readyState > 2
+ && ajaxRequest.readyState == 4)) {
+
+ rq.readyState = 0;
+ rq.lastIndex = 0;
+
+ _reconnect(ajaxRequest, rq, true);
+ return;
+ }
+
+ rq.readyState = ajaxRequest.readyState;
+
+ if (ajaxRequest.readyState == 4) {
+ if (jQuery.browser.msie) {
+ update = true;
+ } else if (rq.transport == 'streaming') {
+ update = true;
+ } else if (rq.transport == 'long-polling') {
+ update = true;
+ clearTimeout(rq.id);
+ }
+
+ } else if (!jQuery.browser.msie && ajaxRequest.readyState == 3 && ajaxRequest.status == 200 && rq.transport != 'long-polling') {
+ update = true;
+ } else {
+ clearTimeout(rq.id);
+ }
+
+ if (update) {
+
+ var tempDate = ajaxRequest.getResponseHeader('X-Cache-Date');
+ if (tempDate != null || tempDate != undefined) {
+ _request.lastTimestamp = tempDate.split(" ").pop();
+ }
+
+ var responseText = ajaxRequest.responseText;
+ this.previousLastIndex = rq.lastIndex;
+ if (rq.transport == 'streaming') {
+ var text = responseText.substring(rq.lastIndex, responseText.length);
+ _response.isJunkEnded = true;
+
+ if (rq.lastIndex == 0 && text.indexOf("<!-- Welcome to the Atmosphere Framework.") != -1) {
+ _response.isJunkEnded = false;
+ }
+
+ if (!_response.isJunkEnded) {
+ var endOfJunk = "<!-- EOD -->";
+ var endOfJunkLenght = endOfJunk.length;
+ var junkEnd = text.indexOf(endOfJunk) + endOfJunkLenght;
+
+ if (junkEnd > endOfJunkLenght && junkEnd != text.length) {
+ _response.responseBody = text.substring(junkEnd);
+ } else {
+ skipCallbackInvocation = true;
+ }
+ } else {
+ var message = responseText.substring(rq.lastIndex, responseText.length);
+ skipCallbackInvocation = _trackMessageSize(message, rq, _response);
+ }
+ rq.lastIndex = responseText.length;
+
+ if (jQuery.browser.opera) {
+ jQuery.atmosphere.iterate(function() {
+ if (ajaxRequest.responseText.length > rq.lastIndex) {
+ try {
+ _response.status = ajaxRequest.status;
+ _response.headers = parseHeaders(ajaxRequest.getAllResponseHeaders());
+ }
+ catch(e) {
+ _response.status = 404;
+ }
+ _response.state = "messageReceived";
+ _response.responseBody = ajaxRequest.responseText.substring(rq.lastIndex);
+ rq.lastIndex = ajaxRequest.responseText.length;
+
+ _invokeCallback();
+ if ((rq.transport == 'streaming') && (ajaxRequest.responseText.length > rq.maxStreamingLength)) {
+ // Close and reopen connection on large data received
+ ajaxRequest.abort();
+ _doRequest(ajaxRequest, rq, true);
+ }
+ }
+ }, 0);
+ }
+
+ if (skipCallbackInvocation) {
+ return;
+ }
+ } else {
+ skipCallbackInvocation = _trackMessageSize(responseText, rq, _response);
+ rq.lastIndex = responseText.length;
+ }
+
+ try {
+ _response.status = ajaxRequest.status;
+ _response.headers = parseHeaders(ajaxRequest.getAllResponseHeaders());
+ } catch(e) {
+ _response.status = 404;
+ }
+
+ if (rq.suspend) {
+ _response.state = _response.status == 0 ? "closed" : "messageReceived";
+ } else {
+ _response.state = "messagePublished";
+ }
+
+ if (!rq.executeCallbackBeforeReconnect) {
+ _reconnect(ajaxRequest, rq, false);
+ }
+
+ // For backward compatibility with Atmosphere < 0.8
+ if (_response.responseBody.indexOf("parent.callback") != -1) {
+ jQuery.atmosphere.log(rq.logLevel, ["parent.callback no longer supported with 0.8 version and up. Please upgrade"]);
+ }
+ _invokeCallback();
+
+ if (rq.executeCallbackBeforeReconnect) {
+ _reconnect(ajaxRequest, rq, false);
+ }
+
+ if ((rq.transport == 'streaming') && (responseText.length > rq.maxStreamingLength)) {
+ // Close and reopen connection on large data received
+ ajaxRequest.abort();
+ _doRequest(ajaxRequest, rq, true);
+ } else {
+ _open('re-opening', rq.transport);
+ }
+ }
+ };
+ ajaxRequest.send(rq.data);
+
+ if (rq.suspend) {
+ rq.id = setTimeout(function() {
+ ajaxRequest.abort();
+ _subscribe(rq);
+ }, rq.timeout);
+ }
+ _subscribed = true;
+
+ } else {
+ jQuery.atmosphere.log(rq.logLevel, ["Max re-connection reached."]);
+ }
+ }
+
+ /**
+ * Do ajax request.
+ * @param ajaxRequest Ajax request.
+ * @param request Request parameters.
+ * @param create If ajax request has to be open.
+ */
+ function _doRequest(ajaxRequest, request, create) {
+ // Prevent Android to cache request
+ var url = jQuery.atmosphere.prepareURL(request.url);
+
+ if (create) {
+ ajaxRequest.open(request.method, url, true);
+ if (request.connectTimeout > -1) {
+ setTimeout(function() {
+ if (request.requestCount == 0) {
+ ajaxRequest.abort();
+ _prepareCallback("Connect timeout", "closed", 200, request.transport);
+ }
+ }, request.connectTimeout);
+ }
+ }
+
+ if (_request.withCredentials) {
+ if ("withCredentials" in ajaxRequest) {
+ ajaxRequest.withCredentials = true;
+ }
+ }
+
+ ajaxRequest.setRequestHeader("X-Atmosphere-Framework", jQuery.atmosphere.version);
+ ajaxRequest.setRequestHeader("X-Atmosphere-Transport", request.transport);
+ if (request.lastTimestamp != undefined) {
+ ajaxRequest.setRequestHeader("X-Cache-Date", request.lastTimestamp);
+ } else {
+ ajaxRequest.setRequestHeader("X-Cache-Date", 0);
+ }
+
+ if (request.trackMessageLength) {
+ ajaxRequest.setRequestHeader("X-Atmosphere-TrackMessageSize", "true")
+ }
+
+ if (request.contentType != '') {
+ ajaxRequest.setRequestHeader("Content-Type", request.contentType);
+ }
+ ajaxRequest.setRequestHeader("X-Atmosphere-tracking-id", _uuid);
+
+ jQuery.each(request.headers, function(name, value) {
+ var h = jQuery.isFunction(value) ? value.call(this, ajaxRequest, request, create) : value;
+ if (h) {
+ ajaxRequest.setRequestHeader(name, h);
+ }
+ });
+ }
+
+ function _reconnect(ajaxRequest, request, force) {
+ if (force || (request.suspend && ajaxRequest.status == 200 && request.transport != 'streaming' && _subscribed)) {
+ _executeRequest();
+ }
+ }
+
+ // From jquery-stream, which is APL2 licensed as well.
+ function _ieXDR(request) {
+ _ieStream = _configureXDR(request);
+ _ieStream.open();
+ }
+
+ // From jquery-stream
+ function _configureXDR(request) {
+ var rq = _request;
+ if ((request != null) && (typeof(request) != 'undefined')) {
+ rq = request;
+ }
+
+ var lastMessage = "";
+ var transport = rq.transport;
+ var lastIndex = 0;
+
+ var xdrCallback = function (xdr) {
+ var responseBody = xdr.responseText;
+ var isJunkEnded = false;
+
+ if (responseBody.indexOf("<!-- Welcome to the Atmosphere Framework.") != -1) {
+ isJunkEnded = true;
+ }
+
+ if (isJunkEnded) {
+ var endOfJunk = "<!-- EOD -->";
+ var endOfJunkLenght = endOfJunk.length;
+ var junkEnd = responseBody.indexOf(endOfJunk) + endOfJunkLenght;
+
+ responseBody = responseBody.substring(junkEnd + lastIndex);
+ lastIndex += responseBody.length;
+ }
+
+ _prepareCallback(responseBody, "messageReceived", 200, transport);
+ };
+
+ var xdr = new window.XDomainRequest();
+ var rewriteURL = rq.rewriteURL || function(url) {
+ // Maintaining session by rewriting URL
+ // http://stackoverflow.com/questions/6453779/maintaining-session-by-rewriting-url
+ var rewriters = {
+ JSESSIONID: function(sid) {
+ return url.replace(/;jsessionid=[^\?]*|(\?)|$/, ";jsessionid=" + sid + "$1");
+ },
+ PHPSESSID: function(sid) {
+ return url.replace(/\?PHPSESSID=[^&]*&?|\?|$/, "?PHPSESSID=" + sid + "&").replace(/&$/, "");
+ }
+ };
+
+ for (var name in rewriters) {
+ // Finds session id from cookie
+ var matcher = new RegExp("(?:^|;\\s*)" + encodeURIComponent(name) + "=([^;]*)").exec(document.cookie);
+ if (matcher) {
+ return rewriters[name](matcher[1]);
+ }
+ }
+
+ return url;
+ };
+
+ // Handles open and message event
+ xdr.onprogress = function() {
+ xdrCallback(xdr);
+ };
+ // Handles error event
+ xdr.onerror = function() {
+ _prepareCallback(xdr.responseText, "error", 500, transport);
+ };
+ // Handles close event
+ xdr.onload = function() {
+ if (lastMessage != xdr.responseText) {
+ xdrCallback(xdr);
+ }
+ if (rq.transport == "long-polling") {
+ _executeRequest();
+ }
+ };
+
+ return {
+ open: function() {
+ if (rq.method == 'POST') {
+ rq.attachHeadersAsQueryString = true;
+ }
+ var url = _attachHeaders(rq);
+ if (rq.method == 'POST') {
+ url += "&X-Atmosphere-Post-Body=" + rq.data;
+ }
+ xdr.open(rq.method, rewriteURL(url));
+ xdr.send();
+ if (rq.connectTimeout > -1) {
+ setTimeout(function() {
+ if (rq.requestCount == 0) {
+ xdr.abort();
+ _prepareCallback("Connect timeout", "closed", 200, rq.transport);
+ }
+ }, rq.connectTimeout);
+ }
+ },
+ close: function() {
+ xdr.abort();
+ _prepareCallback(xdr.responseText, "closed", 200, transport);
+ }
+ };
+ }
+
+ // From jquery-stream, which is APL2 licensed as well.
+ function _ieStreaming(request) {
+ _ieStream = _configureIE(request);
+ _ieStream.open();
+ }
+
+ function _configureIE(request) {
+ var rq = _request;
+ if ((request != null) && (typeof(request) != 'undefined')) {
+ rq = request;
+ }
+
+ var stop;
+ var doc = new window.ActiveXObject("htmlfile");
+
+ doc.open();
+ doc.close();
+
+ var url = rq.url;
+
+ if (rq.transport != 'polling') {
+ _response.transport = rq.transport;
+ }
+
+ return {
+ open: function() {
+ var iframe = doc.createElement("iframe");
+
+ url = _attachHeaders(rq);
+ if (rq.data != '') {
+ url += "&X-Atmosphere-Post-Body=" + rq.data;
+ }
+
+ // Finally attach a timestamp to prevent Android and IE caching.
+ url = jQuery.atmosphere.prepareURL(url);
+
+ iframe.src = url;
+ doc.body.appendChild(iframe);
+
+ // For the server to respond in a consistent format regardless of user agent, we polls response text
+ var cdoc = iframe.contentDocument || iframe.contentWindow.document;
+
+ stop = jQuery.atmosphere.iterate(function() {
+ if (!cdoc.firstChild) {
+ return;
+ }
+
+ // Detects connection failure
+ if (cdoc.readyState === "complete") {
+ try {
+ jQuery.noop(cdoc.fileSize);
+ } catch(e) {
+ _prepareCallback("Connection Failure", "error", 500, rq.transport);
+ return false;
+ }
+ }
+
+ var res = cdoc.body ? cdoc.body.lastChild : cdoc;
+ var readResponse = function() {
+ // Clones the element not to disturb the original one
+ var clone = res.cloneNode(true);
+
+ // If the last character is a carriage return or a line feed, IE ignores it in the innerText property
+ // therefore, we add another non-newline character to preserve it
+ clone.appendChild(cdoc.createTextNode("."));
+
+ var text = clone.innerText;
+ var isJunkEnded = true;
+
+ if (text.indexOf("<!-- Welcome to the Atmosphere Framework.") == -1) {
+ isJunkEnded = false;
+ }
+
+ if (isJunkEnded) {
+ var endOfJunk = "<!-- EOD -->";
+ var endOfJunkLenght = endOfJunk.length;
+ var junkEnd = text.indexOf(endOfJunk) + endOfJunkLenght;
+
+ text = text.substring(junkEnd);
+ }
+ return text.substring(0, text.length - 1);
+ };
+
+ //To support text/html content type
+ if (!jQuery.nodeName(res, "pre")) {
+ // Injects a plaintext element which renders text without interpreting the HTML and cannot be stopped
+ // it is deprecated in HTML5, but still works
+ var head = cdoc.head || cdoc.getElementsByTagName("head")[0] || cdoc.documentElement || cdoc;
+ var script = cdoc.createElement("script");
+
+ script.text = "document.write('<plaintext>')";
+
+ head.insertBefore(script, head.firstChild);
+ head.removeChild(script);
+
+ // The plaintext element will be the response container
+ res = cdoc.body.lastChild;
+ }
+
+ // Handles open event
+ _prepareCallback(readResponse(), "opening", 200, rq.transport);
+
+ // Handles message and close event
+ stop = jQuery.atmosphere.iterate(function() {
+ var text = readResponse();
+ if (text.length > rq.lastIndex) {
+ _response.status = 200;
+ _prepareCallback(text, "messageReceived", 200, rq.transport);
+
+ // Empties response every time that it is handled
+ res.innerText = "";
+ rq.lastIndex = 0;
+ }
+
+ if (cdoc.readyState === "complete") {
+ _prepareCallback("", "re-opening", 200, rq.transport);
+ _ieStreaming(rq);
+ return false;
+ }
+ }, null);
+
+ return false;
+ });
+ },
+
+ close: function() {
+ if (stop) {
+ stop();
+ }
+
+ doc.execCommand("Stop");
+ _prepareCallback("", "closed", 200, rq.transport);
+ }
+ };
+ }
+
+ /**
+ * Send message. <br>
+ * Will be automatically dispatch to other connected.
+ *
+ * @param {Object,
+ * string} Message to send.
+ * @private
+ */
+ function _push(message) {
+ if (_activeRequest != null) {
+ _pushAjaxMessage(message);
+ } else if (_ieStream != null) {
+ _pushIE(message);
+ } else if (_jqxhr != null) {
+ _pushJsonp(message);
+ } else if (_websocket != null) {
+ _pushWebSocket(message);
+ }
+ }
+
+ /**
+ * Send a message using currently opened ajax request (using
+ * http-streaming or long-polling). <br>
+ *
+ * @param {string, Object} Message to send. This is an object, string
+ * message is saved in data member.
+ * @private
+ */
+ function _pushAjaxMessage(message) {
+ var rq = _getPushRequest(message);
+ _executeRequest(rq);
+ }
+
+ /**
+ * Send a message using currently opened ie streaming (using
+ * http-streaming or long-polling). <br>
+ *
+ * @param {string, Object} Message to send. This is an object, string
+ * message is saved in data member.
+ * @private
+ */
+ function _pushIE(message) {
+ _pushAjaxMessage(message);
+ }
+
+ /**
+ * Send a message using jsonp transport. <br>
+ *
+ * @param {string, Object} Message to send. This is an object, string
+ * message is saved in data member.
+ * @private
+ */
+ function _pushJsonp(message) {
+ _pushAjaxMessage(message);
+ }
+
+ function _getStringMessage(message) {
+ var msg = message;
+ if (typeof(msg) == 'object') {
+ msg = message.data;
+ }
+ return msg;
+ }
+
+ /**
+ * Build request use to push message using method 'POST' <br>.
+ * Transport is defined as 'polling' and 'suspend' is set to false.
+ *
+ * @return {Object} Request object use to push message.
+ * @private
+ */
+ function _getPushRequest(message) {
+ var msg = _getStringMessage(message);
+
+ var rq = {
+ connected: false,
+ timeout: 60000,
+ method: 'POST',
+ url: _request.url,
+ contentType : _request.contentType,
+ headers: {},
+ cache: true,
+ async: true,
+ ifModified: false,
+ callback: null,
+ dataType: '',
+ data : msg,
+ suspend : false,
+ maxRequest : 60,
+ logLevel : 'info',
+ requestCount : 0,
+ transport: 'polling'
+ };
+
+ if (typeof(message) == 'object') {
+ rq = $.extend(rq, message);
+ }
+
+ return rq;
+ }
+
+ /**
+ * Send a message using currently opened websocket. <br>
+ *
+ * @param {string, Object}
+ * Message to send. This is an object, string message is
+ * saved in data member.
+ */
+ function _pushWebSocket(message) {
+ var msg = _getStringMessage(message);
+ var data;
+ try {
+ if (_request.webSocketUrl != null) {
+ data = _request.webSocketPathDelimiter
+ + _request.webSocketUrl
+ + _request.webSocketPathDelimiter
+ + msg;
+ } else {
+ data = msg;
+ }
+
+ _websocket.send(data);
+
+ } catch (e) {
+ jQuery.atmosphere.log(_request.logLevel, ["Websocket failed. Downgrading to Comet and resending " + data]);
+
+ _websocket.onclose = function(message) {
+ };
+ _websocket.close();
+
+ _reconnectWithFallbackTransport();
+ _pushAjaxMessage(message);
+ }
+ }
+
+ function _prepareCallback(messageBody, state, errorCode, transport) {
+
+ if (state == "messageReceived") {
+ if (_trackMessageSize(messageBody, _request, _response)) return;
+ }
+
+ _response.transport = transport;
+ _response.status = errorCode;
+
+ // If not -1, we have buffered the message.
+ if (_response.expectedBodySize == -1) {
+ _response.responseBody = messageBody;
+ }
+ _response.state = state;
+
+ _invokeCallback();
+ }
+
+ function _invokeFunction(response) {
+ _f(response, _request);
+ // Global
+ _f(response, jQuery.atmosphere);
+ }
+
+ function _f(response, f) {
+ switch (response.state) {
+ case "messageReceived" :
+ if (typeof(f.onMessage) != 'undefined') f.onMessage(response);
+ break;
+ case "error" :
+ if (typeof(f.onError) != 'undefined') f.onError(response);
+ break;
+ case "opening" :
+ if (typeof(f.onOpen) != 'undefined') f.onOpen(response);
+ break;
+ case "messagePublished" :
+ if (typeof(f.onMessagePublished) != 'undefined') f.onMessagePublished(response);
+ break;
+ case "re-opening" :
+ if (typeof(f.onReconnect) != 'undefined') f.onReconnect(_request, response);
+ break;
+ case "closed" :
+ if (typeof(f.onClose) != 'undefined') f.onClose(response);
+ break;
+ }
+ }
+
+ /**
+ * Invoke request callbacks.
+ *
+ * @private
+ */
+ function _invokeCallback() {
+ var call = function (index, func) {
+ func(_response);
+ };
+
+ _invokeFunction(_response);
+
+ // Invoke global callbacks
+ if (jQuery.atmosphere.callbacks.length > 0) {
+ jQuery.atmosphere.debug("Invoking " + jQuery.atmosphere.callbacks.length + " global callbacks: " + _response.state);
+ try {
+ jQuery.each(jQuery.atmosphere.callbacks, call);
+ } catch (e) {
+ jQuery.atmosphere.log(_request.logLevel, ["Callback exception" + e]);
+ }
+ }
+
+ // Invoke request callback
+ if (typeof(_request.callback) == 'function') {
+ if (_request.logLevel == 'debug') {
+ jQuery.atmosphere.debug("Invoking request callbacks");
+ }
+ try {
+ _request.callback(_response);
+ } catch (e) {
+ jQuery.atmosphere.log(_request.logLevel, ["Callback exception" + e]);
+ }
+ }
+ }
+
+ /**
+ * Close request.
+ *
+ * @private
+ */
+ function _close() {
+ _abordingConnection = true;
+ _response.state = 'unsubscribe';
+ _response.responseBody = "";
+ _response.status = 408;
+ _invokeCallback();
+
+ if (_ieStream != null) {
+ _ieStream.close();
+ _ieStream = null;
+ _abordingConnection = false;
+ }
+ if (_jqxhr != null) {
+ _jqxhr.abort();
+ _jqxhr = null;
+ _abordingConnection = false;
+ }
+ if (_activeRequest != null) {
+ _activeRequest.abort();
+ _activeRequest = null;
+ _abordingConnection = false;
+ }
+ if (_websocket != null) {
+ _closingWebSocket = true;
+ _websocket.close();
+ _websocket = null;
+ }
+ }
+
+ this.subscribe = function(options) {
+ _subscribe(options);
+ };
+
+ this.execute = function() {
+ _execute();
+ };
+
+ this.invokeCallback = function() {
+ _invokeCallback();
+ };
+
+ this.close = function() {
+ _close();
+ };
+
+ this.getUrl = function() {
+ return _request.url;
+ };
+
+ this.push = function(message) {
+ _push(message);
+ }
+
+ this.response = _response;
+ },
+
+ subscribe: function(url, callback, request) {
+ if (typeof(callback) == 'function') {
+ jQuery.atmosphere.addCallback(callback);
+ }
+
+ if (typeof(url) != "string") {
+ request = url;
+ } else {
+ request.url = url;
+ }
+
+ var rq = new jQuery.atmosphere.AtmosphereRequest(request);
+ jQuery.atmosphere.requests[jQuery.atmosphere.requests.length] = rq;
+ return rq;
+ },
+
+ addCallback: function(func) {
+ if (jQuery.inArray(func, jQuery.atmosphere.callbacks) == -1) {
+ jQuery.atmosphere.callbacks.push(func);
+ }
+ },
+
+ removeCallback: function(func) {
+ var index = jQuery.inArray(func, jQuery.atmosphere.callbacks);
+ if (index != -1) {
+ jQuery.atmosphere.callbacks.splice(index, 1);
+ }
+ },
+
+ unsubscribe : function() {
+ if (jQuery.atmosphere.requests.length > 0) {
+ for (var i = 0; i < jQuery.atmosphere.requests.length; i++) {
+ jQuery.atmosphere.requests[i].close();
+ }
+ }
+ jQuery.atmosphere.requests = [];
+ jQuery.atmosphere.callbacks = [];
+ },
+
+ unsubscribeUrl: function(url) {
+ var idx = -1;
+ if (jQuery.atmosphere.requests.length > 0) {
+ for (var i = 0; i < jQuery.atmosphere.requests.length; i++) {
+ var rq = jQuery.atmosphere.requests[i];
+
+ // Suppose you can subscribe once to an url
+ if (rq.getUrl() == url) {
+ rq.close();
+ idx = i;
+ break;
+ }
+ }
+ }
+ if (idx >= 0) {
+ jQuery.atmosphere.requests.splice(idx, 1);
+ }
+ },
+
+ publish: function(request) {
+ if (typeof(request.callback) == 'function') {
+ jQuery.atmosphere.addCallback(callback);
+ }
+ request.transport = "polling";
+
+ var rq = new jQuery.atmosphere.AtmosphereRequest(request);
+ jQuery.atmosphere.requests[jQuery.atmosphere.requests.length] = rq;
+ return rq;
+ },
+
+ checkCORSSupport : function() {
+ if (jQuery.browser.msie && !window.XDomainRequest) {
+ return true;
+ } else if (jQuery.browser.opera) {
+ return true;
+ }
+
+ // Force Android to use CORS as some version like 2.2.3 fail otherwise
+ var ua = navigator.userAgent.toLowerCase();
+ var isAndroid = ua.indexOf("android") > -1;
+ if (isAndroid) {
+ return true;
+ }
+ return false;
+ },
+
+ S4 : function() {
+ return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+ },
+
+ guid : function() {
+ return (jQuery.atmosphere.S4() + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + "-" + jQuery.atmosphere.S4() + jQuery.atmosphere.S4() + jQuery.atmosphere.S4());
+ },
+
+ // From jQuery-Stream
+ prepareURL: function(url) {
+ // Attaches a time stamp to prevent caching
+ var ts = jQuery.now();
+ var ret = url.replace(/([?&])_=[^&]*/, "$1_=" + ts);
+
+ return ret + (ret === url ? (/\?/.test(url) ? "&" : "?") + "_=" + ts : "");
+ },
+
+ // From jQuery-Stream
+ param : function(data) {
+ return jQuery.param(data, jQuery.ajaxSettings.traditional);
+ },
+
+ iterate : function (fn, interval) {
+ var timeoutId;
+
+ // Though the interval is 0 for real-time application, there is a delay between setTimeout calls
+ // For detail, see https://developer.mozilla.org/en/window.setTimeout#Minimum_delay_and_timeout_nesting
+ interval = interval || 0;
+
+ (