├── samples-and-tests
└── pushplay-app
│ ├── public
│ ├── stylesheets
│ │ └── main.css
│ ├── images
│ │ └── favicon.png
│ └── javascripts
│ │ └── jquery-1.6.4.min.js
│ ├── modules
│ └── pushplay
│ ├── conf
│ ├── messages
│ ├── dependencies.yml
│ ├── routes
│ └── application.conf
│ ├── test
│ ├── data.yml
│ ├── Application.test.html
│ ├── ApplicationTest.java
│ └── BasicTest.java
│ └── app
│ ├── views
│ ├── errors
│ │ ├── 404.html
│ │ └── 500.html
│ ├── main.html
│ └── Application
│ │ ├── index.html
│ │ └── presence.html
│ └── controllers
│ └── Application.java
├── src
├── play.plugins
└── play
│ └── modules
│ └── pushplay
│ ├── TopicMessage.java
│ ├── ChannelData.java
│ ├── TriggerEventMessage.java
│ ├── Payload.java
│ ├── Message.java
│ ├── PushPlayUtil.java
│ └── PushPlayPlugin.java
├── lib
└── play-pushplay.jar
├── conf
├── dependencies.yml
├── messages
└── routes
├── .gitignore
├── commands.py
├── documentation
└── manual
│ └── home.textile
├── test
└── controllers
│ └── pushplay
│ └── PushplayWebSocketTest.java
├── README.md
├── app
└── controllers
│ └── pushplay
│ └── PushPlayWebSocket.java
└── public
└── javascripts
└── pusher.min.js
/samples-and-tests/pushplay-app/public/stylesheets/main.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/play.plugins:
--------------------------------------------------------------------------------
1 | 2001:play.modules.pushplay.PushPlayPlugin
--------------------------------------------------------------------------------
/lib/play-pushplay.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danbeaulieu/PushPlay/HEAD/lib/play-pushplay.jar
--------------------------------------------------------------------------------
/conf/dependencies.yml:
--------------------------------------------------------------------------------
1 | self: play -> pushplay 0.1
2 |
3 | require:
4 | - play
5 | - play -> hazelcast 0.4
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/modules/pushplay:
--------------------------------------------------------------------------------
1 | /Users/dbeaulieu/code/PushPlay/samples-and-tests/pushplay-app/../../../pushplay
2 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/conf/messages:
--------------------------------------------------------------------------------
1 | # You can specialize this file for each language.
2 | # For example, for French create a messages.fr file
3 | #
4 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/public/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/danbeaulieu/PushPlay/HEAD/samples-and-tests/pushplay-app/public/images/favicon.png
--------------------------------------------------------------------------------
/conf/messages:
--------------------------------------------------------------------------------
1 | # Default pushplay messages
2 | # You can specialize this file for each language.
3 | # For exemple, for french create a messages.fr file
4 |
5 | #pushplay.name=pushplay
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/test/data.yml:
--------------------------------------------------------------------------------
1 | # you describe your data using the YAML notation here
2 | # and then load them using Fixtures.load("data.yml")
3 |
4 | # User(bob):
5 | # email: bob@gmail.com
6 | # password: secret
7 | # fullname: Bob
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/test/Application.test.html:
--------------------------------------------------------------------------------
1 | *{ You can use plain selenium command using the selenium tag }*
2 |
3 | #{selenium}
4 | // Open the home page, and check that no error occured
5 | open('/')
6 | assertNotTitle('Application error')
7 | #{/selenium}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # playframework
2 | samples-and-tests/pushplay-app/eclipse
3 | tmp
4 | test-result
5 | out
6 | logs
7 | server.pid
8 | *.ser
9 | deploy/*.jar
10 | *.pyc
11 | .DS_Store
12 | .project
13 | .settings
14 | .classpath
15 | /eclipse
16 | /dist
17 | lib
18 | /modules
19 | samples-and-tests/pushplay-app/modules
20 |
--------------------------------------------------------------------------------
/src/play/modules/pushplay/TopicMessage.java:
--------------------------------------------------------------------------------
1 | package play.modules.pushplay;
2 |
3 | public class TopicMessage {
4 |
5 | public String playServerId;
6 |
7 | public String message;
8 |
9 | public TopicMessage(String id, String msg) {
10 |
11 | this.playServerId = id;
12 | this.message = msg;
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/conf/dependencies.yml:
--------------------------------------------------------------------------------
1 | # Application dependencies
2 |
3 | require:
4 | - play -> play master-da9bb15
5 | - pushplay 0.1
6 | - play -> hazelcast 0.4
7 |
8 | repositories:
9 | - localRepo:
10 | type: local
11 | artifact: ${application.path}/../../../[module]
12 | contains:
13 | - pushplay
14 |
--------------------------------------------------------------------------------
/conf/routes:
--------------------------------------------------------------------------------
1 | # This file defines all module routes (Higher priority routes first)
2 | #
3 | # import these routes in the main app as :
4 | # * / module:pushplay
5 | #
6 | # ~~~~
7 | WS /app/{apiKey} pushplay.PushPlayWebSocket.StreamSocket.app
8 | GET /? Module.index
9 |
10 | POST /apps/{appId}/channels/{channel}/events pushplay.PushPlayWebSocket.apps
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/test/ApplicationTest.java:
--------------------------------------------------------------------------------
1 | import org.junit.*;
2 | import play.test.*;
3 | import play.mvc.*;
4 | import play.mvc.Http.*;
5 |
6 | public class ApplicationTest extends FunctionalTest {
7 |
8 | @Test
9 | public void testThatIndexPageWorks() {
10 | Response response = GET("/");
11 | assertIsOk(response);
12 | assertContentType("text/html", response);
13 | assertCharset(play.Play.defaultWebEncoding, response);
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/app/views/errors/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Not found
6 |
7 |
8 |
9 | #{if play.mode.name() == 'DEV'}
10 | #{404 result /}
11 | #{/if}
12 | #{else}
13 | Not found
14 |
15 | ${result.message}
16 |
17 | #{/else}
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/play/modules/pushplay/ChannelData.java:
--------------------------------------------------------------------------------
1 | package play.modules.pushplay;
2 |
3 | import com.google.gson.Gson;
4 |
5 | import java.util.Map;
6 |
7 | /**
8 | * Created by IntelliJ IDEA.
9 | * User: dbeaulieu
10 | * Date: 2/27/12
11 | * Time: 2:21 PM
12 | * To change this template use File | Settings | File Templates.
13 | */
14 | public class ChannelData {
15 |
16 | public String user_id;
17 |
18 | public Map user_info;
19 |
20 | public String toJSON() {
21 |
22 | return new Gson().toJson(this);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/app/views/errors/500.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Application error
6 |
7 |
8 |
9 | #{if play.mode.name() == 'DEV'}
10 | #{500 exception /}
11 | #{/if}
12 | #{else}
13 | Oops, an error occured
14 | #{if exception instanceof play.exceptions.PlayException}
15 |
16 | This exception has been logged with id ${exception.id} .
17 |
18 | #{/if}
19 | #{/else}
20 |
21 |
22 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/conf/routes:
--------------------------------------------------------------------------------
1 | # Routes
2 | # This file defines all application routes (Higher priority routes first)
3 | # ~~~~
4 |
5 | # Home page
6 | GET / Application.index
7 | GET /presence Application.presence
8 |
9 | * / module:pushplay
10 |
11 | POST /pusher/auth Application.auth
12 |
13 | # Ignore favicon requests
14 | GET /favicon.ico 404
15 |
16 | # Map static resources from the /app/public folder to the /public path
17 | GET /public/ staticDir:public
18 |
19 | # Catch all
20 | * /{controller}/{action} {controller}.{action}
21 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/app/views/main.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #{get 'title' /}
6 |
7 |
8 | #{get 'moreStyles' /}
9 |
10 |
11 | #{get 'moreScripts' /}
12 |
13 |
14 |
15 |
16 | #{doLayout /}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/commands.py:
--------------------------------------------------------------------------------
1 | # Here you can create play commands that are specific to the module, and extend existing commands
2 |
3 | MODULE = 'pushplay'
4 |
5 | # Commands that are specific to your module
6 |
7 | COMMANDS = ['pushplay:hello']
8 |
9 | def execute(**kargs):
10 | command = kargs.get("command")
11 | app = kargs.get("app")
12 | args = kargs.get("args")
13 | env = kargs.get("env")
14 |
15 | if command == "pushplay:hello":
16 | print "~ Hello"
17 |
18 |
19 | # This will be executed before any command (new, run...)
20 | def before(**kargs):
21 | command = kargs.get("command")
22 | app = kargs.get("app")
23 | args = kargs.get("args")
24 | env = kargs.get("env")
25 |
26 |
27 | # This will be executed after any command (new, run...)
28 | def after(**kargs):
29 | command = kargs.get("command")
30 | app = kargs.get("app")
31 | args = kargs.get("args")
32 | env = kargs.get("env")
33 |
34 | if command == "new":
35 | pass
36 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/app/views/Application/index.html:
--------------------------------------------------------------------------------
1 | #{extends 'main.html' /}
2 |
3 | #{set title:'Home' /}
4 |
5 |
34 |
35 | PushPlay!
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/test/BasicTest.java:
--------------------------------------------------------------------------------
1 | import com.google.gson.*;
2 | import org.junit.*;
3 |
4 | import java.io.IOException;
5 | import java.util.*;
6 |
7 | import play.modules.pushplay.Message;
8 | import play.test.*;
9 |
10 | public class BasicTest extends UnitTest {
11 |
12 | @Test
13 | public void aVeryImportantThingToTest() {
14 | assertEquals(2, 1 + 1);
15 | }
16 |
17 | @Test
18 | public void testJSONObject() {
19 | MockMessage m = new MockMessage();
20 | m.socket_id = "12345";
21 | m.channel = "presence-chat";
22 | JsonObject data = new JsonObject();
23 | JsonObject user_id = new JsonObject();
24 | user_id.addProperty("user_id", "danb");
25 | data.add("channel_data", user_id);
26 |
27 | Map foo = new HashMap();
28 | foo.put("1234", "dan");
29 | System.out.println(new Gson().toJson(foo));
30 | System.out.println(new Gson().toJson(m));
31 |
32 | }
33 |
34 | class MockMessage {
35 |
36 | public String socket_id;
37 |
38 | public JsonObject data;
39 |
40 | public String channel;
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/documentation/manual/home.textile:
--------------------------------------------------------------------------------
1 | h1. {module name}
2 |
3 | p(note). Write your module documentation in this file, possibly linking to other Textile files, and delete these notes when you are finished.
4 |
5 | p(note). Start with a clear statement of what the module is and what it is for. Link to any third-party libraries that the module is based on.
6 |
7 | p(note). Briefly describe why you might want to use this module in a Play application.
8 |
9 | h2. Sample application
10 |
11 | p(note). Your module should contain a sample app in the @sample@ directory that demonstrates the module. Describe how to run the demo here.
12 |
13 | h2. Getting started
14 |
15 | p(note). Describe any installation that is required, beyond adding and resolving the module dependency.
16 |
17 | h2. Configuration
18 |
19 | p(note). List any configuration options that the module uses, with an @h3.@ heading for each one.
20 |
21 | h2. Usage
22 |
23 | p(note). Explain how to use the module, and describe any routes, controllers, models or views that the module provides.
24 |
25 | h2. Tags
26 |
27 | p(note). List any tags the module provides, with an @h3.@ heading for each one. Include the tags’ arguments.
28 |
29 | h2. Commands
30 |
31 | p(note). List any commands the module provides, with an @h3.@ heading for each one. Include the commands’ options.
32 |
--------------------------------------------------------------------------------
/src/play/modules/pushplay/TriggerEventMessage.java:
--------------------------------------------------------------------------------
1 | package play.modules.pushplay;
2 |
3 | import play.mvc.Scope.Params;
4 |
5 | public class TriggerEventMessage {
6 |
7 | public String auth_key;
8 |
9 | public String auth_timestamp;
10 |
11 | public String auth_version;
12 |
13 | public String body_md5;
14 |
15 | public String name;
16 |
17 | public String socket_id;
18 |
19 | public String auth_signature;
20 |
21 | public TriggerEventMessage(Params params) {
22 |
23 | this.auth_key = params.get("auth_key");
24 | this.auth_timestamp = params.get("auth_timestamp");
25 | this.auth_version = params.get("auth_version");
26 | this.body_md5 = params.get("body_md5");
27 | this.name = params.get("name");
28 | this.socket_id = params.get("socket_id");
29 | this.auth_signature = params.get("auth_signature");
30 | }
31 |
32 | public String toString() {
33 |
34 | StringBuilder sb = new StringBuilder();
35 |
36 | sb.append("auth_key=" + this.auth_key);
37 | sb.append("&auth_timestamp=" + this.auth_timestamp);
38 | sb.append("&auth_version=" + this.auth_version);
39 | sb.append("&body_md5=" + this.body_md5);
40 | sb.append("&name=" + this.name);
41 | sb.append((this.socket_id != null ? "&socket_id=" + socket_id : ""));
42 |
43 | return sb.toString();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/app/controllers/Application.java:
--------------------------------------------------------------------------------
1 | package controllers;
2 |
3 | import play.*;
4 | import play.modules.pushplay.ChannelData;
5 | import play.modules.pushplay.PushPlayPlugin;
6 | import play.modules.pushplay.PushPlayUtil;
7 | import play.mvc.*;
8 | import play.server.Server;
9 |
10 | import java.util.*;
11 |
12 | public class Application extends Controller {
13 |
14 | public static void index() {
15 | renderArgs.put("playPort", Server.httpPort);
16 | render();
17 | }
18 |
19 | public static void presence() {
20 | renderArgs.put("playPort", Server.httpPort);
21 | render();
22 | }
23 |
24 | public static void auth() {
25 | // TODO need to add in channel_data too for presence channels
26 | ChannelData cd = new ChannelData();
27 | if (params.get("channel_name").startsWith("presence-")) {
28 | cd.user_id = UUID.randomUUID().toString();
29 | cd.user_info = new HashMap(){{ put("name", UUID.randomUUID().toString().substring(8));}};
30 | }
31 | String token = PushPlayUtil.authToken(params.get("socket_id"), params.get("channel_name"), cd.toJSON());
32 | renderText("{\"auth\":\"" + Play.configuration.getProperty("pusher.key") + ":" + token + "\",\"channel_data\":" + cd.toJSON() + "}");
33 | }
34 |
35 | }
--------------------------------------------------------------------------------
/src/play/modules/pushplay/Payload.java:
--------------------------------------------------------------------------------
1 | package play.modules.pushplay;
2 |
3 | import com.google.gson.Gson;
4 |
5 | import java.util.*;
6 |
7 | /**
8 | * Created by IntelliJ IDEA.
9 | * User: dbeaulieu
10 | * Date: 2/27/12
11 | * Time: 1:29 PM
12 | * To change this template use File | Settings | File Templates.
13 | */
14 | public class Payload {
15 |
16 | Presence presence;
17 |
18 | public String socket_id;
19 |
20 | public String channel;
21 |
22 | public String auth;
23 |
24 | public ChannelData channel_data;
25 |
26 | public String user_id;
27 |
28 | public Map user_info;
29 |
30 | public Payload(String socket_id) {
31 |
32 | this.socket_id = socket_id;
33 | }
34 |
35 | public Payload() {
36 |
37 | }
38 |
39 | public void updatePresence(List members) {
40 |
41 | Presence p = new Presence();
42 |
43 | Set unique = new HashSet();
44 | unique.addAll(members);
45 | p.count = unique.size();
46 |
47 | for (String s : unique) {
48 | ChannelData cd = new Gson().fromJson(s, ChannelData.class);
49 | p.hash.put(cd.user_id, cd.user_info);
50 | }
51 |
52 | this.presence = p;
53 | }
54 |
55 | private class Presence {
56 |
57 | int count = -1;
58 |
59 | Map> hash = new HashMap>();
60 |
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/play/modules/pushplay/Message.java:
--------------------------------------------------------------------------------
1 | package play.modules.pushplay;
2 |
3 | import java.util.Map;
4 |
5 | import com.google.gson.JsonObject;
6 | import com.google.gson.annotations.SerializedName;
7 |
8 | public class Message {
9 |
10 | String channel;
11 |
12 | String event;
13 | // This needs to be something other than Map. Maybe JsonObject?
14 | //JsonObject data;
15 | //@SerializedName("data")
16 | Payload data;
17 |
18 | public Message() { }
19 |
20 | public Message(String channel, String event, Payload data) {
21 |
22 | this.channel = channel;
23 | this.event = event;
24 | this.data = data;
25 | }
26 |
27 | public String getChannel() {
28 |
29 | return channel;
30 | }
31 |
32 | public void setChannel(String channel) {
33 |
34 | this.channel = channel;
35 | }
36 |
37 | public String getEvent() {
38 |
39 | return event;
40 | }
41 |
42 | public void setEvent(String event) {
43 |
44 | this.event = event;
45 | }
46 |
47 | public Payload getData() {
48 |
49 | return data;
50 | }
51 |
52 | public void setData(Payload data) {
53 |
54 | this.data = data;
55 |
56 | }
57 |
58 | public void clear() {
59 |
60 | this.channel = null;
61 | this.event = null;
62 | this.data = null;
63 | }
64 |
65 | public String toJSON() {
66 | JsonObject json = new JsonObject();
67 | json.addProperty("event", this.getEvent());
68 | json.addProperty("channel", this.getChannel());
69 | //json.add("data", this.getData());
70 |
71 | return json.toString();
72 | }
73 |
74 | }
75 |
76 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/app/views/Application/presence.html:
--------------------------------------------------------------------------------
1 | #{extends 'main.html' /}
2 |
3 | #{set title:'Home' /}
4 |
5 |
47 |
48 | PushPlay!
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/test/controllers/pushplay/PushplayWebSocketTest.java:
--------------------------------------------------------------------------------
1 | package controllers.pushplay;
2 |
3 |
4 | import com.google.gson.*;
5 | import org.junit.Test;
6 |
7 | import play.modules.pushplay.Message;
8 | import play.modules.pushplay.Payload;
9 |
10 | import java.lang.reflect.Type;
11 | import java.util.ArrayList;
12 | import java.util.HashMap;
13 | import java.util.List;
14 | import java.util.Map;
15 |
16 | public class PushplayWebSocketTest {
17 |
18 | @Test
19 | public void testGson() {
20 | String data = "{\"hit\":\"one\"}";
21 | System.out.println(data);
22 | String temp = "{\"channel\":\"foo\",\"event\":\"foo_update\",\"data\":" + data + "}";
23 | System.out.println(temp);
24 | System.out.println(new JsonParser().parse(temp).getAsJsonObject().get("data"));
25 | }
26 |
27 | @Test
28 | public void testJSONObject() {
29 |
30 | Message outgoing = new Message();
31 | outgoing.setEvent("pusher:connection_established");
32 | outgoing.setData(new Payload("123456789"));
33 | System.out.println(new Gson().toJson(outgoing));
34 | String txt = new Gson().toJson(outgoing);
35 | System.out.println(txt);
36 | GsonBuilder gson_builder = new GsonBuilder();
37 | gson_builder.registerTypeAdapter(
38 | JsonObject.class,
39 | new JsonDeserializer() {
40 | @Override
41 | public JsonElement deserialize(JsonElement arg0,
42 | Type arg1,
43 | JsonDeserializationContext arg2)
44 | throws JsonParseException {
45 |
46 | return arg0.getAsJsonObject();
47 | }
48 | } );
49 |
50 | Message incoming = gson_builder.create().fromJson(txt, Message.class);
51 | System.out.println(incoming.getData().socket_id);
52 | }
53 |
54 | class MockMessage {
55 |
56 | public String socket_id;
57 |
58 | public JsonObject data;
59 |
60 | public String channel;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/play/modules/pushplay/PushPlayUtil.java:
--------------------------------------------------------------------------------
1 | package play.modules.pushplay;
2 |
3 | import java.io.UnsupportedEncodingException;
4 | import java.math.BigInteger;
5 | import java.security.InvalidKeyException;
6 | import java.security.MessageDigest;
7 | import java.security.NoSuchAlgorithmException;
8 |
9 | import javax.crypto.Mac;
10 | import javax.crypto.spec.SecretKeySpec;
11 |
12 | import org.apache.commons.codec.digest.DigestUtils;
13 |
14 | import play.mvc.Http.Request;
15 |
16 | public class PushPlayUtil {
17 |
18 | public static String authToken(String socket_id, String channel, String channel_data) {
19 |
20 | String string_to_sign = socket_id + ":" + channel;
21 |
22 | if (channel_data != null) {
23 | string_to_sign = string_to_sign + ":" + channel_data;
24 | }
25 | return PushPlayUtil.sha256(string_to_sign, PushPlayPlugin.getSecret());
26 | }
27 |
28 | public static boolean isRequestValid(Request request,
29 | TriggerEventMessage tem) {
30 |
31 | String signature = "POST\n" + request.path + "\n" + tem.toString();
32 |
33 | return (tem.auth_signature.equals(sha256(signature, PushPlayPlugin.getSecret())));
34 | }
35 |
36 | public static String sha256(String string, String secret) {
37 |
38 | try {
39 | SecretKeySpec signingKey = new SecretKeySpec( secret.getBytes(), "HmacSHA256");
40 |
41 | final Mac mac = Mac.getInstance("HmacSHA256");
42 | mac.init(signingKey);
43 |
44 | byte[] digest = mac.doFinal(string.getBytes());
45 |
46 | BigInteger bigInteger = new BigInteger(1,digest);
47 | return String.format("%0" + (digest.length << 1) + "x", bigInteger);
48 |
49 | } catch (NoSuchAlgorithmException nsae) {
50 | throw new RuntimeException("No HMac SHA256 algorithm");
51 | } catch (InvalidKeyException e) {
52 | throw new RuntimeException("Invalid key exception while converting to HMac SHA256");
53 | }
54 | }
55 |
56 | public static boolean isMD5Valid(String message, TriggerEventMessage tem) {
57 |
58 | return (DigestUtils.md5Hex(message).equals(tem.body_md5));
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/play/modules/pushplay/PushPlayPlugin.java:
--------------------------------------------------------------------------------
1 | package play.modules.pushplay;
2 |
3 | import java.util.HashMap;
4 | import java.util.Map;
5 |
6 | import com.google.gson.Gson;
7 | import com.hazelcast.core.HazelcastInstance;
8 | import com.hazelcast.core.ITopic;
9 | import com.hazelcast.core.MessageListener;
10 | import com.hazelcast.monitor.LocalTopicStats;
11 |
12 |
13 | import play.Logger;
14 | import play.Play;
15 | import play.PlayPlugin;
16 | import play.libs.F.EventStream;
17 | import play.modules.hazelcast.HazelcastPlugin;
18 | import play.mvc.Http.Inbound;
19 |
20 | public class PushPlayPlugin extends PlayPlugin {
21 |
22 | public static Map> streams = new HashMap>();
23 |
24 | public static HazelcastInstance hazel;
25 |
26 | static {
27 |
28 | String appId = Play.configuration.getProperty("pusher.appId");
29 |
30 | String key = Play.configuration.getProperty("pusher.key");
31 |
32 | String secret = Play.configuration.getProperty("pusher.secret");
33 | }
34 |
35 | @Override
36 | public void onApplicationStart() {
37 |
38 | Logger.info("Initializing PushPlayPlugin on %s", Play.id);
39 | hazel = HazelcastPlugin.getHazel();
40 |
41 | if (hazel != null) {
42 |
43 | ITopic topic = hazel.getTopic("pushplay");
44 | topic.addMessageListener(new MessageListener() {
45 | public void onMessage(String msg) {
46 | TopicMessage incoming = new Gson().fromJson(msg, TopicMessage.class);
47 | if (!incoming.playServerId.equals(Play.id)) {
48 | publishToAllEventStreams(incoming.message);
49 | Logger.debug("Message received via Hazelcast Topic - %s", msg);
50 | }
51 | }
52 | });
53 | }
54 | else {
55 | Logger.warn("hazel instance is not available, are you sure it isn't disabled in the conf?");
56 | }
57 | }
58 | private static void publishToAllEventStreams(Message msg) {
59 | publishToAllEventStreams(new Gson().toJson(msg));
60 | }
61 |
62 | private static void publishToAllEventStreams(String msg) {
63 | for (EventStream es: streams.values()) {
64 | es.publish(msg);
65 | }
66 | }
67 |
68 | public static void publishMessage(Message msg) {
69 | publishMessage(new Gson().toJson(msg));
70 | }
71 |
72 | public static void publishMessage(String msg) {
73 |
74 | // we publish locally first even though we'll get it back when we publish to the topic
75 | // just in case there is a hazelcast problem this app server will still be able to handle
76 | // messages
77 | publishToAllEventStreams(msg);
78 |
79 | // don't publish to cluster internal messages
80 | if (hazel != null && !msg.contains("pusher_internal:subscription_succeeded")) {
81 | ITopic topic = hazel.getTopic("pushplay");
82 | Logger.debug("Publishing message: %s", msg);
83 | TopicMessage topicMessage = new TopicMessage(Play.id, msg);
84 | topic.publish(new Gson().toJson(topicMessage));
85 | }
86 | }
87 |
88 | public static String getSecret() {
89 |
90 | return Play.configuration.getProperty("pusher.secret");
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PushPlay
2 |
3 | PushPlay is a Play! Framework module that turns any Play! application into a Pusher service clone. It is compatible with the MIT licensed pusher javascript library.
4 | This is considered in alpha stage, there is still work to be done, please see the TODO list below.
5 |
6 | ## Requirements
7 |
8 | - Play 1.2.4
9 | - Websockets compatible browser
10 |
11 | ## Usage
12 |
13 | This is packaged as a module, so you just need to add it as a dependency to an existing Play! application (Refer to the sample app to see how the dependency looks). You are free to create a
14 | completely empty Play! project that just serves as a container for the PushPlay module. However, you should have an end point at /pusher/auth that generates authentication tokens
15 | for certain events (private channel subscriptions, etc). Refer to the sample application in samples-and-tests/.
16 |
17 | You should also modify the application.conf file to include these three properties:
18 |
19 | pusher.appId
20 | pusher.key
21 | pusher.secret
22 |
23 |
24 | You'll also need to overwrite the pusher connection settings in the pusher javascript client library. Namely the Pusher host and ws_port settings. Check out the index.html view in the sample
25 | application.
26 |
27 | Since integrating with Hazelcast there a few more things to work out. Hazelcast is a clustering data distribution library. It's awesome. Read more at hazelcast.com
28 |
29 | Any application that uses the PushPlay module will need to add the Play! hazelcast plugin to it's dependencies.
30 |
31 |
32 | - play -> hazelcast 0.4
33 |
34 |
35 | and then update your dependencies. For example:
36 |
37 |
38 | play dependencies pushplay/samples-and-tests/pushplay-app/ --sync
39 |
40 |
41 | From there you can configure the hazelcast cluster as you normally would (See the docs). The hazelcast config is in {your-app}/conf/hazelcast-0.4/hazelcast.xml. You can also disable hazelcast altogether if you don't want to use it. See the hazelcast plugin documentation for more info.
42 |
43 | ## Run The Sample App
44 |
45 | Goes something like this:
46 |
47 | - $git clone git@github.com:danbeaulieu/PushPlay.git
48 | - $cd PushPlay
49 | - $mkdir modules
50 | - $play dependencies
51 | - $play build-module
52 | - $cd samples-and-tests/
53 | - $play dependencies pushplay-app/
54 | - $play run pushplay-app/ --%dev1
55 |
56 | ## Alternatives
57 |
58 | Slanger - An open source, robust, self contained Pusher protocol server from Stevie Graham.
59 |
60 | Pusher - The service itself! They even have a free service plan!
61 |
62 | ## TODO
63 |
64 | - Finish up presence support
65 | - Use a database for message passing and state, besides the obvious benefits this will also help with scaling.
66 | - Clean up the code, lots can be refactored.
67 | - Add instructions on how to run the sample app
68 |
69 | ## Notes
70 |
71 | The Play! Framework relies on the netty networking library for websocket support. A newer websocket protocol was recently released that is not currently supported by netty in any of the Generally Available releases. It will take some time for implementation to be completed, netty to be released and then another Play! release with the newer netty dependency. For now, don't use Chrome, as it is already using the new protocol. The sample app was tested with Safari 5.1.
72 |
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/conf/application.conf:
--------------------------------------------------------------------------------
1 | # This is the main configuration file for the application.
2 | # ~~~~~
3 | application.name=pushplay-app
4 |
5 | # Application mode
6 | # ~~~~~
7 | # Set to dev to enable instant reloading and other development help.
8 | # Otherwise set to prod.
9 | application.mode=dev
10 | %prod.application.mode=prod
11 |
12 | # Secret key
13 | # ~~~~~
14 | # The secret key is used to secure cryptographics functions
15 | # If you deploy your application to several instances be sure to use the same key !
16 | application.secret=dtY5V3jEx3nSuGpCys5mTYG3jo4KZn3nuoRAPTnRfXHZjtHBkUPw6npFg1y4lBwt
17 |
18 | # i18n
19 | # ~~~~~
20 | # Define locales used by your application.
21 | # You can then place localized messages in conf/messages.{locale} files
22 | # application.langs=fr,en,ja
23 |
24 | # Date format
25 | # ~~~~~
26 | date.format=yyyy-MM-dd
27 | # date.format.fr=dd/MM/yyyy
28 |
29 | # Server configuration
30 | # ~~~~~
31 | # If you need to change the HTTP port, uncomment this (default is set to 9000)
32 | %dev1.http.port=9001
33 | %dev2.http.port=9002
34 | #hazelcast.disabled=true
35 | #
36 | # By default the server listen for HTTP on the wilcard address.
37 | # You can restrict this.
38 | # http.address=127.0.0.1
39 | #
40 | # Use this if you don't host your Play application at the root of the domain
41 | # you're serving it from. This parameter has no effect when deployed as a
42 | # war, because the path will be handled by the application server.
43 | # http.path=/
44 |
45 | # Session configuration
46 | # ~~~~~~~~~~~~~~~~~~~~~~
47 | # By default, session will be written to the transient PLAY_SESSION cookie.
48 | # The cookies are not secured by default, only set it to true
49 | # if you're serving your pages through https.
50 | # application.session.cookie=PLAY
51 | # application.session.maxAge=1h
52 | # application.session.secure=false
53 |
54 | # Session/Cookie sharing between subdomain
55 | # ~~~~~~~~~~~~~~~~~~~~~~
56 | # By default a cookie is only valid for a specific domain. By setting
57 | # application.defaultCookieDomain to '.example.com', the cookies
58 | # will be valid for all domains ending with '.example.com', ie:
59 | # foo.example.com and bar.example.com
60 | # application.defaultCookieDomain=.example.com
61 |
62 | # JVM configuration
63 | # ~~~~~
64 | # Define which port is used by JPDA when application is in debug mode (default is set to 8000)
65 | # jpda.port=8000
66 | #
67 | # Java source level => 1.5, 1.6 or 1.7 (experimental)
68 | # java.source=1.5
69 |
70 | # Log level
71 | # ~~~~~
72 | # Specify log level for your application.
73 | # If you want a very customized log, create a log4j.properties file in the conf directory
74 | application.log=DEBUG
75 | #
76 | # More logging configuration
77 | # application.log.path=/log4j.properties
78 | # application.log.system.out=off
79 |
80 | # Database configuration
81 | # ~~~~~
82 | # Enable a database engine if needed.
83 | #
84 | # To quickly set up a development database, use either:
85 | # - mem : for a transient in memory database (H2 in memory)
86 | # - fs : for a simple file written database (H2 file stored)
87 | # db=mem
88 | #
89 | # To connect to a local MySQL5 database, use:
90 | # db=mysql://user:pwd@host/database
91 | #
92 | # To connect to a local PostgreSQL9 database, use:
93 | # db=postgres://user:pwd@host/database
94 | #
95 | # If you need a full JDBC configuration use the following :
96 | # db.url=jdbc:postgresql:database_name
97 | # db.driver=org.postgresql.Driver
98 | # db.user=root
99 | # db.pass=secret
100 | #
101 | # Connections pool configuration :
102 | # db.pool.timeout=1000
103 | # db.pool.maxSize=30
104 | # db.pool.minSize=10
105 | #
106 | # If you want to reuse an existing Datasource from your application server, use:
107 | # db=java:/comp/env/jdbc/myDatasource
108 | #
109 | # When using an existing Datasource, it's sometimes needed to destroy it when
110 | # the application is stopped. Depending on the datasource, you can define a
111 | # generic "destroy" method :
112 | # db.destroyMethod=close
113 |
114 | # JPA Configuration (Hibernate)
115 | # ~~~~~
116 | #
117 | # Specify the custom JPA dialect to use here (default to guess):
118 | # jpa.dialect=org.hibernate.dialect.PostgreSQLDialect
119 | #
120 | # Specify the ddl generation pattern to use. Set to none to disable it
121 | # (default to update in DEV mode, and none in PROD mode):
122 | # jpa.ddl=update
123 | #
124 | # Debug SQL statements (logged using DEBUG level):
125 | # jpa.debugSQL=true
126 | #
127 | # You can even specify additional hibernate properties here:
128 | # hibernate.use_sql_comments=true
129 | # ...
130 | #
131 | # Store path for Blob content
132 | attachments.path=data/attachments
133 |
134 | # Memcached configuration
135 | # ~~~~~
136 | # Enable memcached if needed. Otherwise a local cache is used.
137 | # memcached=enabled
138 | #
139 | # Specify memcached host (default to 127.0.0.1:11211)
140 | # memcached.host=127.0.0.1:11211
141 | #
142 | # Or you can specify multiple host to build a distributed cache
143 | # memcached.1.host=127.0.0.1:11211
144 | # memcached.2.host=127.0.0.1:11212
145 | #
146 | # Use plain SASL to authenticate for memcached
147 | # memcached.user=
148 | # memcached.password=
149 |
150 | # HTTP Response headers control for static files
151 | # ~~~~~
152 | # Set the default max-age, telling the user's browser how long it should cache the page.
153 | # Default is 3600 (one hour). Set it to 0 to send no-cache.
154 | # This is only read in prod mode, in dev mode the cache is disabled.
155 | # http.cacheControl=3600
156 |
157 | # If enabled, Play will generate entity tags automatically and send a 304 when needed.
158 | # Default is true, set it to false to deactivate use of entity tags.
159 | # http.useETag=true
160 |
161 | # Custom mime types
162 | # mimetype.xpi=application/x-xpinstall
163 |
164 | # WS configuration
165 | # ~~~~~
166 | # Default engine is Async Http Client, uncomment to use
167 | # the JDK's internal implementation
168 | # webservice = urlfetch
169 | # If you need to set proxy params for WS requests
170 | # http.proxyHost = localhost
171 | # http.proxyPort = 3128
172 | # http.proxyUser = jojo
173 | # http.proxyPassword = jojo
174 |
175 | # Mail configuration
176 | # ~~~~~
177 | # Default is to use a mock Mailer
178 | mail.smtp=mock
179 |
180 | # Or, specify mail host configuration
181 | # mail.smtp.host=127.0.0.1
182 | # mail.smtp.user=admin
183 | # mail.smtp.pass=
184 | # mail.smtp.channel=ssl
185 |
186 | # Url-resolving in Jobs
187 | # ~~~~~~
188 | # When rendering templates with reverse-url-resoling (@@{..}) in Jobs (which do not have an inbound Http.Request),
189 | # ie if sending a HtmlMail, Play need to know which url your users use when accessing your app.
190 | # %test.application.baseUrl=http://localhost:9000/
191 | # %prod.application.baseUrl=http://www.yourdomain.com/
192 |
193 | # Jobs executor
194 | # ~~~~~~
195 | # Size of the Jobs pool
196 | # play.jobs.pool=10
197 |
198 | # Execution pool
199 | # ~~~~~
200 | # Default to 1 thread in DEV mode or (nb processors + 1) threads in PROD mode.
201 | # Try to keep a low as possible. 1 thread will serialize all requests (very useful for debugging purpose)
202 | # play.pool=3
203 |
204 | # Open file from errors pages
205 | # ~~~~~
206 | # If your text editor supports opening files by URL, Play! will
207 | # dynamically link error pages to files
208 | #
209 | # Example, for textmate:
210 | # play.editor=txmt://open?url=file://%s&line=%s
211 |
212 | # Testing. Set up a custom configuration for test mode
213 | # ~~~~~
214 | #%test.module.cobertura=${play.path}/modules/cobertura
215 | %test.application.mode=dev
216 | %test.db.url=jdbc:h2:mem:play;MODE=MYSQL;LOCK_MODE=0
217 | %test.jpa.ddl=create
218 | %test.mail.smtp=mock
219 |
220 | pusher.appId=foo
221 | pusher.key=0987654321
222 | pusher.secret=10100110111
223 |
224 |
--------------------------------------------------------------------------------
/app/controllers/pushplay/PushPlayWebSocket.java:
--------------------------------------------------------------------------------
1 | package controllers.pushplay;
2 |
3 | import static play.libs.F.Matcher.ClassOf;
4 |
5 | import java.io.BufferedReader;
6 | import java.io.IOException;
7 | import java.io.InputStreamReader;
8 | import java.net.URL;
9 | import java.util.*;
10 |
11 | import com.google.gson.JsonObject;
12 | import org.apache.commons.io.IOUtils;
13 |
14 | import play.Logger;
15 | import play.Play;
16 | import play.PlayPlugin;
17 | import play.libs.F;
18 | import play.libs.F.Either;
19 | import play.libs.F.Promise;
20 | import play.modules.pushplay.*;
21 | import play.mvc.Controller;
22 | import play.mvc.Http;
23 |
24 | import static play.libs.F.Matcher.String;
25 | import static play.mvc.Http.WebSocketEvent.*;
26 | import play.mvc.WebSocketController;
27 |
28 | import com.google.gson.Gson;
29 | import com.google.gson.JsonParser;
30 | import com.google.gson.reflect.TypeToken;
31 |
32 | public class PushPlayWebSocket extends Controller {
33 |
34 | //
35 | //private static Map> presence = PushPlayPlugin.hazel.getMap("presence");
36 | // This is for http://pusher.com/docs/rest_api
37 | public static void apps(String appId, String channel) throws IOException {
38 |
39 | TriggerEventMessage tem = new TriggerEventMessage(params);
40 | String message = IOUtils.toString(request.body, "UTF-8");
41 |
42 | // first, make sure our request sha256'd locally match the signature
43 | if (!PushPlayUtil.isRequestValid(request, tem)) {
44 | badRequest();
45 | }
46 |
47 | // make sure md5s match
48 | if (!PushPlayUtil.isMD5Valid(message, tem)) {
49 | badRequest();
50 | }
51 |
52 | // not older than 10 minutes
53 | if ((System.currentTimeMillis() / 1000) - Long.parseLong(tem.auth_timestamp) > 600) {
54 | badRequest();
55 | }
56 |
57 | // do the damn thing
58 | // need to convert message, which is json key/value pairs to map, this map needs to get passed directly to
59 | // the pusher client via data key. Ugghh.
60 | Map map = new Gson().fromJson(message, new TypeToken>() {}.getType());
61 | map.put("socket_id", tem.socket_id);
62 | // TODO fix this
63 | //PushPlayPlugin.publishMessage(new Message(channel, tem.name, map, tem.socket_id));
64 |
65 | response.status=Http.StatusCode.ACCEPTED;
66 | renderText("");
67 | }
68 |
69 | public static class StreamSocket extends WebSocketController {
70 |
71 | //
72 | private static Map> presence = PushPlayPlugin.hazel.getMap("presence");
73 |
74 | /**
75 | * Subscribe
76 | */
77 | public static void app(String apiKey) {
78 |
79 | Logger.info("Got connection api key=[%s]", apiKey);
80 | Map subscriptions = new HashMap();
81 | Message outgoing = new Message();
82 | final String socket_id = UUID.randomUUID().toString();
83 |
84 | if (outbound.isOpen()) {
85 | PushPlayPlugin.streams.put(socket_id, new F.EventStream());
86 | Logger.info("Connection established, socket id=[%s]", socket_id);
87 | outgoing.setEvent("pusher:connection_established");
88 | outgoing.setData(new Payload(socket_id));
89 |
90 | outbound.sendJson(outgoing);
91 | }
92 | else {
93 | Logger.error("ws outbound not open %s");
94 | }
95 |
96 | while (inbound.isOpen()) {
97 |
98 | try {
99 | Either e = await(Promise.waitEither(inbound.nextEvent(), PushPlayPlugin.streams.get(socket_id).nextEvent()));
100 |
101 | for(Http.WebSocketClose closed: SocketClosed.match(e._1)) {
102 | break;
103 | }
104 |
105 | for (String txt : Http.WebSocketEvent.TextFrame.match(e._1)) {
106 | Logger.info("socket_id - %s Incoming Message - %s", socket_id, txt);
107 | Message incoming = new Gson().fromJson(txt, Message.class);
108 |
109 | outgoing.clear();
110 | String channel = incoming.getChannel();
111 | if (channel == null) {
112 | channel = incoming.getData().channel;
113 | }
114 |
115 | if (channel == null) {
116 | Logger.error("No channel provided in message %s", txt);
117 | continue;
118 | }
119 | outgoing.setChannel(channel);
120 |
121 | if (incoming.getEvent().equals("pusher:subscribe")) {
122 | Logger.info("Subscribing to %s", channel);
123 | String auth = incoming.getData().auth;
124 | if (auth != null && !PushPlayUtil.authToken(socket_id, channel, incoming.getData().channel_data.toJSON())
125 | .equals(auth.split(":")[1])) {
126 | // private/presence channel failure
127 | outgoing.setEvent("pusher:error");
128 | } else {
129 | outgoing.setEvent("pusher_internal:subscription_succeeded");
130 | subscriptions.put(channel, null);
131 | Logger.info("Succeeded %s", channel);
132 | if (channel.startsWith("presence-")) {
133 | List members;
134 | if (!presence.containsKey(channel)) {
135 | presence.put(channel, new ArrayList());
136 | }
137 | ChannelData channelData = incoming.getData().channel_data;
138 | // TODO use hazelcast distributed lock here, or eventually a db
139 | members = presence.get(channel);
140 |
141 | // TODO
142 | // check if members already contains this user before sending member_add
143 | // members.put(incoming.getData().get("channel_data").getUserId(), incoming.getData().get("channel_data").getUserInfo());
144 | outgoing.setData(new Payload());
145 | outgoing.getData().updatePresence(members);
146 | // TODO if this is presence channel, need to send a member add message - http://pusher.com/docs/client_api_guide/client_presence_events
147 | if (!members.contains(channelData.toJSON())) {
148 |
149 | Logger.info("new member %s", channelData.toJSON() );
150 | Message memberAdded = new Message();
151 | memberAdded.setChannel(channel);
152 | memberAdded.setEvent("pusher_internal:member_added");
153 | Payload p = new Payload();
154 | p.user_id = channelData.user_id;
155 | p.user_info = channelData.user_info;
156 | memberAdded.setData(p);
157 | PushPlayPlugin.publishMessage(memberAdded);
158 | }
159 | subscriptions.put(channel, channelData.toJSON());
160 | members.add(channelData.toJSON());
161 | outgoing.setData(new Payload());
162 | outgoing.getData().updatePresence(members);
163 |
164 | presence.put(channel, members);
165 | }
166 | Logger.info("socket_id - %s Publishing message to subscribers %s", socket_id, new Gson().toJson(outgoing));
167 |
168 | outbound.sendJson(outgoing);
169 | outgoing.setEvent(null);
170 | }
171 | }
172 | else if (incoming.getEvent().equals("pusher:unsubscribe")) {
173 | // TODO if this is presence channel, need to send a member remove message - http://pusher.com/docs/client_api_guide/client_presence_events
174 | // careful sending a member_removed message if this channel has multiple connections...
175 | subscriptions.remove(channel);
176 | /* if (channel.startsWith("presence-")) {
177 | List members;
178 | if (!presence.containsKey(channel)) {
179 | Logger.error("Presence channel record not found for [%s]", channel);
180 | }
181 | String channelData = subscriptions.get(channel);
182 | // TODO use hazelcast distributed lock here, or eventually a db
183 | members = presence.get(channel);
184 | members.remove(channelData);
185 | presence.put(channel, members);
186 | if (!members.contains(channelData)) {
187 | Logger.info("removing member %s", channelData );
188 | Message memberRemoved = new Message();
189 | memberRemoved.setChannel(channel);
190 | memberRemoved.setEvent("pusher_internal:member_removed");
191 | Payload p = new Payload();
192 | p.user_id = new Gson().fromJson(channelData, ChannelData.class).user_id;
193 | memberRemoved.setData(p);
194 |
195 | PushPlayPlugin.publishMessage(memberRemoved);
196 | }
197 | }*/
198 | }
199 | else if (incoming.getEvent().startsWith("client-")) {
200 | // dear lord...
201 | JsonObject msg = new JsonParser().parse(txt).getAsJsonObject();
202 | // can only trigger events if this user is subscribed to the channel
203 | // and channel must be presence or private
204 | if (subscriptions.keySet().contains(channel) && isAuthenticated(channel)) {
205 | // TODO handle client- events - http://pusher.com/docs/client_api_guide/client_events#trigger-events
206 | //outgoing = incoming;
207 | PushPlayPlugin.publishMessage(txt);
208 | }
209 | }
210 | else {
211 | Logger.warn("Unrecognized event [%s]", incoming.getEvent());
212 |
213 | }
214 |
215 | if (outgoing.getEvent() != null) {
216 | PushPlayPlugin.publishMessage(outgoing);
217 | }
218 | }
219 |
220 | for (String message : ClassOf(String.class).match(e._2)) {
221 | Logger.info("socket_id - %s handling String %s", socket_id, message);
222 | // we only need the channel and event, and it will always serialize as a message, but we send the string
223 | Message m = new Gson().fromJson(message, Message.class);
224 | if (!isClientTriggered(m) && socket_id.equals(m.getData().socket_id)) {
225 | Logger.info("Filtering out message " + message);
226 | continue;
227 | }
228 | // null it out so it doesn't get sent.
229 | //message.getData().socket_id = null;
230 | // only send messages if we are subscribed to it.
231 | if (subscriptions.keySet().contains(m.getChannel())) {
232 | //outbound.send(message.toString());
233 | Logger.info("socket_id - %s Publishing message to subscribers %s", socket_id, message);
234 | outbound.send(message);
235 | } else {
236 | Logger.info("socket_id - %s isn't subscribed to this channel - %s", socket_id, m.getChannel());
237 | }
238 | }
239 | for (Message message : ClassOf(Message.class).match(e._2)) {
240 | Logger.info("BAAAAAD socket_id - %s handling message", socket_id);
241 | // if the socket created the message, don't push it out, unless its a client trigger
242 | // Do we really want this? Only other way to trigger events is from REST API, which indicates it should be broadcast to
243 | // all sockets...
244 | if (!isClientTriggered(message) && socket_id.equals(message.getData().socket_id)) {
245 | Logger.info("Filtering out message " + new Gson().toJson(message));
246 | continue;
247 | }
248 | // null it out so it doesn't get sent.
249 | message.getData().socket_id = null;
250 | // only send messages if we are subscribed to it.
251 | if (subscriptions.keySet().contains(message.getChannel())) {
252 | //outbound.send(message.toString());
253 | Logger.info("socket_id - %s Publishing message to subscribers %s", socket_id, new Gson().toJson(message));
254 | outbound.sendJson(message);
255 | } else {
256 | Logger.info("socket_id - %s isn't subscribed to this channel - %s", socket_id, message.getChannel());
257 | }
258 | }
259 | } catch (Throwable t) {
260 | Logger.error("Caught error %s %s", t.getMessage(), t.toString());
261 | }
262 | }
263 | Logger.debug("Reaping EventStream for socket_id=[%s]", socket_id);
264 | PushPlayPlugin.streams.remove(socket_id);
265 |
266 | // TODO Loop through subscriptions and notify presence channels that user left
267 |
268 | }
269 |
270 | private static boolean isClientTriggered(Message message) {
271 | return message.getEvent().startsWith("client-");
272 | }
273 |
274 | private static boolean isAuthenticated(String channel) {
275 | return channel.startsWith("private-") || channel.startsWith("presence-");
276 | }
277 | }
278 | }
279 |
--------------------------------------------------------------------------------
/public/javascripts/pusher.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Pusher JavaScript Library v1.10.1
3 | * http://pusherapp.com/
4 | *
5 | * Copyright 2011, Pusher
6 | * Released under the MIT licence.
7 | */
8 | if (typeof Function.prototype.scopedTo === "undefined") Function.prototype.scopedTo = function (a, c) {
9 | var b = this;
10 | return function () {
11 | return b.apply(a, Array.prototype.slice.call(c || []).concat(Array.prototype.slice.call(arguments)))
12 | }
13 | };
14 | var Pusher = function (a, c) {
15 | this.options = c || {};
16 | this.path = "/app/" + a + "?client=js&version=" + Pusher.VERSION;
17 | this.key = a;
18 | this.channels = new Pusher.Channels;
19 | this.global_channel = new Pusher.Channel("pusher_global_channel");
20 | this.global_channel.global = !0;
21 | var b = this;
22 | this.connection = new Pusher.Connection(this.key, this.options);
23 | this.connection.bind("connected", function () {
24 | b.subscribeAll()
25 | }).bind("message", function (a) {
26 | b.send_local_event(a.event, a.data, a.channel)
27 | }).bind("disconnected", function () {
28 | b.channels.disconnect()
29 | }).bind("error", function (a) {
30 | Pusher.debug("Error", a)
31 | });
32 | Pusher.instances.push(this);
33 | Pusher.isReady && b.connect()
34 | };
35 | Pusher.instances = [];
36 | Pusher.prototype = {
37 | channel: function (a) {
38 | return this.channels.find(a)
39 | },
40 | connect: function () {
41 | this.connection.connect()
42 | },
43 | disconnect: function () {
44 | this.connection.disconnect()
45 | },
46 | bind: function (a, c) {
47 | this.global_channel.bind(a, c);
48 | return this
49 | },
50 | bind_all: function (a) {
51 | this.global_channel.bind_all(a);
52 | return this
53 | },
54 | subscribeAll: function () {
55 | for (var a in this.channels.channels) this.channels.channels.hasOwnProperty(a) && this.subscribe(a)
56 | },
57 | subscribe: function (a) {
58 | var c = this,
59 | b = this.channels.add(a, this);
60 | this.connection.state === "connected" && b.authorize(this, function (e, f) {
61 | e ? b.emit("pusher:subscription_error", f) : c.send_event("pusher:subscribe", {
62 | channel: a,
63 | auth: f.auth,
64 | channel_data: f.channel_data
65 | })
66 | });
67 | return b
68 | },
69 | unsubscribe: function (a) {
70 | this.channels.remove(a);
71 | this.connection.state === "connected" && this.send_event("pusher:unsubscribe", {
72 | channel: a
73 | })
74 | },
75 | send_event: function (a, c, b) {
76 | Pusher.debug("Event sent (channel,event,data)", b, a, c);
77 | a = {
78 | event: a,
79 | data: c
80 | };
81 | b && (a.channel = b);
82 | return this.connection.send(JSON.stringify(a))
83 | },
84 | send_local_event: function (a, c, b) {
85 | c = Pusher.data_decorator(a, c);
86 | b ? (b = this.channel(b)) && b.dispatch_with_all(a, c) : Pusher.debug("Event recd (event,data)", a, c);
87 | this.global_channel.dispatch_with_all(a, c)
88 | }
89 | };
90 | Pusher.Util = {
91 | extend: function extend(c, b) {
92 | for (var e in b) c[e] = b[e] && b[e].constructor && b[e].constructor === Object ? extend(c[e] || {}, b[e]) : b[e];
93 | return c
94 | }
95 | };
96 | Pusher.debug = function () {
97 | if (Pusher.log) {
98 | for (var a = ["Pusher"], c = 0; c < arguments.length; c++) typeof arguments[c] === "string" ? a.push(arguments[c]) : window.JSON == void 0 ? a.push(arguments[c].toString()) : a.push(JSON.stringify(arguments[c]));
99 | Pusher.log(a.join(" : "))
100 | }
101 | };
102 | Pusher.VERSION = "1.10.1";
103 | Pusher.host = "localhost";
104 | Pusher.ws_port = 9000;
105 | Pusher.wss_port = 443;
106 | Pusher.channel_auth_endpoint = "/pusher/auth";
107 | Pusher.connection_timeout = 5E3;
108 | Pusher.cdn_http = "http://js.pusherapp.com/";
109 | Pusher.cdn_https = "https://d3ds63zw57jt09.cloudfront.net/";
110 | Pusher.dependency_suffix = ".min";
111 | Pusher.data_decorator = function (a, c) {
112 | return c
113 | };
114 | Pusher.allow_reconnect = !0;
115 | Pusher.channel_auth_transport = "ajax";
116 | Pusher.isReady = !1;
117 | Pusher.ready = function () {
118 | Pusher.isReady = !0;
119 | for (var a = 0, c = Pusher.instances.length; a < c; a++) Pusher.instances[a].connect()
120 | };
121 | (function () {
122 | function a() {
123 | this.callbacks = {};
124 | this.global_callbacks = []
125 | }
126 | a.prototype.bind = function (a, b) {
127 | this.callbacks[a] = this.callbacks[a] || [];
128 | this.callbacks[a].push(b);
129 | return this
130 | };
131 | a.prototype.emit = function (a, b) {
132 | this.dispatch_global_callbacks(a, b);
133 | this.dispatch(a, b);
134 | return this
135 | };
136 | a.prototype.bind_all = function (a) {
137 | this.global_callbacks.push(a);
138 | return this
139 | };
140 | a.prototype.dispatch = function (a, b) {
141 | var e = this.callbacks[a];
142 | if (e) for (var f = 0; f < e.length; f++) e[f](b);
143 | else!this.global && !(this instanceof Pusher.Connection || this instanceof Pusher.Machine) && Pusher.debug("No callbacks for " + a, b)
144 | };
145 | a.prototype.dispatch_global_callbacks = function (a, b) {
146 | for (var e = 0; e < this.global_callbacks.length; e++) this.global_callbacks[e](a, b)
147 | };
148 | a.prototype.dispatch_with_all = function (a, b) {
149 | this.dispatch(a, b);
150 | this.dispatch_global_callbacks(a, b)
151 | };
152 | this.Pusher.EventsDispatcher = a
153 | }).call(this);
154 | (function () {
155 | function a(a, b) {
156 | if (a == null) return -1;
157 | if (f && a.indexOf === f) return a.indexOf(b);
158 | for (i = 0, l = a.length; i < l; i++) if (a[i] === b) return i;
159 | return -1
160 | }
161 | function c(a, b, c) {
162 | if (b[a] !== void 0) b[a](c)
163 | }
164 | function b(a, b, c, f) {
165 | e.EventsDispatcher.call(this);
166 | this.actor = a;
167 | this.state = void 0;
168 | this.errors = [];
169 | this.stateActions = f;
170 | this.transitions = c;
171 | this.transition(b)
172 | }
173 | var e = this.Pusher,
174 | f = Array.prototype.indexOf;
175 | b.prototype.transition = function (b, e) {
176 | var f = this.state,
177 | h = this.stateActions;
178 | if (f && a(this.transitions[f], b) == -1) throw Error(this.actor.key + ": Invalid transition [" + f + " to " + b + "]");
179 | c(f + "Exit", h, e);
180 | c(f + "To" + (b.substr(0, 1).toUpperCase() + b.substr(1)), h, e);
181 | c(b + "Pre", h, e);
182 | this.state = b;
183 | this.emit("state_change", {
184 | oldState: f,
185 | newState: b
186 | });
187 | c(b + "Post", h, e)
188 | };
189 | b.prototype.is = function (a) {
190 | return this.state === a
191 | };
192 | b.prototype.isNot = function (a) {
193 | return this.state !== a
194 | };
195 | e.Util.extend(b.prototype, e.EventsDispatcher.prototype);
196 | this.Pusher.Machine = b
197 | }).call(this);
198 | (function () {
199 | function a(a) {
200 | a.connectionWait = 0;
201 | a.openTimeout = b.TransportType === "flash" ? 5E3 : 2E3;
202 | a.connectedTimeout = 2E3;
203 | a.connectionSecure = a.compulsorySecure;
204 | a.connectionAttempts = 0
205 | }
206 | function c(c, e) {
207 | function m() {
208 | d.connectionWait < h && (d.connectionWait += k);
209 | d.openTimeout < s && (d.openTimeout += g);
210 | d.connectedTimeout < t && (d.connectedTimeout += p);
211 | if (d.compulsorySecure !== !0) d.connectionSecure = !d.connectionSecure;
212 | d.connectionAttempts++
213 | }
214 | function q() {
215 | d._machine.transition("impermanentlyClosing")
216 | }
217 | function u() {
218 | d._machine.transition("open")
219 | }
220 |
221 | function r(a) {
222 | var c;
223 | a: {
224 | try {
225 | var e = JSON.parse(a.data);
226 | if (typeof e.data === "string") try {
227 | e.data = JSON.parse(e.data)
228 | } catch (f) {
229 | if (!(f instanceof SyntaxError)) throw f;
230 | }
231 | c = e;
232 | break a
233 | } catch (h) {
234 | d.emit("error", {
235 | type: "MessageParseError",
236 | error: h,
237 | data: a.data
238 | })
239 | }
240 | c = void 0
241 | }
242 | if (typeof c !== "undefined") if (b.debug("Event recd (event,data)", c.event, c.data), c.event === "pusher:connection_established") d._machine.transition("connected", c.data.socket_id);
243 | else if (c.event === "pusher:error") {
244 | if (d.emit("error", {
245 | type: "PusherError",
246 | data: c.data
247 | }), c.data.code === 4001 && d._machine.transition("permanentlyClosing"), c.data.code === 4E3) b.debug(c.data.message), d.compulsorySecure = !0, d.connectionSecure = !0, d.options.encrypted = !0
248 | } else c.event !== "pusher:heartbeat" && d._machine.is("connected") && d.emit("message", c)
249 | }
250 | function n() {
251 | d._machine.transition("waiting")
252 | }
253 | function o() {
254 | d.emit("error", {
255 | type: "WebSocketError"
256 | });
257 | d.socket.close();
258 | d._machine.transition("impermanentlyClosing")
259 | }
260 | function j(a, c) {
261 | if (d.state !== a) {
262 | var e = d.state;
263 | d.state = a;
264 | b.debug("State changed", e + " -> " + a);
265 | d.emit("state_change", {
266 | previous: e,
267 | current: a
268 | });
269 | d.emit(a, c)
270 | }
271 | }
272 | var d = this;
273 | b.EventsDispatcher.call(this);
274 | this.options = b.Util.extend({
275 | encrypted: !1
276 | }, e || {});
277 | this.netInfo = new b.NetInfo;
278 | this.netInfo.bind("online", function () {
279 | d._machine.is("waiting") && (d._machine.transition("connecting"), j("connecting"))
280 | });
281 | this.netInfo.bind("offline", function () {
282 | if (d._machine.is("connected")) d.socket.onclose = void 0, d.socket.onmessage = void 0, d.socket.onerror = void 0, d.socket.onopen = void 0, d.socket.close(), d.socket = void 0, d._machine.transition("waiting")
283 | });
284 | this._machine = new b.Machine(d, "initialized", f, {
285 | initializedPre: function () {
286 | d.compulsorySecure = d.options.encrypted;
287 | d.key = c;
288 | d.socket = null;
289 | d.socket_id = null;
290 | d.state = "initialized"
291 | },
292 | waitingPre: function () {
293 | d.connectionWait > 0 && d.emit("connecting_in", d.connectionWait);
294 | d.netInfo.isOnLine() === !1 || d.connectionAttempts > 4 ? j("unavailable") : j("connecting");
295 | if (d.netInfo.isOnLine() === !0) d._waitingTimer = setTimeout(function () {
296 | d._machine.transition("connecting")
297 | }, d.connectionWait)
298 | },
299 | waitingExit: function () {
300 | clearTimeout(d._waitingTimer)
301 | },
302 | connectingPre: function () {
303 | if (d.netInfo.isOnLine() === !1) d._machine.transition("waiting"), j("unavailable");
304 | else {
305 | var a;
306 | a = b.ws_port;
307 | var c = "ws://";
308 | if (d.connectionSecure || document.location.protocol === "https:") a = b.wss_port, c = "wss://";
309 | a = c + b.host + ":" + a + "/app/" + d.key + "?client=js&version=" + b.VERSION;
310 |
311 | b.debug("Connecting", a);
312 | d.socket = new b.Transport(a);
313 | d.socket.onopen = u;
314 | d.socket.onclose = n;
315 | d.socket.onerror = o;
316 | d._connectingTimer = setTimeout(q, d.openTimeout)
317 | }
318 | },
319 | connectingExit: function () {
320 | clearTimeout(d._connectingTimer)
321 | },
322 | connectingToWaiting: function () {
323 | m()
324 | },
325 | connectingToImpermanentlyClosing: function () {
326 | m()
327 | },
328 | openPre: function () {
329 | d.socket.onmessage = r;
330 | d.socket.onerror = o;
331 | d.socket.onclose = n;
332 | d._openTimer = setTimeout(q, d.connectedTimeout)
333 | },
334 | openExit: function () {
335 | clearTimeout(d._openTimer)
336 | },
337 | openToWaiting: function () {
338 | m()
339 | },
340 | openToImpermanentlyClosing: function () {
341 | m()
342 | },
343 | connectedPre: function (b) {
344 | d.socket_id = b;
345 | d.socket.onmessage = r;
346 | d.socket.onerror = o;
347 | d.socket.onclose = n;
348 | a(d)
349 | },
350 | connectedPost: function () {
351 | j("connected")
352 | },
353 | connectedExit: function () {
354 | j("disconnected")
355 | },
356 | impermanentlyClosingPost: function () {
357 | if (d.socket) d.socket.onclose = n, d.socket.close()
358 | },
359 | permanentlyClosingPost: function () {
360 | d.socket ? (d.socket.onclose = function () {
361 | a(d);
362 | d._machine.transition("permanentlyClosed")
363 | }, d.socket.close()) : (a(d), d._machine.transition("permanentlyClosed"))
364 | },
365 | failedPre: function () {
366 | j("failed");
367 | b.debug("WebSockets are not available in this browser.")
368 | }
369 | })
370 | }
371 | var b = this.Pusher,
372 | e = function () {
373 | var a = this;
374 | b.EventsDispatcher.call(this);
375 | window.addEventListener !== void 0 && (window.addEventListener("online", function () {
376 | a.emit("online", null)
377 | }, !1), window.addEventListener("offline", function () {
378 | a.emit("offline", null)
379 | }, !1))
380 | };
381 | e.prototype.isOnLine = function () {
382 | return window.navigator.onLine === void 0 ? !0 : window.navigator.onLine
383 | };
384 | b.Util.extend(e.prototype, b.EventsDispatcher.prototype);
385 | this.Pusher.NetInfo = b.NetInfo = e;
386 | var f = {
387 | initialized: ["waiting", "failed"],
388 | waiting: ["connecting", "permanentlyClosed"],
389 | connecting: ["open", "permanentlyClosing", "impermanentlyClosing", "waiting"],
390 | open: ["connected", "permanentlyClosing", "impermanentlyClosing", "waiting"],
391 | connected: ["permanentlyClosing", "impermanentlyClosing", "waiting"],
392 | impermanentlyClosing: ["waiting", "permanentlyClosing"],
393 | permanentlyClosing: ["permanentlyClosed"],
394 | permanentlyClosed: ["waiting"],
395 | failed: ["permanentlyClosing"]
396 | },
397 | k = 2E3,
398 | g = 2E3,
399 | p = 2E3,
400 | h = 5 * k,
401 | s = 5 * g,
402 | t = 5 * p;
403 | c.prototype.connect = function () {
404 | b.Transport === null || typeof b.Transport === "undefined" ? this._machine.transition("failed") : this._machine.is("initialized") ? (a(this), this._machine.transition("waiting")) : this._machine.is("waiting") && this.netInfo.isOnLine() === !0 ? this._machine.transition("connecting") : this._machine.is("permanentlyClosed") && this._machine.transition("waiting")
405 | };
406 | c.prototype.send = function (a) {
407 | return this._machine.is("connected") ? (this.socket.send(a), !0) : !1
408 | };
409 | c.prototype.disconnect = function () {
410 | this._machine.is("permanentlyClosed") || (b.debug("Disconnecting"), this._machine.is("waiting") ? this._machine.transition("permanentlyClosed") : this._machine.transition("permanentlyClosing"))
411 | };
412 | b.Util.extend(c.prototype, b.EventsDispatcher.prototype);
413 | this.Pusher.Connection = c
414 | }).call(this);
415 | Pusher.Channels = function () {
416 | this.channels = {}
417 | };
418 | Pusher.Channels.prototype = {
419 | add: function (a, c) {
420 | var b = this.find(a);
421 | b || (b = Pusher.Channel.factory(a, c), this.channels[a] = b);
422 | return b
423 | },
424 | find: function (a) {
425 | return this.channels[a]
426 | },
427 | remove: function (a) {
428 | delete this.channels[a]
429 | },
430 | disconnect: function () {
431 | for (var a in this.channels) this.channels[a].disconnect()
432 | }
433 | };
434 | Pusher.Channel = function (a, c) {
435 | var b = this;
436 | Pusher.EventsDispatcher.call(this);
437 | this.pusher = c;
438 | this.name = a;
439 | this.subscribed = !1;
440 | this.bind("pusher_internal:subscription_succeeded", function (a) {
441 | b.acknowledge_subscription(a)
442 | })
443 | };
444 | Pusher.Channel.prototype = {
445 | init: function () {},
446 | disconnect: function () {},
447 | acknowledge_subscription: function () {
448 | this.subscribed = !0;
449 | this.dispatch_with_all("pusher:subscription_succeeded")
450 | },
451 | is_private: function () {
452 | return !1
453 | },
454 | is_presence: function () {
455 | return !1
456 | },
457 | authorize: function (a, c) {
458 | c(!1, {})
459 | },
460 | trigger: function (a, c) {
461 | return this.pusher.send_event(a, c, this.name)
462 | }
463 | };
464 | Pusher.Util.extend(Pusher.Channel.prototype, Pusher.EventsDispatcher.prototype);
465 | Pusher.auth_callbacks = {};
466 | Pusher.authorizers = {
467 | ajax: function (a, c) {
468 | var b;
469 | b = Pusher.XHR ? new Pusher.XHR : window.XMLHttpRequest ? new window.XMLHttpRequest : new ActiveXObject("Microsoft.XMLHTTP");
470 | b.open("POST", Pusher.channel_auth_endpoint, !0);
471 | b.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
472 | b.onreadystatechange = function () {
473 | if (b.readyState == 4) if (b.status == 200) {
474 | var a, f = !1;
475 | try {
476 | a = JSON.parse(b.responseText), f = !0
477 | } catch (k) {
478 | c(!0, "JSON returned from webapp was invalid, yet status code was 200. Data was: " + b.responseText)
479 | }
480 | f && c(!1, a)
481 | } else Pusher.debug("Couldn't get auth info from your webapp", status), c(!0, b.status)
482 | };
483 | b.send("socket_id=" + encodeURIComponent(a.connection.socket_id) + "&channel_name=" + encodeURIComponent(this.name))
484 | },
485 | jsonp: function (a, c) {
486 | var b = "socket_id=" + encodeURIComponent(a.connection.socket_id) + "&channel_name=" + encodeURIComponent(this.name),
487 | e = document.createElement("script");
488 | Pusher.auth_callbacks[this.name] = function (a) {
489 | c(!1, a)
490 | };
491 | e.src = Pusher.channel_auth_endpoint + "?callback=" + encodeURIComponent("Pusher.auth_callbacks['" + this.name + "']") + "&" + b;
492 | b = document.getElementsByTagName("head")[0] || document.documentElement;
493 | b.insertBefore(e, b.firstChild)
494 | }
495 | };
496 | Pusher.Channel.PrivateChannel = {
497 | is_private: function () {
498 | return !0
499 | },
500 | authorize: function (a, c) {
501 | Pusher.authorizers[Pusher.channel_auth_transport].scopedTo(this)(a, c)
502 | }
503 | };
504 | Pusher.Channel.PresenceChannel = {
505 | init: function () {
506 | this.bind("pusher_internal:member_added", function (a) {
507 | this.dispatch_with_all("pusher:member_added", this.members.add(a.user_id, a.user_info))
508 | }.scopedTo(this));
509 | this.bind("pusher_internal:member_removed", function (a) {
510 | (a = this.members.remove(a.user_id)) && this.dispatch_with_all("pusher:member_removed", a)
511 | }.scopedTo(this))
512 | },
513 | disconnect: function () {
514 | this.members.clear()
515 | },
516 | acknowledge_subscription: function (a) {
517 | this.members._members_map = a.presence.hash;
518 | this.members.count = a.presence.count;
519 | this.subscribed = !0;
520 | this.dispatch_with_all("pusher:subscription_succeeded", this.members)
521 | },
522 | is_presence: function () {
523 | return !0
524 | },
525 | members: {
526 | _members_map: {},
527 | count: 0,
528 | each: function (a) {
529 | for (var c in this._members_map) a({
530 | id: c,
531 | info: this._members_map[c]
532 | })
533 | },
534 | add: function (a, c) {
535 | this._members_map[a] = c;
536 | this.count++;
537 | return this.get(a)
538 | },
539 | remove: function (a) {
540 | var c = this.get(a);
541 | c && (delete this._members_map[a], this.count--);
542 | return c
543 | },
544 | get: function (a) {
545 | return this._members_map.hasOwnProperty(a) ? {
546 | id: a,
547 | info: this._members_map[a]
548 | } : null
549 | },
550 | clear: function () {
551 | this._members_map = {};
552 | this.count = 0
553 | }
554 | }
555 | };
556 | Pusher.Channel.factory = function (a, c) {
557 | var b = new Pusher.Channel(a, c);
558 | a.indexOf(Pusher.Channel.private_prefix) === 0 ? Pusher.Util.extend(b, Pusher.Channel.PrivateChannel) : a.indexOf(Pusher.Channel.presence_prefix) === 0 && (Pusher.Util.extend(b, Pusher.Channel.PrivateChannel), Pusher.Util.extend(b, Pusher.Channel.PresenceChannel));
559 | b.init();
560 | return b
561 | };
562 | Pusher.Channel.private_prefix = "private-";
563 | Pusher.Channel.presence_prefix = "presence-";
564 | var _require = function () {
565 | var a;
566 | a = document.addEventListener ?
567 | function (a, b) {
568 | a.addEventListener("load", b, !1)
569 | } : function (a, b) {
570 | a.attachEvent("onreadystatechange", function () {
571 | (a.readyState == "loaded" || a.readyState == "complete") && b()
572 | })
573 | };
574 | return function (c, b) {
575 | function e(b, c) {
576 | var c = c ||
577 | function () {}, e = document.getElementsByTagName("head")[0], g = document.createElement("script");
578 | g.setAttribute("src", b);
579 | g.setAttribute("type", "text/javascript");
580 | g.setAttribute("async", !0);
581 | a(g, function () {
582 | var a = c;
583 | f++;
584 | k == f && setTimeout(a, 0)
585 | });
586 | e.appendChild(g)
587 | }
588 | for (var f = 0, k = c.length, g = 0; g < k; g++) e(c[g], b)
589 | }
590 | }();
591 | (function () {
592 | var a = (document.location.protocol == "http:" ? Pusher.cdn_http : Pusher.cdn_https) + Pusher.VERSION,
593 | c = [];
594 | typeof window.JSON === "undefined" && c.push(a + "/json2" + Pusher.dependency_suffix + ".js");
595 | if (typeof window.WebSocket === "undefined" && typeof window.MozWebSocket === "undefined") window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = !0, c.push(a + "/flashfallback" + Pusher.dependency_suffix + ".js");
596 | var b = function () {
597 | return typeof window.WebSocket === "undefined" && typeof window.MozWebSocket === "undefined" ?
598 | function () {
599 | typeof window.WebSocket !== "undefined" && typeof window.MozWebSocket === "undefined" ? (Pusher.Transport = window.WebSocket, Pusher.TransportType = "flash", window.WEB_SOCKET_SWF_LOCATION = a + "/WebSocketMain.swf", WebSocket.__addTask(function () {
600 | Pusher.ready()
601 | }), WebSocket.__initialize()) : (Pusher.Transport = null, Pusher.TransportType = "none", Pusher.ready())
602 | } : function () {
603 | Pusher.Transport = typeof window.MozWebSocket !== "undefined" ? window.MozWebSocket : window.WebSocket;
604 | Pusher.TransportType = "native";
605 | Pusher.ready()
606 | }
607 | }(),
608 | e = function (a) {
609 | var b = function () {
610 | document.body ? a() : setTimeout(b, 0)
611 | };
612 | b()
613 | },
614 | f = function () {
615 | e(b)
616 | };
617 | c.length > 0 ? _require(c, f) : f()
618 | })();
--------------------------------------------------------------------------------
/samples-and-tests/pushplay-app/public/javascripts/jquery-1.6.4.min.js:
--------------------------------------------------------------------------------
1 | /*! jQuery v1.6.4 http://jquery.com/ | http://jquery.org/license */
2 | (function(a,b){function cu(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cr(a){if(!cg[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ch||(ch=c.createElement("iframe"),ch.frameBorder=ch.width=ch.height=0),b.appendChild(ch);if(!ci||!ch.createElement)ci=(ch.contentWindow||ch.contentDocument).document,ci.write((c.compatMode==="CSS1Compat"?"":"")+""),ci.close();d=ci.createElement(a),ci.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ch)}cg[a]=e}return cg[a]}function cq(a,b){var c={};f.each(cm.concat.apply([],cm.slice(0,b)),function(){c[this]=a});return c}function cp(){cn=b}function co(){setTimeout(cp,0);return cn=f.now()}function cf(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ce(){try{return new a.XMLHttpRequest}catch(b){}}function b$(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){c!=="border"&&f.each(e,function(){c||(d-=parseFloat(f.css(a,"padding"+this))||0),c==="margin"?d+=parseFloat(f.css(a,c+this))||0:d-=parseFloat(f.css(a,"border"+this+"Width"))||0});return d+"px"}d=bv(a,b,b);if(d<0||d==null)d=a.style[b]||0;d=parseFloat(d)||0,c&&f.each(e,function(){d+=parseFloat(f.css(a,"padding"+this))||0,c!=="padding"&&(d+=parseFloat(f.css(a,"border"+this+"Width"))||0),c==="margin"&&(d+=parseFloat(f.css(a,c+this))||0)});return d+"px"}function bl(a,b){b.src?f.ajax({url:b.src,async:!1,dataType:"script"}):f.globalEval((b.text||b.textContent||b.innerHTML||"").replace(bd,"/*$0*/")),b.parentNode&&b.parentNode.removeChild(b)}function bk(a){f.nodeName(a,"input")?bj(a):"getElementsByTagName"in a&&f.grep(a.getElementsByTagName("input"),bj)}function bj(a){if(a.type==="checkbox"||a.type==="radio")a.defaultChecked=a.checked}function bi(a){return"getElementsByTagName"in a?a.getElementsByTagName("*"):"querySelectorAll"in a?a.querySelectorAll("*"):[]}function bh(a,b){var c;if(b.nodeType===1){b.clearAttributes&&b.clearAttributes(),b.mergeAttributes&&b.mergeAttributes(a),c=b.nodeName.toLowerCase();if(c==="object")b.outerHTML=a.outerHTML;else if(c!=="input"||a.type!=="checkbox"&&a.type!=="radio"){if(c==="option")b.selected=a.defaultSelected;else if(c==="input"||c==="textarea")b.defaultValue=a.defaultValue}else a.checked&&(b.defaultChecked=b.checked=a.checked),b.value!==a.value&&(b.value=a.value);b.removeAttribute(f.expando)}}function bg(a,b){if(b.nodeType===1&&!!f.hasData(a)){var c=f.expando,d=f.data(a),e=f.data(b,d);if(d=d[c]){var g=d.events;e=e[c]=f.extend({},d);if(g){delete e.handle,e.events={};for(var h in g)for(var i=0,j=g[h].length;i=0===c})}function U(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function M(a,b){return(a&&a!=="*"?a+".":"")+b.replace(y,"`").replace(z,"&")}function L(a){var b,c,d,e,g,h,i,j,k,l,m,n,o,p=[],q=[],r=f._data(this,"events");if(!(a.liveFired===this||!r||!r.live||a.target.disabled||a.button&&a.type==="click")){a.namespace&&(n=new RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)")),a.liveFired=this;var s=r.live.slice(0);for(i=0;ic)break;a.currentTarget=e.elem,a.data=e.handleObj.data,a.handleObj=e.handleObj,o=e.handleObj.origHandler.apply(e.elem,arguments);if(o===!1||a.isPropagationStopped()){c=e.level,o===!1&&(b=!1);if(a.isImmediatePropagationStopped())break}}return b}}function J(a,c,d){var e=f.extend({},d[0]);e.type=a,e.originalEvent={},e.liveFired=b,f.event.handle.call(c,e),e.isDefaultPrevented()&&d[0].preventDefault()}function D(){return!0}function C(){return!1}function m(a,c,d){var e=c+"defer",g=c+"queue",h=c+"mark",i=f.data(a,e,b,!0);i&&(d==="queue"||!f.data(a,g,b,!0))&&(d==="mark"||!f.data(a,h,b,!0))&&setTimeout(function(){!f.data(a,g,b,!0)&&!f.data(a,h,b,!0)&&(f.removeData(a,e,!0),i.resolve())},0)}function l(a){for(var b in a)if(b!=="toJSON")return!1;return!0}function k(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(j,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNaN(d)?i.test(d)?f.parseJSON(d):d:parseFloat(d)}catch(g){}f.data(a,c,d)}else d=b}return d}var c=a.document,d=a.navigator,e=a.location,f=function(){function K(){if(!e.isReady){try{c.documentElement.doScroll("left")}catch(a){setTimeout(K,1);return}e.ready()}}var e=function(a,b){return new e.fn.init(a,b,h)},f=a.jQuery,g=a.$,h,i=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/\d/,n=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,o=/^[\],:{}\s]*$/,p=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,q=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,r=/(?:^|:|,)(?:\s*\[)+/g,s=/(webkit)[ \/]([\w.]+)/,t=/(opera)(?:.*version)?[ \/]([\w.]+)/,u=/(msie) ([\w.]+)/,v=/(mozilla)(?:.*? rv:([\w.]+))?/,w=/-([a-z]|[0-9])/ig,x=/^-ms-/,y=function(a,b){return(b+"").toUpperCase()},z=d.userAgent,A,B,C,D=Object.prototype.toString,E=Object.prototype.hasOwnProperty,F=Array.prototype.push,G=Array.prototype.slice,H=String.prototype.trim,I=Array.prototype.indexOf,J={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=n.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.6.4",length:0,size:function(){return this.length},toArray:function(){return G.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?F.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),B.done(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(G.apply(this,arguments),"slice",G.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:F,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;B.resolveWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").unbind("ready")}},bindReady:function(){if(!B){B=e._Deferred();if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",C,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",C),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&K()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a&&typeof a=="object"&&"setInterval"in a},isNaN:function(a){return a==null||!m.test(a)||isNaN(a)},type:function(a){return a==null?String(a):J[D.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!E.call(a,"constructor")&&!E.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||E.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw a},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(o.test(b.replace(p,"@").replace(q,"]").replace(r,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(x,"ms-").replace(w,y)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?h.call(arguments,0):c,--e||g.resolveWith(g,h.call(b,0))}}var b=arguments,c=0,d=b.length,e=d,g=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred();if(d>1){for(;ca ",d=a.getElementsByTagName("*"),e=a.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=a.getElementsByTagName("input")[0],k={leadingWhitespace:a.firstChild.nodeType===3,tbody:!a.getElementsByTagName("tbody").length,htmlSerialize:!!a.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55$/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:a.className!=="t",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0},i.checked=!0,k.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,k.optDisabled=!h.disabled;try{delete a.test}catch(v){k.deleteExpando=!1}!a.addEventListener&&a.attachEvent&&a.fireEvent&&(a.attachEvent("onclick",function(){k.noCloneEvent=!1}),a.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),k.radioValue=i.value==="t",i.setAttribute("checked","checked"),a.appendChild(i),l=c.createDocumentFragment(),l.appendChild(a.firstChild),k.checkClone=l.cloneNode(!0).cloneNode(!0).lastChild.checked,a.innerHTML="",a.style.width=a.style.paddingLeft="1px",m=c.getElementsByTagName("body")[0],o=c.createElement(m?"div":"body"),p={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},m&&f.extend(p,{position:"absolute",left:"-1000px",top:"-1000px"});for(t in p)o.style[t]=p[t];o.appendChild(a),n=m||b,n.insertBefore(o,n.firstChild),k.appendChecked=i.checked,k.boxModel=a.offsetWidth===2,"zoom"in a.style&&(a.style.display="inline",a.style.zoom=1,k.inlineBlockNeedsLayout=a.offsetWidth===2,a.style.display="",a.innerHTML="
",k.shrinkWrapBlocks=a.offsetWidth!==2),a.innerHTML="",q=a.getElementsByTagName("td"),u=q[0].offsetHeight===0,q[0].style.display="",q[1].style.display="none",k.reliableHiddenOffsets=u&&q[0].offsetHeight===0,a.innerHTML="",c.defaultView&&c.defaultView.getComputedStyle&&(j=c.createElement("div"),j.style.width="0",j.style.marginRight="0",a.appendChild(j),k.reliableMarginRight=(parseInt((c.defaultView.getComputedStyle(j,null)||{marginRight:0}).marginRight,10)||0)===0),o.innerHTML="",n.removeChild(o);if(a.attachEvent)for(t in{submit:1,change:1,focusin:1})s="on"+t,u=s in a,u||(a.setAttribute(s,"return;"),u=typeof a[s]=="function"),k[t+"Bubbles"]=u;o=l=g=h=m=j=a=i=null;return k}(),f.boxModel=f.support.boxModel;var i=/^(?:\{.*\}|\[.*\])$/,j=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!l(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i=f.expando,j=typeof c=="string",k=a.nodeType,l=k?f.cache:a,m=k?a[f.expando]:a[f.expando]&&f.expando;if((!m||e&&m&&l[m]&&!l[m][i])&&j&&d===b)return;m||(k?a[f.expando]=m=++f.uuid:m=f.expando),l[m]||(l[m]={},k||(l[m].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?l[m][i]=f.extend(l[m][i],c):l[m]=f.extend(l[m],c);g=l[m],e&&(g[i]||(g[i]={}),g=g[i]),d!==b&&(g[f.camelCase(c)]=d);if(c==="events"&&!g[c])return g[i]&&g[i].events;j?(h=g[c],h==null&&(h=g[f.camelCase(c)])):h=g;return h}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e=f.expando,g=a.nodeType,h=g?f.cache:a,i=g?a[f.expando]:f.expando;if(!h[i])return;if(b){d=c?h[i][e]:h[i];if(d){d[b]||(b=f.camelCase(b)),delete d[b];if(!l(d))return}}if(c){delete h[i][e];if(!l(h[i]))return}var j=h[i][e];f.support.deleteExpando||!h.setInterval?delete h[i]:h[i]=null,j?(h[i]={},g||(h[i].toJSON=f.noop),h[i][e]=j):g&&(f.support.deleteExpando?delete a[f.expando]:a.removeAttribute?a.removeAttribute(f.expando):a[f.expando]=null)}},_data:function(a,b,c){return f.data(a,b,c,!0)},acceptData:function(a){if(a.nodeName){var b=f.noData[a.nodeName.toLowerCase()];if(b)return b!==!0&&a.getAttribute("classid")===b}return!0}}),f.fn.extend({data:function(a,c){var d=null;if(typeof a=="undefined"){if(this.length){d=f.data(this[0]);if(this[0].nodeType===1){var e=this[0].attributes,g;for(var h=0,i=e.length;h-1)return!0;return!1},val:function(a){var c,d,e=this[0];if(!arguments.length){if(e){c=f.valHooks[e.nodeName.toLowerCase()]||f.valHooks[e.type];if(c&&"get"in c&&(d=c.get(e,"value"))!==b)return d;d=e.value;return typeof d=="string"?d.replace(p,""):d==null?"":d}return b}var g=f.isFunction(a);return this.each(function(d){var e=f(this),h;if(this.nodeType===1){g?h=a.call(this,d,e.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.nodeName.toLowerCase()]||f.valHooks[this.type];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c=a.selectedIndex,d=[],e=a.options,g=a.type==="select-one";if(c<0)return null;for(var h=g?c:0,i=g?c+1:e.length;h=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attrFix:{tabindex:"tabIndex"},attr:function(a,c,d,e){var g=a.nodeType;if(!a||g===3||g===8||g===2)return b;if(e&&c in f.attrFn)return f(a)[c](d);if(!("getAttribute"in a))return f.prop(a,c,d);var h,i,j=g!==1||!f.isXMLDoc(a);j&&(c=f.attrFix[c]||c,i=f.attrHooks[c],i||(t.test(c)?i=v:u&&(i=u)));if(d!==b){if(d===null){f.removeAttr(a,c);return b}if(i&&"set"in i&&j&&(h=i.set(a,d,c))!==b)return h;a.setAttribute(c,""+d);return d}if(i&&"get"in i&&j&&(h=i.get(a,c))!==null)return h;h=a.getAttribute(c);return h===null?b:h},removeAttr:function(a,b){var c;a.nodeType===1&&(b=f.attrFix[b]||b,f.attr(a,b,""),a.removeAttribute(b),t.test(b)&&(c=f.propFix[b]||b)in a&&(a[c]=!1))},attrHooks:{type:{set:function(a,b){if(q.test(a.nodeName)&&a.parentNode)f.error("type property can't be changed");else if(!f.support.radioValue&&b==="radio"&&f.nodeName(a,"input")){var c=a.value;a.setAttribute("type",b),c&&(a.value=c);return b}}},value:{get:function(a,b){if(u&&f.nodeName(a,"button"))return u.get(a,b);return b in a?a.value:null},set:function(a,b,c){if(u&&f.nodeName(a,"button"))return u.set(a,b,c);a.value=b}}},propFix:{tabindex:"tabIndex",readonly:"readOnly","for":"htmlFor","class":"className",maxlength:"maxLength",cellspacing:"cellSpacing",cellpadding:"cellPadding",rowspan:"rowSpan",colspan:"colSpan",usemap:"useMap",frameborder:"frameBorder",contenteditable:"contentEditable"},prop:function(a,c,d){var e=a.nodeType;if(!a||e===3||e===8||e===2)return b;var g,h,i=e!==1||!f.isXMLDoc(a);i&&(c=f.propFix[c]||c,h=f.propHooks[c]);return d!==b?h&&"set"in h&&(g=h.set(a,d,c))!==b?g:a[c]=d:h&&"get"in h&&(g=h.get(a,c))!==null?g:a[c]},propHooks:{tabIndex:{get:function(a){var c=a.getAttributeNode("tabindex");return c&&c.specified?parseInt(c.value,10):r.test(a.nodeName)||s.test(a.nodeName)&&a.href?0:b}}}}),f.attrHooks.tabIndex=f.propHooks.tabIndex,v={get:function(a,c){var d;return f.prop(a,c)===!0||(d=a.getAttributeNode(c))&&d.nodeValue!==!1?c.toLowerCase():b},set:function(a,b,c){var d;b===!1?f.removeAttr(a,c):(d=f.propFix[c]||c,d in a&&(a[d]=!0),a.setAttribute(c,c.toLowerCase()));return c}},f.support.getSetAttribute||(u=f.valHooks.button={get:function(a,c){var d;d=a.getAttributeNode(c);return d&&d.nodeValue!==""?d.nodeValue:b},set:function(a,b,d){var e=a.getAttributeNode(d);e||(e=c.createAttribute(d),a.setAttributeNode(e));return e.nodeValue=b+""}},f.each(["width","height"],function(a,b){f.attrHooks[b]=f.extend(f.attrHooks[b],{set:function(a,c){if(c===""){a.setAttribute(b,"auto");return c}}})})),f.support.hrefNormalized||f.each(["href","src","width","height"],function(a,c){f.attrHooks[c]=f.extend(f.attrHooks[c],{get:function(a){var d=a.getAttribute(c,2);return d===null?b:d}})}),f.support.style||(f.attrHooks.style={get:function(a){return a.style.cssText.toLowerCase()||b},set:function(a,b){return a.style.cssText=""+b}}),f.support.optSelected||(f.propHooks.selected=f.extend(f.propHooks.selected,{get:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex);return null}})),f.support.checkOn||f.each(["radio","checkbox"],function(){f.valHooks[this]={get:function(a){return a.getAttribute("value")===null?"on":a.value}}}),f.each(["radio","checkbox"],function(){f.valHooks[this]=f.extend(f.valHooks[this],{set:function(a,b){if(f.isArray(b))return a.checked=f.inArray(f(a).val(),b)>=0}})});var w=/\.(.*)$/,x=/^(?:textarea|input|select)$/i,y=/\./g,z=/ /g,A=/[^\w\s.|`]/g,B=function(a){return a.replace(A,"\\$&")};f.event={add:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){if(d===!1)d=C;else if(!d)return;var g,h;d.handler&&(g=d,d=g.handler),d.guid||(d.guid=f.guid++);var i=f._data(a);if(!i)return;var j=i.events,k=i.handle;j||(i.events=j={}),k||(i.handle=k=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.handle.apply(k.elem,arguments):b}),k.elem=a,c=c.split(" ");var l,m=0,n;while(l=c[m++]){h=g?f.extend({},g):{handler:d,data:e},l.indexOf(".")>-1?(n=l.split("."),l=n.shift(),h.namespace=n.slice(0).sort().join(".")):(n=[],h.namespace=""),h.type=l,h.guid||(h.guid=d.guid);var o=j[l],p=f.event.special[l]||{};if(!o){o=j[l]=[];if(!p.setup||p.setup.call(a,e,n,k)===!1)a.addEventListener?a.addEventListener(l,k,!1):a.attachEvent&&a.attachEvent("on"+l,k)}p.add&&(p.add.call(a,h),h.handler.guid||(h.handler.guid=d.guid)),o.push(h),f.event.global[l]=!0}a=null}},global:{},remove:function(a,c,d,e){if(a.nodeType!==3&&a.nodeType!==8){d===!1&&(d=C);var g,h,i,j,k=0,l,m,n,o,p,q,r,s=f.hasData(a)&&f._data(a),t=s&&s.events;if(!s||!t)return;c&&c.type&&(d=c.handler,c=c.type);if(!c||typeof c=="string"&&c.charAt(0)==="."){c=c||"";for(h in t)f.event.remove(a,h+c);return}c=c.split(" ");while(h=c[k++]){r=h,q=null,l=h.indexOf(".")<0,m=[],l||(m=h.split("."),h=m.shift(),n=new RegExp("(^|\\.)"+f.map(m.slice(0).sort(),B).join("\\.(?:.*\\.)?")+"(\\.|$)")),p=t[h];if(!p)continue;if(!d){for(j=0;j =0&&(h=h.slice(0,-1),j=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if(!!e&&!f.event.customEvent[h]||!!f.event.global[h]){c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.exclusive=j,c.namespace=i.join("."),c.namespace_re=new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)");if(g||!e)c.preventDefault(),c.stopPropagation();if(!e){f.each(f.cache,function(){var a=f.expando,b=this[a];b&&b.events&&b.events[h]&&f.event.trigger(c,d,b.handle.elem)});return}if(e.nodeType===3||e.nodeType===8)return;c.result=b,c.target=e,d=d!=null?f.makeArray(d):[],d.unshift(c);var k=e,l=h.indexOf(":")<0?"on"+h:"";do{var m=f._data(k,"handle");c.currentTarget=k,m&&m.apply(k,d),l&&f.acceptData(k)&&k[l]&&k[l].apply(k,d)===!1&&(c.result=!1,c.preventDefault()),k=k.parentNode||k.ownerDocument||k===c.target.ownerDocument&&a}while(k&&!c.isPropagationStopped());if(!c.isDefaultPrevented()){var n,o=f.event.special[h]||{};if((!o._default||o._default.call(e.ownerDocument,c)===!1)&&(h!=="click"||!f.nodeName(e,"a"))&&f.acceptData(e)){try{l&&e[h]&&(n=e[l],n&&(e[l]=null),f.event.triggered=h,e[h]())}catch(p){}n&&(e[l]=n),f.event.triggered=b}}return c.result}},handle:function(c){c=f.event.fix(c||a.event);var d=((f._data(this,"events")||{})[c.type]||[]).slice(0),e=!c.exclusive&&!c.namespace,g=Array.prototype.slice.call(arguments,0);g[0]=c,c.currentTarget=this;for(var h=0,i=d.length;h-1?f.map(a.options,function(a){return a.selected}).join("-"):"":f.nodeName(a,"select")&&(c=a.selectedIndex);return c},I=function(c){var d=c.target,e,g;if(!!x.test(d.nodeName)&&!d.readOnly){e=f._data(d,"_change_data"),g=H(d),(c.type!=="focusout"||d.type!=="radio")&&f._data(d,"_change_data",g);if(e===b||g===e)return;if(e!=null||g)c.type="change",c.liveFired=b,f.event.trigger(c,arguments[1],d)}};f.event.special.change={filters:{focusout:I,beforedeactivate:I,click:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(c==="radio"||c==="checkbox"||f.nodeName(b,"select"))&&I.call(this,a)},keydown:function(a){var b=a.target,c=f.nodeName(b,"input")?b.type:"";(a.keyCode===13&&!f.nodeName(b,"textarea")||a.keyCode===32&&(c==="checkbox"||c==="radio")||c==="select-multiple")&&I.call(this,a)},beforeactivate:function(a){var b=a.target;f._data(b,"_change_data",H(b))}},setup:function(a,b){if(this.type==="file")return!1;for(var c in G)f.event.add(this,c+".specialChange",G[c]);return x.test(this.nodeName)},teardown:function(a){f.event.remove(this,".specialChange");return x.test(this.nodeName)}},G=f.event.special.change.filters,G.focus=G.beforeactivate}f.support.focusinBubbles||f.each({focus:"focusin",blur:"focusout"},function(a,b){function e(a){var c=f.event.fix(a);c.type=b,c.originalEvent={},f.event.trigger(c,null,c.target),c.isDefaultPrevented()&&a.preventDefault()}var d=0;f.event.special[b]={setup:function(){d++===0&&c.addEventListener(a,e,!0)},teardown:function(){--d===0&&c.removeEventListener(a,e,!0)}}}),f.each(["bind","one"],function(a,c){f.fn[c]=function(a,d,e){var g;if(typeof a=="object"){for(var h in a)this[c](h,d,a[h],e);return this}if(arguments.length===2||d===!1)e=d,d=b;c==="one"?(g=function(a){f(this).unbind(a,g);return e.apply(this,arguments)},g.guid=e.guid||f.guid++):g=e;if(a==="unload"&&c!=="one")this.one(a,d,e);else for(var i=0,j=this.length;i0?this.bind(b,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0)}),function(){function u(a,b,c,d,e,f){for(var g=0,h=d.length;g0){j=i;break}}i=i[a]}d[g]=j}}}function t(a,b,c,d,e,f){for(var g=0,h=d.length;g+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d=0,e=Object.prototype.toString,g=!1,h=!0,i=/\\/g,j=/\W/;[0,0].sort(function(){h=!1;return 0});var k=function(b,d,f,g){f=f||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return f;var i,j,n,o,q,r,s,t,u=!0,w=k.isXML(d),x=[],y=b;do{a.exec(""),i=a.exec(y);if(i){y=i[3],x.push(i[1]);if(i[2]){o=i[3];break}}}while(i);if(x.length>1&&m.exec(b))if(x.length===2&&l.relative[x[0]])j=v(x[0]+x[1],d);else{j=l.relative[x[0]]?[d]:k(x.shift(),d);while(x.length)b=x.shift(),l.relative[b]&&(b+=x.shift()),j=v(b,j)}else{!g&&x.length>1&&d.nodeType===9&&!w&&l.match.ID.test(x[0])&&!l.match.ID.test(x[x.length-1])&&(q=k.find(x.shift(),d,w),d=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]);if(d){q=g?{expr:x.pop(),set:p(g)}:k.find(x.pop(),x.length===1&&(x[0]==="~"||x[0]==="+")&&d.parentNode?d.parentNode:d,w),j=q.expr?k.filter(q.expr,q.set):q.set,x.length>0?n=p(j):u=!1;while(x.length)r=x.pop(),s=r,l.relative[r]?s=x.pop():r="",s==null&&(s=d),l.relative[r](n,s,w)}else n=x=[]}n||(n=j),n||k.error(r||b);if(e.call(n)==="[object Array]")if(!u)f.push.apply(f,n);else if(d&&d.nodeType===1)for(t=0;n[t]!=null;t++)n[t]&&(n[t]===!0||n[t].nodeType===1&&k.contains(d,n[t]))&&f.push(j[t]);else for(t=0;n[t]!=null;t++)n[t]&&n[t].nodeType===1&&f.push(j[t]);else p(n,f);o&&(k(o,h,f,g),k.uniqueSort(f));return f};k.uniqueSort=function(a){if(r){g=h,a.sort(r);if(g)for(var b=1;b0},k.find=function(a,b,c){var d;if(!a)return[];for(var e=0,f=l.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!j.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(i,"")},TAG:function(a,b){return a[1].replace(i,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||k.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&k.error(a[0]);a[0]=d++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(i,"");!f&&l.attrMap[g]&&(a[1]=l.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(i,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=k(b[3],null,null,c);else{var g=k.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(l.match.POS.test(b[0])||l.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!k(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=l.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||k.getText([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=l.attrHandle[c]?l.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=l.setFilters[e];if(f)return f(a,c,b,d)}}},m=l.match.POS,n=function(a,b){return"\\"+(b-0+1)};for(var o in l.match)l.match[o]=new RegExp(l.match[o].source+/(?![^\[]*\])(?![^\(]*\))/.source),l.leftMatch[o]=new RegExp(/(^(?:.|\r|\n)*?)/.source+l.match[o].source.replace(/\\(\d+)/g,n));var p=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(q){p=function(a,b){var c=0,d=b||[];if(e.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var f=a.length;c ",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(l.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},l.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(l.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML=" ",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(l.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=k,b=c.createElement("div"),d="__sizzle__";b.innerHTML="
";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){k=function(b,e,f,g){e=e||c;if(!g&&!k.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return p(e.getElementsByTagName(b),f);if(h[2]&&l.find.CLASS&&e.getElementsByClassName)return p(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return p([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return p([],f);if(i.id===h[3])return p([i],f)}try{return p(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var m=e,n=e.getAttribute("id"),o=n||d,q=e.parentNode,r=/^\s*[+~]/.test(b);n?o=o.replace(/'/g,"\\$&"):e.setAttribute("id",o),r&&q&&(e=e.parentNode);try{if(!r||q)return p(e.querySelectorAll("[id='"+o+"'] "+b),f)}catch(s){}finally{n||m.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)k[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}k.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(a))try{if(e||!l.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return k(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;l.order.splice(1,0,"CLASS"),l.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?k.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?k.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:k.contains=function(){return!1},k.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var v=function(a,b){var c,d=[],e="",f=b.nodeType?[b]:b;while(c=l.match.PSEUDO.exec(a))e+=c[0],a=a.replace(l.match.PSEUDO,"");a=l.relative[a]?a+"*":a;for(var g=0,h=f.length;g0)for(h=g;h0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h,i,j={},k=1;if(g&&a.length){for(d=0,e=a.length;d-1:f(g).is(h))&&c.push({selector:i,elem:g,level:k});g=g.parentNode,k++}}return c}var l=S.test(a)||typeof a!="string"?f(a,b||this.context):0;for(d=0,e=this.length;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(U(c[0])||U(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling(a.parentNode.firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c),g=R.call(arguments);N.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!T[a]?f.unique(e):e,(this.length>1||P.test(d))&&O.test(a)&&(e=e.reverse());return this.pushStack(e,a,g.join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/",""],legend:[1,""," "],thead:[1,""],tr:[2,""],td:[3,""],col:[2,""],area:[1,""," "],_default:[0,"",""]};be.optgroup=be.option,be.tbody=be.tfoot=be.colgroup=be.caption=be.thead,be.th=be.td,f.support.htmlSerialize||(be._default=[1,"div","
"]),f.fn.extend({text:function(a){if(f.isFunction(a))return this.each(function(b){var c=f(this);c.text(a.call(this,b,c.text()))});if(typeof a!="object"&&a!==b)return this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a));return f.text(this)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){f(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f(arguments[0]).toArray());return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){if(a===b)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!be[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>$2>");try{for(var c=0,d=this.length;c1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d=a.cloneNode(!0),e,g,h;if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bh(a,d),e=bi(a),g=bi(d);for(h=0;e[h];++h)g[h]&&bh(e[h],g[h])}if(b){bg(a,d);if(c){e=bi(a),g=bi(d);for(h=0;e[h];++h)bg(e[h],g[h])}}e=g=null;return d},clean:function(a,b,d,e){var g;b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);var h=[],i;for(var j=0,k;(k=a[j])!=null;j++){typeof k=="number"&&(k+="");if(!k)continue;if(typeof k=="string")if(!_.test(k))k=b.createTextNode(k);else{k=k.replace(Y,"<$1>$2>");var l=(Z.exec(k)||["",""])[1].toLowerCase(),m=be[l]||be._default,n=m[0],o=b.createElement("div");o.innerHTML=m[1]+k+m[2];while(n--)o=o.lastChild;if(!f.support.tbody){var p=$.test(k),q=l==="table"&&!p?o.firstChild&&o.firstChild.childNodes:m[1]===""&&!p?o.childNodes:[];for(i=q.length-1;i>=0;--i)f.nodeName(q[i],"tbody")&&!q[i].childNodes.length&&q[i].parentNode.removeChild(q[i])}!f.support.leadingWhitespace&&X.test(k)&&o.insertBefore(b.createTextNode(X.exec(k)[0]),o.firstChild),k=o.childNodes}var r;if(!f.support.appendChecked)if(k[0]&&typeof (r=k.length)=="number")for(i=0;i=0)return b+"px"}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bn.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNaN(b)?"":"alpha(opacity="+b*100+")",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bm,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bm.test(g)?g.replace(bm,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){var c;f.swap(a,{display:"inline-block"},function(){b?c=bv(a,"margin-right","marginRight"):c=a.style.marginRight});return c}})}),c.defaultView&&c.defaultView.getComputedStyle&&(bw=function(a,c){var d,e,g;c=c.replace(bo,"-$1").toLowerCase();if(!(e=a.ownerDocument.defaultView))return b;if(g=e.getComputedStyle(a,null))d=g.getPropertyValue(c),d===""&&!f.contains(a.ownerDocument.documentElement,a)&&(d=f.style(a,c));return d}),c.documentElement.currentStyle&&(bx=function(a,b){var c,d=a.currentStyle&&a.currentStyle[b],e=a.runtimeStyle&&a.runtimeStyle[b],f=a.style;!bp.test(d)&&bq.test(d)&&(c=f.left,e&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":d||0,d=f.pixelLeft+"px",f.left=c,e&&(a.runtimeStyle.left=e));return d===""?"auto":d}),bv=bw||bx,f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)});var bz=/%20/g,bA=/\[\]$/,bB=/\r?\n/g,bC=/#.*$/,bD=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bE=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bF=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bG=/^(?:GET|HEAD)$/,bH=/^\/\//,bI=/\?/,bJ=/