├── .gitignore ├── .idea └── uiDesigner.xml ├── README.md ├── build.gradle ├── gradlew ├── gradlew.bat └── src └── main ├── groovy └── com │ └── thang │ └── realtime │ └── dashboard │ ├── controller │ └── PollController.groovy │ ├── domain │ ├── Poll.groovy │ ├── PollAnswer.groovy │ └── PollChoice.groovy │ ├── dto │ ├── ChoiceStats.groovy │ └── PollStats.groovy │ ├── repository │ ├── PollAnswerRepository.groovy │ └── PollRepository.groovy │ └── service │ └── PollService.groovy ├── java └── com │ └── thang │ └── realtime │ └── dashboard │ ├── Angular2Application.java │ └── config │ ├── ApplicationConfig.java │ ├── MessageSchedulingConfigurer.java │ ├── SecurityConfig.java │ └── WebSocketConfig.java └── resources ├── application.properties ├── poll_data.sql ├── poll_schema.sql └── public ├── app ├── app.component.ts ├── dashboard.component.ts ├── domain │ ├── poll.answer.ts │ ├── poll.choice.domain.ts │ └── poll.domain.ts ├── main.ts ├── poll.component.ts └── poll.list.component.ts ├── index.html ├── js └── sockjs-0.3.4.js ├── package.json ├── systemjs.config.js ├── templates ├── dashboard.html ├── index.html ├── poll.html └── poll.list.html ├── tsconfig.json └── typings.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.nb-gradle/ 2 | /src/main/resources/public/node_modules/ 3 | /src/main/resources/public/typings/ 4 | /.gradle/ 5 | /.idea/ 6 | /gradle/ 7 | /build/ -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spring Boot-Angular2 Realtime Dashboard 2 | ## Overview 3 | This project demonstrate the upcoming AngularJS 2 and integrating it with Spring Boot. It will also demo some hot features of Spring Boot such as realtime programming using Websocket + Stomp. 4 | Most of the backend code are written in Groovy, instead of Java. Most of them are still looks Java, but there are some major improvements over Java in object initialising. 5 | There are 2 routes (pages) in the application 6 | - http://localhost:8080/ The root page. This page will show a list of polls. Clicking on one of those will show the poll details with choices. 7 | Try to open the page in multiple browsers (or multiple tabs). Notice that when you change a poll, or select a choice, that action will be synchronized in all other browsers. 8 | - http://localhost:8080/dashboard --> The Dashboard page. This show the statistics of the polls, using a Pie chart. 9 | When you submit the poll in another browser(tab), the dashboard will be updated immediately in real time. 10 | All in all, it is very straightforward to develop a realtime application in Spring Boot. You don't need NodeJS for that. And Java/Groovy is far more powerful and easier to maintain in the long run. 11 | 12 | ## Technology stacks 13 | - Spring Boot 14 | - Java & Groovy 15 | - AngularJS 2 + TypeScript 16 | - MySQL 17 | - Websocket (STOMP + SockJS) 18 | 19 | ## Dependencies 20 | - Java 8 21 | - Gradle 22 | - NodeJS & NPM 23 | - MySQL 24 | 25 | ## Building and starting Spring boot 26 | 1. First we need to download all the front-end dependencies 27 | `` 28 | cd src/main/resources/public 29 | npm install 30 | `` 31 | 2. Run the SQL scripts to create the tables and the initial data 32 | You can either run them manually in a MySQL client, or rename them to data.sql and schema.sql respectively. Spring Boot will run them automatically when the application starts. 33 | The catch is you will need to rename them back or the next time you start the app, Spring Boot will try to run them again. 34 | 35 | 3. Change to the root folder of the project and start Spring Boot 36 | `` 37 | gradle bootRun 38 | `` 39 | 40 | 4. The application can now be accessed at 41 | `` 42 | http://localhost:8080 43 | `` 44 | The default username and password is admin/admin. You can change them in application.properties -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath("org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE") 8 | classpath 'org.springframework:springloaded:1.2.6.RELEASE' 9 | } 10 | } 11 | 12 | apply plugin: 'java' 13 | apply plugin: 'groovy' 14 | apply plugin: 'eclipse' 15 | apply plugin: 'idea' 16 | apply plugin: 'spring-boot' 17 | 18 | jar { 19 | baseName = 'realtime-dashboard' 20 | version = '0.1.0' 21 | } 22 | 23 | repositories { 24 | jcenter() 25 | } 26 | 27 | sourceCompatibility = 1.8 28 | targetCompatibility = 1.8 29 | 30 | dependencies { 31 | compile("org.springframework.boot:spring-boot-starter-web") 32 | compile('org.codehaus.groovy:groovy') 33 | compile("org.springframework.boot:spring-boot-starter-websocket") 34 | compile("org.springframework.boot:spring-boot-starter-security") 35 | compile("org.springframework:spring-messaging") 36 | compile("org.springframework.boot:spring-boot-starter-data-jpa") 37 | runtime("mysql:mysql-connector-java") 38 | compile("org.springframework.boot:spring-boot-starter-jdbc") 39 | 40 | compile("javax.inject:javax.inject:1") 41 | testCompile("junit:junit") 42 | testCompile("org.springframework.boot:spring-boot-starter-test") 43 | } 44 | 45 | bootRun { 46 | addResources = true 47 | } 48 | 49 | task wrapper(type: Wrapper) { 50 | gradleVersion = '2.3' 51 | } 52 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/controller/PollController.groovy: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.controller; 2 | 3 | import com.thang.realtime.dashboard.domain.Poll 4 | import com.thang.realtime.dashboard.domain.PollAnswer 5 | import com.thang.realtime.dashboard.domain.PollChoice 6 | import com.thang.realtime.dashboard.dto.PollStats 7 | import com.thang.realtime.dashboard.service.PollService 8 | import groovy.transform.CompileStatic 9 | import org.springframework.beans.factory.annotation.Autowired 10 | import org.springframework.messaging.handler.annotation.Payload 11 | import org.springframework.security.core.Authentication 12 | import org.springframework.security.core.context.SecurityContextHolder 13 | import org.springframework.web.bind.annotation.PathVariable 14 | import org.springframework.web.bind.annotation.RequestBody 15 | 16 | import javax.inject.Inject; 17 | import org.springframework.messaging.handler.annotation.MessageMapping; 18 | import org.springframework.messaging.handler.annotation.SendTo; 19 | import org.springframework.messaging.simp.SimpMessagingTemplate 20 | import org.springframework.web.bind.annotation.RequestMapping 21 | import org.springframework.web.bind.annotation.RequestMethod; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | /** 25 | * 26 | * @author thangnguyen 27 | */ 28 | @RestController 29 | @RequestMapping("/api") 30 | @CompileStatic 31 | public class PollController { 32 | private SimpMessagingTemplate template; 33 | 34 | @Autowired 35 | private PollService pollService; 36 | 37 | @Inject 38 | public PollController(SimpMessagingTemplate template) { 39 | this.template = template; 40 | } 41 | 42 | @RequestMapping(value = "/poll", method = RequestMethod.GET) 43 | public Set getPolls() { 44 | Set polls = (Set) pollService.findAll(); 45 | return polls; 46 | } 47 | 48 | @RequestMapping(value = "/poll/stats", method = RequestMethod.GET) 49 | def ArrayList getPollStats() { 50 | ArrayList stats = pollService.getPollStats(); 51 | return stats; 52 | } 53 | 54 | 55 | @RequestMapping(value = "/poll/{id:[\\d]+}/submit",method = RequestMethod.POST) 56 | public void submitPoll(@PathVariable Long id, @RequestBody PollChoice choice) { 57 | Poll poll = pollService.findById(id); 58 | PollAnswer answer = new PollAnswer(); 59 | Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 60 | answer.setUser(auth.getName()); 61 | answer.setPollChoice(choice) 62 | pollService.savePollAnswer(answer) 63 | 64 | //refresh the data in the Dashboard 65 | ArrayList stats = pollService.getPollStats(); 66 | template.convertAndSend("/queue/answerSubmitted", stats); 67 | } 68 | 69 | @MessageMapping("/selectPoll") 70 | @SendTo("/queue/selectPoll") 71 | def Poll getPollList(@Payload Poll poll) { 72 | return poll 73 | } 74 | 75 | @MessageMapping("/selectChoice") 76 | @SendTo("/queue/selectChoice") 77 | def PollChoice selectPollChoice(@Payload PollChoice pollChoice) { 78 | return pollChoice 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/domain/Poll.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.thang.realtime.dashboard.domain 7 | 8 | import javax.persistence.CascadeType 9 | import javax.persistence.Entity 10 | import javax.persistence.FetchType 11 | import javax.persistence.GeneratedValue 12 | import javax.persistence.GenerationType 13 | import javax.persistence.Id 14 | import javax.persistence.OneToMany 15 | import javax.persistence.OrderBy 16 | 17 | /** 18 | * 19 | * @author thangnguyen 20 | */ 21 | @Entity 22 | public class Poll { 23 | @Id 24 | @GeneratedValue(strategy=GenerationType.AUTO) 25 | Long id; 26 | String name; 27 | 28 | @OneToMany(mappedBy = "poll", cascade = CascadeType.ALL,fetch = FetchType.EAGER) 29 | @OrderBy("id ASC") 30 | Set choices; 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/domain/PollAnswer.groovy: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.domain 2 | 3 | import javax.persistence.Entity 4 | import javax.persistence.GeneratedValue 5 | import javax.persistence.GenerationType 6 | import javax.persistence.Id 7 | import javax.persistence.JoinColumn 8 | import javax.persistence.ManyToOne 9 | 10 | /** 11 | * Created by thangnguyen on 5/22/16. 12 | */ 13 | @Entity 14 | public class PollAnswer { 15 | @Id 16 | @GeneratedValue 17 | Long id 18 | String user 19 | 20 | @ManyToOne 21 | @JoinColumn(name = "poll_choice_id") 22 | PollChoice pollChoice 23 | } 24 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/domain/PollChoice.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * To change this license header, choose License Headers in Project Properties. 3 | * To change this template file, choose Tools | Templates 4 | * and open the template in the editor. 5 | */ 6 | package com.thang.realtime.dashboard.domain 7 | 8 | import com.fasterxml.jackson.annotation.JsonIgnore 9 | 10 | import javax.persistence.CascadeType 11 | import javax.persistence.Entity 12 | import javax.persistence.GeneratedValue 13 | import javax.persistence.GenerationType 14 | import javax.persistence.Id 15 | import javax.persistence.JoinColumn 16 | import javax.persistence.ManyToOne 17 | import javax.persistence.OneToMany 18 | import javax.persistence.OrderBy; 19 | 20 | /** 21 | * 22 | * @author thangnguyen 23 | */ 24 | @Entity 25 | public class PollChoice { 26 | @Id 27 | @GeneratedValue(strategy=GenerationType.AUTO) 28 | Long id; 29 | String choice; 30 | 31 | @JsonIgnore //don't export this one, or it will become an infiteloop 32 | @ManyToOne 33 | @JoinColumn(name = "poll_id") 34 | Poll poll 35 | 36 | @JsonIgnore 37 | @OneToMany(mappedBy = "pollChoice", cascade = CascadeType.ALL) 38 | Set answers 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/dto/ChoiceStats.groovy: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.dto 2 | 3 | /** 4 | * Created by thangnguyen on 5/24/16. 5 | */ 6 | public class ChoiceStats { 7 | Long id; 8 | String choice; 9 | Long totalVote; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/dto/PollStats.groovy: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.dto 2 | 3 | import groovy.transform.CompileStatic 4 | 5 | /** 6 | * Created by thangnguyen on 5/24/16. 7 | */ 8 | @CompileStatic 9 | public class PollStats { 10 | Long id 11 | String name 12 | Long totalVote; 13 | ArrayList choices 14 | } 15 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/repository/PollAnswerRepository.groovy: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.repository 2 | 3 | import com.thang.realtime.dashboard.domain.PollAnswer 4 | import org.springframework.data.repository.CrudRepository 5 | import org.springframework.stereotype.Repository 6 | 7 | /** 8 | * Created by thangnguyen on 5/22/16. 9 | */ 10 | @Repository 11 | public interface PollAnswerRepository extends CrudRepository { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/repository/PollRepository.groovy: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.repository 2 | 3 | import com.thang.realtime.dashboard.domain.Poll 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | /** 8 | * Created by thangnguyen on 5/22/16. 9 | */ 10 | @Repository 11 | public interface PollRepository extends CrudRepository { 12 | } 13 | -------------------------------------------------------------------------------- /src/main/groovy/com/thang/realtime/dashboard/service/PollService.groovy: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.service 2 | 3 | import com.thang.realtime.dashboard.domain.PollAnswer 4 | import com.thang.realtime.dashboard.dto.ChoiceStats 5 | import com.thang.realtime.dashboard.dto.PollStats 6 | import com.thang.realtime.dashboard.repository.PollAnswerRepository 7 | import com.thang.realtime.dashboard.repository.PollRepository 8 | import org.springframework.beans.factory.annotation.Autowired 9 | import org.springframework.stereotype.Service 10 | 11 | import javax.persistence.EntityManager 12 | import javax.persistence.Query 13 | 14 | /** 15 | * Created by thangnguyen on 5/22/16. 16 | */ 17 | @Service 18 | public class PollService { 19 | @Autowired 20 | private PollRepository pollRepository; 21 | 22 | @Autowired 23 | private PollAnswerRepository pollAnswerRepository; 24 | 25 | @Autowired 26 | private EntityManager entityManager; 27 | 28 | def findAll() { 29 | return pollRepository.findAll(); 30 | } 31 | 32 | def findById(Long id){ 33 | return pollRepository.findOne(id); 34 | } 35 | 36 | def PollAnswer savePollAnswer(PollAnswer answer) { 37 | return pollAnswerRepository.save(answer) 38 | } 39 | 40 | def ArrayList getPollStats() { 41 | Query q1 = this.entityManager.createQuery("SELECT p.id,p.name,count(a.id) as totalVote FROM PollAnswer a" 42 | + " JOIN a.pollChoice c" 43 | + " JOIN c.poll p" 44 | + " GROUP BY p.id"); 45 | 46 | ArrayList results = q1.getResultList(); 47 | ArrayList stats = new ArrayList(); 48 | results.each { result -> 49 | //with each poll, we will query all the choices along with the statistics 50 | Query q2 = this.entityManager.createQuery("SELECT c.id,c.choice,count(a.id) as totalVote FROM PollAnswer a" 51 | + " JOIN a.pollChoice c" 52 | + " JOIN c.poll p" 53 | + " WHERE p.id = :id" 54 | + " GROUP BY c.id"); 55 | ArrayList choices = q2.setParameter("id",result[0]).getResultList() 56 | ArrayList choiceStats = new ArrayList<>(); 57 | choices.each { choice -> 58 | ChoiceStats choiceStat = new ChoiceStats([ 59 | id: choice[0], 60 | choice: choice[1], 61 | totalVote: choice[2] 62 | ]); 63 | choiceStats.add(choiceStat); 64 | } 65 | 66 | PollStats stat = new PollStats([ 67 | id: result[0], 68 | name: result[1], 69 | totalVote: result[2], 70 | choices: choiceStats 71 | ]) 72 | stats.add(stat); 73 | } 74 | 75 | return stats; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/thang/realtime/dashboard/Angular2Application.java: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | 7 | @SpringBootApplication 8 | @ComponentScan 9 | public class Angular2Application { 10 | public static void main(String[] args) { 11 | SpringApplication.run(Angular2Application.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/thang/realtime/dashboard/config/ApplicationConfig.java: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.config; 2 | 3 | import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; 4 | import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer; 5 | import org.springframework.boot.context.embedded.ErrorPage; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.http.HttpStatus; 9 | 10 | /** 11 | * Created by thangnguyen on 5/18/16. 12 | */ 13 | @Configuration 14 | public class ApplicationConfig { 15 | @Bean 16 | public EmbeddedServletContainerCustomizer containerCustomizer() { 17 | return new EmbeddedServletContainerCustomizer() { 18 | @Override 19 | public void customize(ConfigurableEmbeddedServletContainer container) { 20 | container.addErrorPages(new ErrorPage(HttpStatus.NOT_FOUND, "/index.html")); 21 | } 22 | }; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/thang/realtime/dashboard/config/MessageSchedulingConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.scheduling.annotation.EnableScheduling; 6 | import org.springframework.scheduling.annotation.SchedulingConfigurer; 7 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; 8 | import org.springframework.scheduling.config.ScheduledTaskRegistrar; 9 | 10 | /** 11 | * Created by thangnguyen on 5/05/16. 12 | */ 13 | @Configuration 14 | @EnableScheduling 15 | public class MessageSchedulingConfigurer implements SchedulingConfigurer { 16 | 17 | @Bean 18 | public ThreadPoolTaskScheduler taskScheduler() { 19 | return new ThreadPoolTaskScheduler(); 20 | } 21 | 22 | @Override 23 | public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { 24 | taskRegistrar.setTaskScheduler(taskScheduler()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/thang/realtime/dashboard/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.config; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.http.HttpMethod; 6 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 7 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 8 | import org.springframework.security.config.annotation.web.builders.WebSecurity; 9 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 10 | 11 | /** 12 | * Web Security configuration class, used to configure the security for REST API 13 | * 14 | * @author agrawald 15 | */ 16 | @Configuration 17 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 18 | @Value("${security.user.name}") 19 | String adminUser; 20 | @Value("${security.user.password}") 21 | String adminPassword; 22 | @Value("${user.name}") 23 | String user; 24 | @Value("${user.password}") 25 | String password; 26 | 27 | @Override 28 | public void init(WebSecurity web) throws Exception { 29 | 30 | } 31 | 32 | @Override 33 | protected void configure(HttpSecurity http) throws Exception { 34 | http.authorizeRequests() 35 | .anyRequest() 36 | .fullyAuthenticated(); 37 | http.httpBasic(); 38 | http.csrf().disable(); 39 | } 40 | 41 | @Override 42 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 43 | auth.inMemoryAuthentication().withUser(adminUser).password(adminPassword).roles("USER", "ADMIN"); 44 | auth.inMemoryAuthentication().withUser(user).password(password).roles("USER"); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/thang/realtime/dashboard/config/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.thang.realtime.dashboard.config; 2 | 3 | import java.util.List; 4 | 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.messaging.converter.MessageConverter; 9 | import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; 10 | import org.springframework.messaging.handler.invocation.HandlerMethodReturnValueHandler; 11 | import org.springframework.messaging.simp.config.ChannelRegistration; 12 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 13 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 14 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 15 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 16 | import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration; 17 | 18 | /** 19 | * Created by thangnguyen on 5/05/16. 20 | */ 21 | @Configuration 22 | @EnableWebSocketMessageBroker 23 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 24 | private static final Logger LOGGER = LoggerFactory.getLogger(WebSocketConfig.class); 25 | 26 | @Override 27 | public void configureMessageBroker(MessageBrokerRegistry config) { 28 | config.enableSimpleBroker("/queue", "/topic"); 29 | config.setApplicationDestinationPrefixes("/websocket"); 30 | } 31 | 32 | @Override 33 | public void registerStompEndpoints(StompEndpointRegistry registry) { 34 | registry.addEndpoint("/message").withSockJS(); 35 | } 36 | 37 | @Override 38 | public void configureClientInboundChannel(ChannelRegistration channelRegistration) { 39 | } 40 | 41 | @Override 42 | public void configureClientOutboundChannel(ChannelRegistration channelRegistration) { 43 | } 44 | 45 | @Override 46 | public boolean configureMessageConverters(List converters) { 47 | return true; 48 | } 49 | 50 | @Override 51 | public void configureWebSocketTransport(WebSocketTransportRegistration var1) { 52 | } 53 | 54 | @Override 55 | public void addReturnValueHandlers(List var1) { 56 | } 57 | 58 | @Override 59 | public void addArgumentResolvers(List var1) { 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | ## security configuration 2 | security.user.name=admin 3 | security.user.password=admin 4 | user.name = user 5 | user.password = user 6 | 7 | ## database configuration 8 | spring.jpa.hibernate.ddl-auto=update 9 | spring.datasource.url=jdbc:mysql://localhost/poll_db 10 | spring.datasource.username=root 11 | spring.datasource.password=elmo 12 | spring.datasource.driver-class-name=com.mysql.jdbc.Driver 13 | 14 | ## This is required as we do not want spring web to check the location of template files 15 | spring.groovy.template.check-template-location=false 16 | 17 | server.port=8080 18 | -------------------------------------------------------------------------------- /src/main/resources/poll_data.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO `poll` (id,`name`) VALUES (1,'What is the best Rock band ?'); 2 | INSERT INTO `poll` (id,`name`) VALUES (2,'Which MVC framework do you like the most ?'); 3 | INSERT INTO `poll` (id,`name`) VALUES (3, 'Which is the best Javascript framework ?'); 4 | 5 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('1', 'Metallica', '1'); 6 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('2', 'Guns N Roses', '1'); 7 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('3', 'Queen', '1'); 8 | 9 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('4', 'Spring Boot/Spring MVC', '2'); 10 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('5', 'Ruby on Rails', '2'); 11 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('6', 'Django', '2'); 12 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('7', 'Symfony (PHP)', '2'); 13 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('8', 'Other', '2'); 14 | 15 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('9', 'Meteor', '3'); 16 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('10', 'AngularJS 2', '3'); 17 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('11', 'EmberJS', '3'); 18 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('12', 'Backbone', '3'); 19 | INSERT INTO `poll_choice` (`id`, `choice`, `poll_id`) VALUES ('13', 'Other', '3'); 20 | -------------------------------------------------------------------------------- /src/main/resources/poll_schema.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE poll_db; 2 | USE poll_db; 3 | 4 | CREATE TABLE poll ( 5 | id INT NOT NULL AUTO_INCREMENT, 6 | name VARCHAR(255) NULL, 7 | PRIMARY KEY (id)); 8 | 9 | CREATE TABLE poll_choice ( 10 | id INT NOT NULL AUTO_INCREMENT, 11 | choice VARCHAR(255) NULL, 12 | poll_id INT NULL, 13 | PRIMARY KEY (id), 14 | INDEX Poll_id (poll_id ASC), 15 | CONSTRAINT poll 16 | FOREIGN KEY (poll_id) 17 | REFERENCES poll (id) 18 | ON DELETE NO ACTION 19 | ON UPDATE NO ACTION); 20 | 21 | CREATE TABLE poll_answer ( 22 | id INT NOT NULL AUTO_INCREMENT , 23 | poll_choice_id INT NULL, 24 | user VARCHAR(45) NULL, 25 | PRIMARY KEY (id), 26 | INDEX poll_choice_id (poll_choice_id ASC), 27 | CONSTRAINT poll_choice_id 28 | FOREIGN KEY (poll_choice_id) 29 | REFERENCES poll_choice (id) 30 | ON DELETE NO ACTION 31 | ON UPDATE NO ACTION); 32 | -------------------------------------------------------------------------------- /src/main/resources/public/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Http, HTTP_PROVIDERS} from '@angular/http'; 3 | import { Poll } from './domain/poll.domain'; 4 | import { PollListComponent } from './poll.list.component'; 5 | import { DashboardComponent } from './dashboard.component'; 6 | import { Router,ROUTER_DIRECTIVES, Routes } from '@angular/router'; 7 | 8 | @Component({ 9 | selector: 'my-app', 10 | templateUrl: 'templates/index.html', 11 | directives: [ROUTER_DIRECTIVES] 12 | }) 13 | @Routes([ 14 | {path: '/dashboard', component: DashboardComponent}, 15 | {path: '/poll', component: PollListComponent}, 16 | {path: '/', component: PollListComponent} 17 | ]) 18 | export class AppComponent { 19 | constructor(public http: Http,private router: Router) { 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /src/main/resources/public/app/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core'; 2 | import {Http, HTTP_PROVIDERS} from '@angular/http'; 3 | import { PollStats } from './domain/pollStats.domain'; 4 | import { ChoiceStats } from './domain/choiceStats.domain'; 5 | import {nvD3} from 'ng2-nvd3' 6 | 7 | declare let d3: any; 8 | declare var SockJS; 9 | declare var Stomp; 10 | 11 | @Component({ 12 | templateUrl: 'templates/dashboard.html', 13 | directives: [nvD3] 14 | }) 15 | export class DashboardComponent { 16 | stompClient : any; 17 | stats: PollStats[]; 18 | options; 19 | 20 | constructor(public http: Http) { 21 | } 22 | formatChartData = (stats) => { 23 | //we need to reformat the data a bit to use in pie chart 24 | for (var i=0;i< stats.length;i++){ 25 | var stat : any = this.stats[i]; 26 | stat.data = [] 27 | for (var j=0;j< stat.choices.length;j++) { 28 | let choice : ChoiceStats = stat.choices[j]; 29 | stat.data.push({ 30 | "key": choice.choice, 31 | "y": choice.totalVote 32 | }); 33 | } 34 | } 35 | console.log(stats); 36 | } 37 | stompAnswerSubmittedCallback = (message) => { 38 | this.stats = JSON.parse(message.body); 39 | this.formatChartData(this.stats); 40 | }; 41 | 42 | ngOnInit() { 43 | this.options = { 44 | "chart": { 45 | type: 'pieChart', 46 | height: 500, 47 | x: function(d){return d.key;}, 48 | y: function(d){return d.y;}, 49 | showLabels: true, 50 | duration: 500, 51 | labelThreshold: 0.01, 52 | labelSunbeamLayout: true, 53 | legend: { 54 | margin: { 55 | top: 5, 56 | right: 35, 57 | bottom: 5, 58 | left: 0 59 | } 60 | }, 61 | } 62 | } 63 | 64 | this.http.get("/api/poll/stats").subscribe( 65 | res => { 66 | this.stats = res.json(); 67 | this.formatChartData(this.stats); 68 | } 69 | ); 70 | 71 | //subscribe to the websocket 72 | this.connect(); 73 | } 74 | 75 | /** 76 | * Connect to SpringBoot Websocket 77 | */ 78 | connect() { 79 | let socket : any = new SockJS('/message'); 80 | this.stompClient = Stomp.over(socket); 81 | let stompConnect = (frame) => { 82 | //subscribe to /user/queue/polls if you only want messages for the current user 83 | this.stompClient.subscribe('/queue/answerSubmitted', this.stompAnswerSubmittedCallback); 84 | } 85 | 86 | this.stompClient.connect({}, stompConnect); 87 | } 88 | } -------------------------------------------------------------------------------- /src/main/resources/public/app/domain/poll.answer.ts: -------------------------------------------------------------------------------- 1 | import { PollChoice } from './poll.choice.domain.ts'; 2 | import { Poll } from './poll.domain.ts'; 3 | 4 | export class PollAnswer { 5 | id: number; 6 | user: String; 7 | pollChoice: PollChoice; //the question that was selected 8 | } -------------------------------------------------------------------------------- /src/main/resources/public/app/domain/poll.choice.domain.ts: -------------------------------------------------------------------------------- 1 | export class PollChoice { 2 | id: number; 3 | choice: string; 4 | } -------------------------------------------------------------------------------- /src/main/resources/public/app/domain/poll.domain.ts: -------------------------------------------------------------------------------- 1 | import { PollChoice } from './poll.choice.domain.ts'; 2 | 3 | export class Poll { 4 | id: number; 5 | name: String; 6 | choices : PollChoice[]; 7 | } -------------------------------------------------------------------------------- /src/main/resources/public/app/main.ts: -------------------------------------------------------------------------------- 1 | import { bootstrap } from '@angular/platform-browser-dynamic'; 2 | import {Http, HTTP_PROVIDERS} from '@angular/http'; 3 | import { Router,ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from '@angular/router'; 4 | import { AppComponent } from './app.component'; 5 | bootstrap(AppComponent,[ HTTP_PROVIDERS,ROUTER_PROVIDERS ]); -------------------------------------------------------------------------------- /src/main/resources/public/app/poll.component.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core'; 2 | import {Http, HTTP_PROVIDERS,Headers} from '@angular/http'; 3 | import { Observable } from 'rxjs/Observable'; 4 | import { Poll } from './domain/poll.domain'; 5 | import { PollChoice } from './domain/poll.choice.domain'; 6 | 7 | @Component({ 8 | selector: 'poll-form', 9 | templateUrl: 'templates/poll.html' 10 | }) 11 | export class PollComponent { 12 | @Input() poll : Poll; 13 | @Input() stompClient; 14 | selectedChoice : PollChoice; 15 | 16 | stompSelectChoiceCallback = (message) => { 17 | this.selectedChoice = JSON.parse(message.body); 18 | }; 19 | 20 | constructor(public http: Http) { 21 | } 22 | 23 | ngOnInit() { 24 | //subscribe to the websocket 25 | this.stompClient.subscribe('/queue/selectChoice', this.stompSelectChoiceCallback); 26 | } 27 | 28 | selectChoice(pollChoice : PollChoice) { 29 | this.stompClient.send('/websocket/selectChoice',{},JSON.stringify(pollChoice)); 30 | } 31 | 32 | submitPoll() { 33 | var headers : Headers = new Headers(); 34 | headers.append('Content-Type', 'application/json'); 35 | 36 | this.http.post("/api/poll/"+this.poll.id+"/submit",JSON.stringify(this.selectedChoice),{"headers": headers}) 37 | .subscribe( 38 | res => { 39 | let data = res.json(); 40 | } 41 | ); 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/resources/public/app/poll.list.component.ts: -------------------------------------------------------------------------------- 1 | import {Component,Input} from '@angular/core'; 2 | import {Http, HTTP_PROVIDERS} from '@angular/http'; 3 | import { Poll } from './domain/poll.domain'; 4 | import { PollComponent } from './poll.component'; 5 | import { ROUTER_DIRECTIVES} from '@angular/router'; 6 | 7 | declare var SockJS; 8 | declare var Stomp; 9 | 10 | @Component({ 11 | templateUrl: 'templates/poll.list.html', 12 | directives: [PollComponent, ROUTER_DIRECTIVES] 13 | }) 14 | export class PollListComponent { 15 | polls: Poll[]; 16 | currentPoll: Poll = null; 17 | stompClient : any; 18 | 19 | stompPollCallback = (message) => { 20 | this.polls = JSON.parse(message.body); 21 | }; 22 | 23 | stompSelectPollCallback = (message) => { 24 | this.currentPoll = JSON.parse(message.body); 25 | }; 26 | 27 | constructor(public http: Http) { 28 | 29 | } 30 | 31 | ngOnInit() { 32 | this.http.get("/api/poll").subscribe( 33 | res => { 34 | let data = res.json(); 35 | this.polls = data; 36 | }, 37 | err => { 38 | } 39 | ); 40 | 41 | //subscribe to the websocket 42 | this.connect(); 43 | } 44 | 45 | /** 46 | * Connect to SpringBoot Websocket 47 | */ 48 | connect() { 49 | let socket : any = new SockJS('/message'); 50 | this.stompClient = Stomp.over(socket); 51 | let stompConnect = (frame) => { 52 | let whoami : any = frame.headers['user-name']; 53 | //subscribe to /user/queue/polls if you only want messages for the current user 54 | this.stompClient.subscribe('/queue/polls', this.stompPollCallback); 55 | this.stompClient.subscribe('/queue/selectPoll', this.stompSelectPollCallback); 56 | } 57 | 58 | this.stompClient.connect({}, stompConnect); 59 | } 60 | 61 | showPoll(poll : Poll) { 62 | //this.currentPoll = poll; 63 | this.stompClient.send('/websocket/selectPoll',{},JSON.stringify(poll)); 64 | } 65 | } -------------------------------------------------------------------------------- /src/main/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Angular 2 QuickStart 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | Loading... 29 | 30 | -------------------------------------------------------------------------------- /src/main/resources/public/js/sockjs-0.3.4.js: -------------------------------------------------------------------------------- 1 | /* SockJS client, version 0.3.4, http://sockjs.org, MIT License 2 | 3 | Copyright (c) 2011-2012 VMware, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | 24 | // JSON2 by Douglas Crockford (minified). 25 | var JSON;JSON||(JSON={}),function(){function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c 1) { 70 | this._listeners[eventType] = arr.slice(0, idx).concat( arr.slice(idx+1) ); 71 | } else { 72 | delete this._listeners[eventType]; 73 | } 74 | return; 75 | } 76 | return; 77 | }; 78 | 79 | REventTarget.prototype.dispatchEvent = function (event) { 80 | var t = event.type; 81 | var args = Array.prototype.slice.call(arguments, 0); 82 | if (this['on'+t]) { 83 | this['on'+t].apply(this, args); 84 | } 85 | if (this._listeners && t in this._listeners) { 86 | for(var i=0; i < this._listeners[t].length; i++) { 87 | this._listeners[t][i].apply(this, args); 88 | } 89 | } 90 | }; 91 | // [*] End of lib/reventtarget.js 92 | 93 | 94 | // [*] Including lib/simpleevent.js 95 | /* 96 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 97 | * 98 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 99 | */ 100 | 101 | var SimpleEvent = function(type, obj) { 102 | this.type = type; 103 | if (typeof obj !== 'undefined') { 104 | for(var k in obj) { 105 | if (!obj.hasOwnProperty(k)) continue; 106 | this[k] = obj[k]; 107 | } 108 | } 109 | }; 110 | 111 | SimpleEvent.prototype.toString = function() { 112 | var r = []; 113 | for(var k in this) { 114 | if (!this.hasOwnProperty(k)) continue; 115 | var v = this[k]; 116 | if (typeof v === 'function') v = '[function]'; 117 | r.push(k + '=' + v); 118 | } 119 | return 'SimpleEvent(' + r.join(', ') + ')'; 120 | }; 121 | // [*] End of lib/simpleevent.js 122 | 123 | 124 | // [*] Including lib/eventemitter.js 125 | /* 126 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 127 | * 128 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 129 | */ 130 | 131 | var EventEmitter = function(events) { 132 | var that = this; 133 | that._events = events || []; 134 | that._listeners = {}; 135 | }; 136 | EventEmitter.prototype.emit = function(type) { 137 | var that = this; 138 | that._verifyType(type); 139 | if (that._nuked) return; 140 | 141 | var args = Array.prototype.slice.call(arguments, 1); 142 | if (that['on'+type]) { 143 | that['on'+type].apply(that, args); 144 | } 145 | if (type in that._listeners) { 146 | for(var i = 0; i < that._listeners[type].length; i++) { 147 | that._listeners[type][i].apply(that, args); 148 | } 149 | } 150 | }; 151 | 152 | EventEmitter.prototype.on = function(type, callback) { 153 | var that = this; 154 | that._verifyType(type); 155 | if (that._nuked) return; 156 | 157 | if (!(type in that._listeners)) { 158 | that._listeners[type] = []; 159 | } 160 | that._listeners[type].push(callback); 161 | }; 162 | 163 | EventEmitter.prototype._verifyType = function(type) { 164 | var that = this; 165 | if (utils.arrIndexOf(that._events, type) === -1) { 166 | utils.log('Event ' + JSON.stringify(type) + 167 | ' not listed ' + JSON.stringify(that._events) + 168 | ' in ' + that); 169 | } 170 | }; 171 | 172 | EventEmitter.prototype.nuke = function() { 173 | var that = this; 174 | that._nuked = true; 175 | for(var i=0; i= 3000 && code <= 4999); 259 | }; 260 | 261 | // See: http://www.erg.abdn.ac.uk/~gerrit/dccp/notes/ccid2/rto_estimator/ 262 | // and RFC 2988. 263 | utils.countRTO = function (rtt) { 264 | var rto; 265 | if (rtt > 100) { 266 | rto = 3 * rtt; // rto > 300msec 267 | } else { 268 | rto = rtt + 200; // 200msec < rto <= 300msec 269 | } 270 | return rto; 271 | } 272 | 273 | utils.log = function() { 274 | if (_window.console && console.log && console.log.apply) { 275 | console.log.apply(console, arguments); 276 | } 277 | }; 278 | 279 | utils.bind = function(fun, that) { 280 | if (fun.bind) { 281 | return fun.bind(that); 282 | } else { 283 | return function() { 284 | return fun.apply(that, arguments); 285 | }; 286 | } 287 | }; 288 | 289 | utils.flatUrl = function(url) { 290 | return url.indexOf('?') === -1 && url.indexOf('#') === -1; 291 | }; 292 | 293 | utils.amendUrl = function(url) { 294 | var dl = _document.location; 295 | if (!url) { 296 | throw new Error('Wrong url for SockJS'); 297 | } 298 | if (!utils.flatUrl(url)) { 299 | throw new Error('Only basic urls are supported in SockJS'); 300 | } 301 | 302 | // '//abc' --> 'http://abc' 303 | if (url.indexOf('//') === 0) { 304 | url = dl.protocol + url; 305 | } 306 | // '/abc' --> 'http://localhost:80/abc' 307 | if (url.indexOf('/') === 0) { 308 | url = dl.protocol + '//' + dl.host + url; 309 | } 310 | // strip trailing slashes 311 | url = url.replace(/[/]+$/,''); 312 | return url; 313 | }; 314 | 315 | // IE doesn't support [].indexOf. 316 | utils.arrIndexOf = function(arr, obj){ 317 | for(var i=0; i < arr.length; i++){ 318 | if(arr[i] === obj){ 319 | return i; 320 | } 321 | } 322 | return -1; 323 | }; 324 | 325 | utils.arrSkip = function(arr, obj) { 326 | var idx = utils.arrIndexOf(arr, obj); 327 | if (idx === -1) { 328 | return arr.slice(); 329 | } else { 330 | var dst = arr.slice(0, idx); 331 | return dst.concat(arr.slice(idx+1)); 332 | } 333 | }; 334 | 335 | // Via: https://gist.github.com/1133122/2121c601c5549155483f50be3da5305e83b8c5df 336 | utils.isArray = Array.isArray || function(value) { 337 | return {}.toString.call(value).indexOf('Array') >= 0 338 | }; 339 | 340 | utils.delay = function(t, fun) { 341 | if(typeof t === 'function') { 342 | fun = t; 343 | t = 0; 344 | } 345 | return setTimeout(fun, t); 346 | }; 347 | 348 | 349 | // Chars worth escaping, as defined by Douglas Crockford: 350 | // https://github.com/douglascrockford/JSON-js/blob/47a9882cddeb1e8529e07af9736218075372b8ac/json2.js#L196 351 | var json_escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 352 | json_lookup = { 353 | "\u0000":"\\u0000","\u0001":"\\u0001","\u0002":"\\u0002","\u0003":"\\u0003", 354 | "\u0004":"\\u0004","\u0005":"\\u0005","\u0006":"\\u0006","\u0007":"\\u0007", 355 | "\b":"\\b","\t":"\\t","\n":"\\n","\u000b":"\\u000b","\f":"\\f","\r":"\\r", 356 | "\u000e":"\\u000e","\u000f":"\\u000f","\u0010":"\\u0010","\u0011":"\\u0011", 357 | "\u0012":"\\u0012","\u0013":"\\u0013","\u0014":"\\u0014","\u0015":"\\u0015", 358 | "\u0016":"\\u0016","\u0017":"\\u0017","\u0018":"\\u0018","\u0019":"\\u0019", 359 | "\u001a":"\\u001a","\u001b":"\\u001b","\u001c":"\\u001c","\u001d":"\\u001d", 360 | "\u001e":"\\u001e","\u001f":"\\u001f","\"":"\\\"","\\":"\\\\", 361 | "\u007f":"\\u007f","\u0080":"\\u0080","\u0081":"\\u0081","\u0082":"\\u0082", 362 | "\u0083":"\\u0083","\u0084":"\\u0084","\u0085":"\\u0085","\u0086":"\\u0086", 363 | "\u0087":"\\u0087","\u0088":"\\u0088","\u0089":"\\u0089","\u008a":"\\u008a", 364 | "\u008b":"\\u008b","\u008c":"\\u008c","\u008d":"\\u008d","\u008e":"\\u008e", 365 | "\u008f":"\\u008f","\u0090":"\\u0090","\u0091":"\\u0091","\u0092":"\\u0092", 366 | "\u0093":"\\u0093","\u0094":"\\u0094","\u0095":"\\u0095","\u0096":"\\u0096", 367 | "\u0097":"\\u0097","\u0098":"\\u0098","\u0099":"\\u0099","\u009a":"\\u009a", 368 | "\u009b":"\\u009b","\u009c":"\\u009c","\u009d":"\\u009d","\u009e":"\\u009e", 369 | "\u009f":"\\u009f","\u00ad":"\\u00ad","\u0600":"\\u0600","\u0601":"\\u0601", 370 | "\u0602":"\\u0602","\u0603":"\\u0603","\u0604":"\\u0604","\u070f":"\\u070f", 371 | "\u17b4":"\\u17b4","\u17b5":"\\u17b5","\u200c":"\\u200c","\u200d":"\\u200d", 372 | "\u200e":"\\u200e","\u200f":"\\u200f","\u2028":"\\u2028","\u2029":"\\u2029", 373 | "\u202a":"\\u202a","\u202b":"\\u202b","\u202c":"\\u202c","\u202d":"\\u202d", 374 | "\u202e":"\\u202e","\u202f":"\\u202f","\u2060":"\\u2060","\u2061":"\\u2061", 375 | "\u2062":"\\u2062","\u2063":"\\u2063","\u2064":"\\u2064","\u2065":"\\u2065", 376 | "\u2066":"\\u2066","\u2067":"\\u2067","\u2068":"\\u2068","\u2069":"\\u2069", 377 | "\u206a":"\\u206a","\u206b":"\\u206b","\u206c":"\\u206c","\u206d":"\\u206d", 378 | "\u206e":"\\u206e","\u206f":"\\u206f","\ufeff":"\\ufeff","\ufff0":"\\ufff0", 379 | "\ufff1":"\\ufff1","\ufff2":"\\ufff2","\ufff3":"\\ufff3","\ufff4":"\\ufff4", 380 | "\ufff5":"\\ufff5","\ufff6":"\\ufff6","\ufff7":"\\ufff7","\ufff8":"\\ufff8", 381 | "\ufff9":"\\ufff9","\ufffa":"\\ufffa","\ufffb":"\\ufffb","\ufffc":"\\ufffc", 382 | "\ufffd":"\\ufffd","\ufffe":"\\ufffe","\uffff":"\\uffff"}; 383 | 384 | // Some extra characters that Chrome gets wrong, and substitutes with 385 | // something else on the wire. 386 | var extra_escapable = /[\x00-\x1f\ud800-\udfff\ufffe\uffff\u0300-\u0333\u033d-\u0346\u034a-\u034c\u0350-\u0352\u0357-\u0358\u035c-\u0362\u0374\u037e\u0387\u0591-\u05af\u05c4\u0610-\u0617\u0653-\u0654\u0657-\u065b\u065d-\u065e\u06df-\u06e2\u06eb-\u06ec\u0730\u0732-\u0733\u0735-\u0736\u073a\u073d\u073f-\u0741\u0743\u0745\u0747\u07eb-\u07f1\u0951\u0958-\u095f\u09dc-\u09dd\u09df\u0a33\u0a36\u0a59-\u0a5b\u0a5e\u0b5c-\u0b5d\u0e38-\u0e39\u0f43\u0f4d\u0f52\u0f57\u0f5c\u0f69\u0f72-\u0f76\u0f78\u0f80-\u0f83\u0f93\u0f9d\u0fa2\u0fa7\u0fac\u0fb9\u1939-\u193a\u1a17\u1b6b\u1cda-\u1cdb\u1dc0-\u1dcf\u1dfc\u1dfe\u1f71\u1f73\u1f75\u1f77\u1f79\u1f7b\u1f7d\u1fbb\u1fbe\u1fc9\u1fcb\u1fd3\u1fdb\u1fe3\u1feb\u1fee-\u1fef\u1ff9\u1ffb\u1ffd\u2000-\u2001\u20d0-\u20d1\u20d4-\u20d7\u20e7-\u20e9\u2126\u212a-\u212b\u2329-\u232a\u2adc\u302b-\u302c\uaab2-\uaab3\uf900-\ufa0d\ufa10\ufa12\ufa15-\ufa1e\ufa20\ufa22\ufa25-\ufa26\ufa2a-\ufa2d\ufa30-\ufa6d\ufa70-\ufad9\ufb1d\ufb1f\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4e\ufff0-\uffff]/g, 387 | extra_lookup; 388 | 389 | // JSON Quote string. Use native implementation when possible. 390 | var JSONQuote = (JSON && JSON.stringify) || function(string) { 391 | json_escapable.lastIndex = 0; 392 | if (json_escapable.test(string)) { 393 | string = string.replace(json_escapable, function(a) { 394 | return json_lookup[a]; 395 | }); 396 | } 397 | return '"' + string + '"'; 398 | }; 399 | 400 | // This may be quite slow, so let's delay until user actually uses bad 401 | // characters. 402 | var unroll_lookup = function(escapable) { 403 | var i; 404 | var unrolled = {} 405 | var c = [] 406 | for(i=0; i<65536; i++) { 407 | c.push( String.fromCharCode(i) ); 408 | } 409 | escapable.lastIndex = 0; 410 | c.join('').replace(escapable, function (a) { 411 | unrolled[ a ] = '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 412 | return ''; 413 | }); 414 | escapable.lastIndex = 0; 415 | return unrolled; 416 | }; 417 | 418 | // Quote string, also taking care of unicode characters that browsers 419 | // often break. Especially, take care of unicode surrogates: 420 | // http://en.wikipedia.org/wiki/Mapping_of_Unicode_characters#Surrogates 421 | utils.quote = function(string) { 422 | var quoted = JSONQuote(string); 423 | 424 | // In most cases this should be very fast and good enough. 425 | extra_escapable.lastIndex = 0; 426 | if(!extra_escapable.test(quoted)) { 427 | return quoted; 428 | } 429 | 430 | if(!extra_lookup) extra_lookup = unroll_lookup(extra_escapable); 431 | 432 | return quoted.replace(extra_escapable, function(a) { 433 | return extra_lookup[a]; 434 | }); 435 | } 436 | 437 | var _all_protocols = ['websocket', 438 | 'xdr-streaming', 439 | 'xhr-streaming', 440 | 'iframe-eventsource', 441 | 'iframe-htmlfile', 442 | 'xdr-polling', 443 | 'xhr-polling', 444 | 'iframe-xhr-polling', 445 | 'jsonp-polling']; 446 | 447 | utils.probeProtocols = function() { 448 | var probed = {}; 449 | for(var i=0; i<_all_protocols.length; i++) { 450 | var protocol = _all_protocols[i]; 451 | // User can have a typo in protocol name. 452 | probed[protocol] = SockJS[protocol] && 453 | SockJS[protocol].enabled(); 454 | } 455 | return probed; 456 | }; 457 | 458 | utils.detectProtocols = function(probed, protocols_whitelist, info) { 459 | var pe = {}, 460 | protocols = []; 461 | if (!protocols_whitelist) protocols_whitelist = _all_protocols; 462 | for(var i=0; i 0) { 472 | maybe_push(protos); 473 | } 474 | } 475 | } 476 | 477 | // 1. Websocket 478 | if (info.websocket !== false) { 479 | maybe_push(['websocket']); 480 | } 481 | 482 | // 2. Streaming 483 | if (pe['xhr-streaming'] && !info.null_origin) { 484 | protocols.push('xhr-streaming'); 485 | } else { 486 | if (pe['xdr-streaming'] && !info.cookie_needed && !info.null_origin) { 487 | protocols.push('xdr-streaming'); 488 | } else { 489 | maybe_push(['iframe-eventsource', 490 | 'iframe-htmlfile']); 491 | } 492 | } 493 | 494 | // 3. Polling 495 | if (pe['xhr-polling'] && !info.null_origin) { 496 | protocols.push('xhr-polling'); 497 | } else { 498 | if (pe['xdr-polling'] && !info.cookie_needed && !info.null_origin) { 499 | protocols.push('xdr-polling'); 500 | } else { 501 | maybe_push(['iframe-xhr-polling', 502 | 'jsonp-polling']); 503 | } 504 | } 505 | return protocols; 506 | } 507 | // [*] End of lib/utils.js 508 | 509 | 510 | // [*] Including lib/dom.js 511 | /* 512 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 513 | * 514 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 515 | */ 516 | 517 | // May be used by htmlfile jsonp and transports. 518 | var MPrefix = '_sockjs_global'; 519 | utils.createHook = function() { 520 | var window_id = 'a' + utils.random_string(8); 521 | if (!(MPrefix in _window)) { 522 | var map = {}; 523 | _window[MPrefix] = function(window_id) { 524 | if (!(window_id in map)) { 525 | map[window_id] = { 526 | id: window_id, 527 | del: function() {delete map[window_id];} 528 | }; 529 | } 530 | return map[window_id]; 531 | } 532 | } 533 | return _window[MPrefix](window_id); 534 | }; 535 | 536 | 537 | 538 | utils.attachMessage = function(listener) { 539 | utils.attachEvent('message', listener); 540 | }; 541 | utils.attachEvent = function(event, listener) { 542 | if (typeof _window.addEventListener !== 'undefined') { 543 | _window.addEventListener(event, listener, false); 544 | } else { 545 | // IE quirks. 546 | // According to: http://stevesouders.com/misc/test-postmessage.php 547 | // the message gets delivered only to 'document', not 'window'. 548 | _document.attachEvent("on" + event, listener); 549 | // I get 'window' for ie8. 550 | _window.attachEvent("on" + event, listener); 551 | } 552 | }; 553 | 554 | utils.detachMessage = function(listener) { 555 | utils.detachEvent('message', listener); 556 | }; 557 | utils.detachEvent = function(event, listener) { 558 | if (typeof _window.addEventListener !== 'undefined') { 559 | _window.removeEventListener(event, listener, false); 560 | } else { 561 | _document.detachEvent("on" + event, listener); 562 | _window.detachEvent("on" + event, listener); 563 | } 564 | }; 565 | 566 | 567 | var on_unload = {}; 568 | // Things registered after beforeunload are to be called immediately. 569 | var after_unload = false; 570 | 571 | var trigger_unload_callbacks = function() { 572 | for(var ref in on_unload) { 573 | on_unload[ref](); 574 | delete on_unload[ref]; 575 | }; 576 | }; 577 | 578 | var unload_triggered = function() { 579 | if(after_unload) return; 580 | after_unload = true; 581 | trigger_unload_callbacks(); 582 | }; 583 | 584 | // 'unload' alone is not reliable in opera within an iframe, but we 585 | // can't use `beforeunload` as IE fires it on javascript: links. 586 | utils.attachEvent('unload', unload_triggered); 587 | 588 | utils.unload_add = function(listener) { 589 | var ref = utils.random_string(8); 590 | on_unload[ref] = listener; 591 | if (after_unload) { 592 | utils.delay(trigger_unload_callbacks); 593 | } 594 | return ref; 595 | }; 596 | utils.unload_del = function(ref) { 597 | if (ref in on_unload) 598 | delete on_unload[ref]; 599 | }; 600 | 601 | 602 | utils.createIframe = function (iframe_url, error_callback) { 603 | var iframe = _document.createElement('iframe'); 604 | var tref, unload_ref; 605 | var unattach = function() { 606 | clearTimeout(tref); 607 | // Explorer had problems with that. 608 | try {iframe.onload = null;} catch (x) {} 609 | iframe.onerror = null; 610 | }; 611 | var cleanup = function() { 612 | if (iframe) { 613 | unattach(); 614 | // This timeout makes chrome fire onbeforeunload event 615 | // within iframe. Without the timeout it goes straight to 616 | // onunload. 617 | setTimeout(function() { 618 | if(iframe) { 619 | iframe.parentNode.removeChild(iframe); 620 | } 621 | iframe = null; 622 | }, 0); 623 | utils.unload_del(unload_ref); 624 | } 625 | }; 626 | var onerror = function(r) { 627 | if (iframe) { 628 | cleanup(); 629 | error_callback(r); 630 | } 631 | }; 632 | var post = function(msg, origin) { 633 | try { 634 | // When the iframe is not loaded, IE raises an exception 635 | // on 'contentWindow'. 636 | if (iframe && iframe.contentWindow) { 637 | iframe.contentWindow.postMessage(msg, origin); 638 | } 639 | } catch (x) {}; 640 | }; 641 | 642 | iframe.src = iframe_url; 643 | iframe.style.display = 'none'; 644 | iframe.style.position = 'absolute'; 645 | iframe.onerror = function(){onerror('onerror');}; 646 | iframe.onload = function() { 647 | // `onload` is triggered before scripts on the iframe are 648 | // executed. Give it few seconds to actually load stuff. 649 | clearTimeout(tref); 650 | tref = setTimeout(function(){onerror('onload timeout');}, 2000); 651 | }; 652 | _document.body.appendChild(iframe); 653 | tref = setTimeout(function(){onerror('timeout');}, 15000); 654 | unload_ref = utils.unload_add(cleanup); 655 | return { 656 | post: post, 657 | cleanup: cleanup, 658 | loaded: unattach 659 | }; 660 | }; 661 | 662 | utils.createHtmlfile = function (iframe_url, error_callback) { 663 | var doc = new ActiveXObject('htmlfile'); 664 | var tref, unload_ref; 665 | var iframe; 666 | var unattach = function() { 667 | clearTimeout(tref); 668 | }; 669 | var cleanup = function() { 670 | if (doc) { 671 | unattach(); 672 | utils.unload_del(unload_ref); 673 | iframe.parentNode.removeChild(iframe); 674 | iframe = doc = null; 675 | CollectGarbage(); 676 | } 677 | }; 678 | var onerror = function(r) { 679 | if (doc) { 680 | cleanup(); 681 | error_callback(r); 682 | } 683 | }; 684 | var post = function(msg, origin) { 685 | try { 686 | // When the iframe is not loaded, IE raises an exception 687 | // on 'contentWindow'. 688 | if (iframe && iframe.contentWindow) { 689 | iframe.contentWindow.postMessage(msg, origin); 690 | } 691 | } catch (x) {}; 692 | }; 693 | 694 | doc.open(); 695 | doc.write('' + 696 | 'document.domain="' + document.domain + '";' + 697 | ''); 698 | doc.close(); 699 | doc.parentWindow[WPrefix] = _window[WPrefix]; 700 | var c = doc.createElement('div'); 701 | doc.body.appendChild(c); 702 | iframe = doc.createElement('iframe'); 703 | c.appendChild(iframe); 704 | iframe.src = iframe_url; 705 | tref = setTimeout(function(){onerror('timeout');}, 15000); 706 | unload_ref = utils.unload_add(cleanup); 707 | return { 708 | post: post, 709 | cleanup: cleanup, 710 | loaded: unattach 711 | }; 712 | }; 713 | // [*] End of lib/dom.js 714 | 715 | 716 | // [*] Including lib/dom2.js 717 | /* 718 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 719 | * 720 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 721 | */ 722 | 723 | var AbstractXHRObject = function(){}; 724 | AbstractXHRObject.prototype = new EventEmitter(['chunk', 'finish']); 725 | 726 | AbstractXHRObject.prototype._start = function(method, url, payload, opts) { 727 | var that = this; 728 | 729 | try { 730 | that.xhr = new XMLHttpRequest(); 731 | } catch(x) {}; 732 | 733 | if (!that.xhr) { 734 | try { 735 | that.xhr = new _window.ActiveXObject('Microsoft.XMLHTTP'); 736 | } catch(x) {}; 737 | } 738 | if (_window.ActiveXObject || _window.XDomainRequest) { 739 | // IE8 caches even POSTs 740 | url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date); 741 | } 742 | 743 | // Explorer tends to keep connection open, even after the 744 | // tab gets closed: http://bugs.jquery.com/ticket/5280 745 | that.unload_ref = utils.unload_add(function(){that._cleanup(true);}); 746 | try { 747 | that.xhr.open(method, url, true); 748 | } catch(e) { 749 | // IE raises an exception on wrong port. 750 | that.emit('finish', 0, ''); 751 | that._cleanup(); 752 | return; 753 | }; 754 | 755 | if (!opts || !opts.no_credentials) { 756 | // Mozilla docs says https://developer.mozilla.org/en/XMLHttpRequest : 757 | // "This never affects same-site requests." 758 | that.xhr.withCredentials = 'true'; 759 | } 760 | if (opts && opts.headers) { 761 | for(var key in opts.headers) { 762 | that.xhr.setRequestHeader(key, opts.headers[key]); 763 | } 764 | } 765 | 766 | that.xhr.onreadystatechange = function() { 767 | if (that.xhr) { 768 | var x = that.xhr; 769 | switch (x.readyState) { 770 | case 3: 771 | // IE doesn't like peeking into responseText or status 772 | // on Microsoft.XMLHTTP and readystate=3 773 | try { 774 | var status = x.status; 775 | var text = x.responseText; 776 | } catch (x) {}; 777 | // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 778 | if (status === 1223) status = 204; 779 | 780 | // IE does return readystate == 3 for 404 answers. 781 | if (text && text.length > 0) { 782 | that.emit('chunk', status, text); 783 | } 784 | break; 785 | case 4: 786 | var status = x.status; 787 | // IE returns 1223 for 204: http://bugs.jquery.com/ticket/1450 788 | if (status === 1223) status = 204; 789 | 790 | that.emit('finish', status, x.responseText); 791 | that._cleanup(false); 792 | break; 793 | } 794 | } 795 | }; 796 | that.xhr.send(payload); 797 | }; 798 | 799 | AbstractXHRObject.prototype._cleanup = function(abort) { 800 | var that = this; 801 | if (!that.xhr) return; 802 | utils.unload_del(that.unload_ref); 803 | 804 | // IE needs this field to be a function 805 | that.xhr.onreadystatechange = function(){}; 806 | 807 | if (abort) { 808 | try { 809 | that.xhr.abort(); 810 | } catch(x) {}; 811 | } 812 | that.unload_ref = that.xhr = null; 813 | }; 814 | 815 | AbstractXHRObject.prototype.close = function() { 816 | var that = this; 817 | that.nuke(); 818 | that._cleanup(true); 819 | }; 820 | 821 | var XHRCorsObject = utils.XHRCorsObject = function() { 822 | var that = this, args = arguments; 823 | utils.delay(function(){that._start.apply(that, args);}); 824 | }; 825 | XHRCorsObject.prototype = new AbstractXHRObject(); 826 | 827 | var XHRLocalObject = utils.XHRLocalObject = function(method, url, payload) { 828 | var that = this; 829 | utils.delay(function(){ 830 | that._start(method, url, payload, { 831 | no_credentials: true 832 | }); 833 | }); 834 | }; 835 | XHRLocalObject.prototype = new AbstractXHRObject(); 836 | 837 | 838 | 839 | // References: 840 | // http://ajaxian.com/archives/100-line-ajax-wrapper 841 | // http://msdn.microsoft.com/en-us/library/cc288060(v=VS.85).aspx 842 | var XDRObject = utils.XDRObject = function(method, url, payload) { 843 | var that = this; 844 | utils.delay(function(){that._start(method, url, payload);}); 845 | }; 846 | XDRObject.prototype = new EventEmitter(['chunk', 'finish']); 847 | XDRObject.prototype._start = function(method, url, payload) { 848 | var that = this; 849 | var xdr = new XDomainRequest(); 850 | // IE caches even POSTs 851 | url += ((url.indexOf('?') === -1) ? '?' : '&') + 't='+(+new Date); 852 | 853 | var onerror = xdr.ontimeout = xdr.onerror = function() { 854 | that.emit('finish', 0, ''); 855 | that._cleanup(false); 856 | }; 857 | xdr.onprogress = function() { 858 | that.emit('chunk', 200, xdr.responseText); 859 | }; 860 | xdr.onload = function() { 861 | that.emit('finish', 200, xdr.responseText); 862 | that._cleanup(false); 863 | }; 864 | that.xdr = xdr; 865 | that.unload_ref = utils.unload_add(function(){that._cleanup(true);}); 866 | try { 867 | // Fails with AccessDenied if port number is bogus 868 | that.xdr.open(method, url); 869 | that.xdr.send(payload); 870 | } catch(x) { 871 | onerror(); 872 | } 873 | }; 874 | 875 | XDRObject.prototype._cleanup = function(abort) { 876 | var that = this; 877 | if (!that.xdr) return; 878 | utils.unload_del(that.unload_ref); 879 | 880 | that.xdr.ontimeout = that.xdr.onerror = that.xdr.onprogress = 881 | that.xdr.onload = null; 882 | if (abort) { 883 | try { 884 | that.xdr.abort(); 885 | } catch(x) {}; 886 | } 887 | that.unload_ref = that.xdr = null; 888 | }; 889 | 890 | XDRObject.prototype.close = function() { 891 | var that = this; 892 | that.nuke(); 893 | that._cleanup(true); 894 | }; 895 | 896 | // 1. Is natively via XHR 897 | // 2. Is natively via XDR 898 | // 3. Nope, but postMessage is there so it should work via the Iframe. 899 | // 4. Nope, sorry. 900 | utils.isXHRCorsCapable = function() { 901 | if (_window.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()) { 902 | return 1; 903 | } 904 | // XDomainRequest doesn't work if page is served from file:// 905 | if (_window.XDomainRequest && _document.domain) { 906 | return 2; 907 | } 908 | if (IframeTransport.enabled()) { 909 | return 3; 910 | } 911 | return 4; 912 | }; 913 | // [*] End of lib/dom2.js 914 | 915 | 916 | // [*] Including lib/sockjs.js 917 | /* 918 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 919 | * 920 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 921 | */ 922 | 923 | var SockJS = function(url, dep_protocols_whitelist, options) { 924 | if (this === _window) { 925 | // makes `new` optional 926 | return new SockJS(url, dep_protocols_whitelist, options); 927 | } 928 | 929 | var that = this, protocols_whitelist; 930 | that._options = {devel: false, debug: false, protocols_whitelist: [], 931 | info: undefined, rtt: undefined}; 932 | if (options) { 933 | utils.objectExtend(that._options, options); 934 | } 935 | that._base_url = utils.amendUrl(url); 936 | that._server = that._options.server || utils.random_number_string(1000); 937 | if (that._options.protocols_whitelist && 938 | that._options.protocols_whitelist.length) { 939 | protocols_whitelist = that._options.protocols_whitelist; 940 | } else { 941 | // Deprecated API 942 | if (typeof dep_protocols_whitelist === 'string' && 943 | dep_protocols_whitelist.length > 0) { 944 | protocols_whitelist = [dep_protocols_whitelist]; 945 | } else if (utils.isArray(dep_protocols_whitelist)) { 946 | protocols_whitelist = dep_protocols_whitelist 947 | } else { 948 | protocols_whitelist = null; 949 | } 950 | if (protocols_whitelist) { 951 | that._debug('Deprecated API: Use "protocols_whitelist" option ' + 952 | 'instead of supplying protocol list as a second ' + 953 | 'parameter to SockJS constructor.'); 954 | } 955 | } 956 | that._protocols = []; 957 | that.protocol = null; 958 | that.readyState = SockJS.CONNECTING; 959 | that._ir = createInfoReceiver(that._base_url); 960 | that._ir.onfinish = function(info, rtt) { 961 | that._ir = null; 962 | if (info) { 963 | if (that._options.info) { 964 | // Override if user supplies the option 965 | info = utils.objectExtend(info, that._options.info); 966 | } 967 | if (that._options.rtt) { 968 | rtt = that._options.rtt; 969 | } 970 | that._applyInfo(info, rtt, protocols_whitelist); 971 | that._didClose(); 972 | } else { 973 | that._didClose(1002, 'Can\'t connect to server', true); 974 | } 975 | }; 976 | }; 977 | // Inheritance 978 | SockJS.prototype = new REventTarget(); 979 | 980 | SockJS.version = "0.3.4"; 981 | 982 | SockJS.CONNECTING = 0; 983 | SockJS.OPEN = 1; 984 | SockJS.CLOSING = 2; 985 | SockJS.CLOSED = 3; 986 | 987 | SockJS.prototype._debug = function() { 988 | if (this._options.debug) 989 | utils.log.apply(utils, arguments); 990 | }; 991 | 992 | SockJS.prototype._dispatchOpen = function() { 993 | var that = this; 994 | if (that.readyState === SockJS.CONNECTING) { 995 | if (that._transport_tref) { 996 | clearTimeout(that._transport_tref); 997 | that._transport_tref = null; 998 | } 999 | that.readyState = SockJS.OPEN; 1000 | that.dispatchEvent(new SimpleEvent("open")); 1001 | } else { 1002 | // The server might have been restarted, and lost track of our 1003 | // connection. 1004 | that._didClose(1006, "Server lost session"); 1005 | } 1006 | }; 1007 | 1008 | SockJS.prototype._dispatchMessage = function(data) { 1009 | var that = this; 1010 | if (that.readyState !== SockJS.OPEN) 1011 | return; 1012 | that.dispatchEvent(new SimpleEvent("message", {data: data})); 1013 | }; 1014 | 1015 | SockJS.prototype._dispatchHeartbeat = function(data) { 1016 | var that = this; 1017 | if (that.readyState !== SockJS.OPEN) 1018 | return; 1019 | that.dispatchEvent(new SimpleEvent('heartbeat', {})); 1020 | }; 1021 | 1022 | SockJS.prototype._didClose = function(code, reason, force) { 1023 | var that = this; 1024 | if (that.readyState !== SockJS.CONNECTING && 1025 | that.readyState !== SockJS.OPEN && 1026 | that.readyState !== SockJS.CLOSING) 1027 | throw new Error('INVALID_STATE_ERR'); 1028 | if (that._ir) { 1029 | that._ir.nuke(); 1030 | that._ir = null; 1031 | } 1032 | 1033 | if (that._transport) { 1034 | that._transport.doCleanup(); 1035 | that._transport = null; 1036 | } 1037 | 1038 | var close_event = new SimpleEvent("close", { 1039 | code: code, 1040 | reason: reason, 1041 | wasClean: utils.userSetCode(code)}); 1042 | 1043 | if (!utils.userSetCode(code) && 1044 | that.readyState === SockJS.CONNECTING && !force) { 1045 | if (that._try_next_protocol(close_event)) { 1046 | return; 1047 | } 1048 | close_event = new SimpleEvent("close", {code: 2000, 1049 | reason: "All transports failed", 1050 | wasClean: false, 1051 | last_event: close_event}); 1052 | } 1053 | that.readyState = SockJS.CLOSED; 1054 | 1055 | utils.delay(function() { 1056 | that.dispatchEvent(close_event); 1057 | }); 1058 | }; 1059 | 1060 | SockJS.prototype._didMessage = function(data) { 1061 | var that = this; 1062 | var type = data.slice(0, 1); 1063 | switch(type) { 1064 | case 'o': 1065 | that._dispatchOpen(); 1066 | break; 1067 | case 'a': 1068 | var payload = JSON.parse(data.slice(1) || '[]'); 1069 | for(var i=0; i < payload.length; i++){ 1070 | that._dispatchMessage(payload[i]); 1071 | } 1072 | break; 1073 | case 'm': 1074 | var payload = JSON.parse(data.slice(1) || 'null'); 1075 | that._dispatchMessage(payload); 1076 | break; 1077 | case 'c': 1078 | var payload = JSON.parse(data.slice(1) || '[]'); 1079 | that._didClose(payload[0], payload[1]); 1080 | break; 1081 | case 'h': 1082 | that._dispatchHeartbeat(); 1083 | break; 1084 | } 1085 | }; 1086 | 1087 | SockJS.prototype._try_next_protocol = function(close_event) { 1088 | var that = this; 1089 | if (that.protocol) { 1090 | that._debug('Closed transport:', that.protocol, ''+close_event); 1091 | that.protocol = null; 1092 | } 1093 | if (that._transport_tref) { 1094 | clearTimeout(that._transport_tref); 1095 | that._transport_tref = null; 1096 | } 1097 | 1098 | while(1) { 1099 | var protocol = that.protocol = that._protocols.shift(); 1100 | if (!protocol) { 1101 | return false; 1102 | } 1103 | // Some protocols require access to `body`, what if were in 1104 | // the `head`? 1105 | if (SockJS[protocol] && 1106 | SockJS[protocol].need_body === true && 1107 | (!_document.body || 1108 | (typeof _document.readyState !== 'undefined' 1109 | && _document.readyState !== 'complete'))) { 1110 | that._protocols.unshift(protocol); 1111 | that.protocol = 'waiting-for-load'; 1112 | utils.attachEvent('load', function(){ 1113 | that._try_next_protocol(); 1114 | }); 1115 | return true; 1116 | } 1117 | 1118 | if (!SockJS[protocol] || 1119 | !SockJS[protocol].enabled(that._options)) { 1120 | that._debug('Skipping transport:', protocol); 1121 | } else { 1122 | var roundTrips = SockJS[protocol].roundTrips || 1; 1123 | var to = ((that._options.rto || 0) * roundTrips) || 5000; 1124 | that._transport_tref = utils.delay(to, function() { 1125 | if (that.readyState === SockJS.CONNECTING) { 1126 | // I can't understand how it is possible to run 1127 | // this timer, when the state is CLOSED, but 1128 | // apparently in IE everythin is possible. 1129 | that._didClose(2007, "Transport timeouted"); 1130 | } 1131 | }); 1132 | 1133 | var connid = utils.random_string(8); 1134 | var trans_url = that._base_url + '/' + that._server + '/' + connid; 1135 | that._debug('Opening transport:', protocol, ' url:'+trans_url, 1136 | ' RTO:'+that._options.rto); 1137 | that._transport = new SockJS[protocol](that, trans_url, 1138 | that._base_url); 1139 | return true; 1140 | } 1141 | } 1142 | }; 1143 | 1144 | SockJS.prototype.close = function(code, reason) { 1145 | var that = this; 1146 | if (code && !utils.userSetCode(code)) 1147 | throw new Error("INVALID_ACCESS_ERR"); 1148 | if(that.readyState !== SockJS.CONNECTING && 1149 | that.readyState !== SockJS.OPEN) { 1150 | return false; 1151 | } 1152 | that.readyState = SockJS.CLOSING; 1153 | that._didClose(code || 1000, reason || "Normal closure"); 1154 | return true; 1155 | }; 1156 | 1157 | SockJS.prototype.send = function(data) { 1158 | var that = this; 1159 | if (that.readyState === SockJS.CONNECTING) 1160 | throw new Error('INVALID_STATE_ERR'); 1161 | if (that.readyState === SockJS.OPEN) { 1162 | that._transport.doSend(utils.quote('' + data)); 1163 | } 1164 | return true; 1165 | }; 1166 | 1167 | SockJS.prototype._applyInfo = function(info, rtt, protocols_whitelist) { 1168 | var that = this; 1169 | that._options.info = info; 1170 | that._options.rtt = rtt; 1171 | that._options.rto = utils.countRTO(rtt); 1172 | that._options.info.null_origin = !_document.domain; 1173 | var probed = utils.probeProtocols(); 1174 | that._protocols = utils.detectProtocols(probed, protocols_whitelist, info); 1175 | }; 1176 | // [*] End of lib/sockjs.js 1177 | 1178 | 1179 | // [*] Including lib/trans-websocket.js 1180 | /* 1181 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1182 | * 1183 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1184 | */ 1185 | 1186 | var WebSocketTransport = SockJS.websocket = function(ri, trans_url) { 1187 | var that = this; 1188 | var url = trans_url + '/websocket'; 1189 | if (url.slice(0, 5) === 'https') { 1190 | url = 'wss' + url.slice(5); 1191 | } else { 1192 | url = 'ws' + url.slice(4); 1193 | } 1194 | that.ri = ri; 1195 | that.url = url; 1196 | var Constructor = _window.WebSocket || _window.MozWebSocket; 1197 | 1198 | that.ws = new Constructor(that.url); 1199 | that.ws.onmessage = function(e) { 1200 | that.ri._didMessage(e.data); 1201 | }; 1202 | // Firefox has an interesting bug. If a websocket connection is 1203 | // created after onunload, it stays alive even when user 1204 | // navigates away from the page. In such situation let's lie - 1205 | // let's not open the ws connection at all. See: 1206 | // https://github.com/sockjs/sockjs-client/issues/28 1207 | // https://bugzilla.mozilla.org/show_bug.cgi?id=696085 1208 | that.unload_ref = utils.unload_add(function(){that.ws.close()}); 1209 | that.ws.onclose = function() { 1210 | that.ri._didMessage(utils.closeFrame(1006, "WebSocket connection broken")); 1211 | }; 1212 | }; 1213 | 1214 | WebSocketTransport.prototype.doSend = function(data) { 1215 | this.ws.send('[' + data + ']'); 1216 | }; 1217 | 1218 | WebSocketTransport.prototype.doCleanup = function() { 1219 | var that = this; 1220 | var ws = that.ws; 1221 | if (ws) { 1222 | ws.onmessage = ws.onclose = null; 1223 | ws.close(); 1224 | utils.unload_del(that.unload_ref); 1225 | that.unload_ref = that.ri = that.ws = null; 1226 | } 1227 | }; 1228 | 1229 | WebSocketTransport.enabled = function() { 1230 | return !!(_window.WebSocket || _window.MozWebSocket); 1231 | }; 1232 | 1233 | // In theory, ws should require 1 round trip. But in chrome, this is 1234 | // not very stable over SSL. Most likely a ws connection requires a 1235 | // separate SSL connection, in which case 2 round trips are an 1236 | // absolute minumum. 1237 | WebSocketTransport.roundTrips = 2; 1238 | // [*] End of lib/trans-websocket.js 1239 | 1240 | 1241 | // [*] Including lib/trans-sender.js 1242 | /* 1243 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1244 | * 1245 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1246 | */ 1247 | 1248 | var BufferedSender = function() {}; 1249 | BufferedSender.prototype.send_constructor = function(sender) { 1250 | var that = this; 1251 | that.send_buffer = []; 1252 | that.sender = sender; 1253 | }; 1254 | BufferedSender.prototype.doSend = function(message) { 1255 | var that = this; 1256 | that.send_buffer.push(message); 1257 | if (!that.send_stop) { 1258 | that.send_schedule(); 1259 | } 1260 | }; 1261 | 1262 | // For polling transports in a situation when in the message callback, 1263 | // new message is being send. If the sending connection was started 1264 | // before receiving one, it is possible to saturate the network and 1265 | // timeout due to the lack of receiving socket. To avoid that we delay 1266 | // sending messages by some small time, in order to let receiving 1267 | // connection be started beforehand. This is only a halfmeasure and 1268 | // does not fix the big problem, but it does make the tests go more 1269 | // stable on slow networks. 1270 | BufferedSender.prototype.send_schedule_wait = function() { 1271 | var that = this; 1272 | var tref; 1273 | that.send_stop = function() { 1274 | that.send_stop = null; 1275 | clearTimeout(tref); 1276 | }; 1277 | tref = utils.delay(25, function() { 1278 | that.send_stop = null; 1279 | that.send_schedule(); 1280 | }); 1281 | }; 1282 | 1283 | BufferedSender.prototype.send_schedule = function() { 1284 | var that = this; 1285 | if (that.send_buffer.length > 0) { 1286 | var payload = '[' + that.send_buffer.join(',') + ']'; 1287 | that.send_stop = that.sender(that.trans_url, payload, function(success, abort_reason) { 1288 | that.send_stop = null; 1289 | if (success === false) { 1290 | that.ri._didClose(1006, 'Sending error ' + abort_reason); 1291 | } else { 1292 | that.send_schedule_wait(); 1293 | } 1294 | }); 1295 | that.send_buffer = []; 1296 | } 1297 | }; 1298 | 1299 | BufferedSender.prototype.send_destructor = function() { 1300 | var that = this; 1301 | if (that._send_stop) { 1302 | that._send_stop(); 1303 | } 1304 | that._send_stop = null; 1305 | }; 1306 | 1307 | var jsonPGenericSender = function(url, payload, callback) { 1308 | var that = this; 1309 | 1310 | if (!('_send_form' in that)) { 1311 | var form = that._send_form = _document.createElement('form'); 1312 | var area = that._send_area = _document.createElement('textarea'); 1313 | area.name = 'd'; 1314 | form.style.display = 'none'; 1315 | form.style.position = 'absolute'; 1316 | form.method = 'POST'; 1317 | form.enctype = 'application/x-www-form-urlencoded'; 1318 | form.acceptCharset = "UTF-8"; 1319 | form.appendChild(area); 1320 | _document.body.appendChild(form); 1321 | } 1322 | var form = that._send_form; 1323 | var area = that._send_area; 1324 | var id = 'a' + utils.random_string(8); 1325 | form.target = id; 1326 | form.action = url + '/jsonp_send?i=' + id; 1327 | 1328 | var iframe; 1329 | try { 1330 | // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) 1331 | iframe = _document.createElement('