├── .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(''); 1332 | } catch(x) { 1333 | iframe = _document.createElement('iframe'); 1334 | iframe.name = id; 1335 | } 1336 | iframe.id = id; 1337 | form.appendChild(iframe); 1338 | iframe.style.display = 'none'; 1339 | 1340 | try { 1341 | area.value = payload; 1342 | } catch(e) { 1343 | utils.log('Your browser is seriously broken. Go home! ' + e.message); 1344 | } 1345 | form.submit(); 1346 | 1347 | var completed = function(e) { 1348 | if (!iframe.onerror) return; 1349 | iframe.onreadystatechange = iframe.onerror = iframe.onload = null; 1350 | // Opera mini doesn't like if we GC iframe 1351 | // immediately, thus this timeout. 1352 | utils.delay(500, function() { 1353 | iframe.parentNode.removeChild(iframe); 1354 | iframe = null; 1355 | }); 1356 | area.value = ''; 1357 | // It is not possible to detect if the iframe succeeded or 1358 | // failed to submit our form. 1359 | callback(true); 1360 | }; 1361 | iframe.onerror = iframe.onload = completed; 1362 | iframe.onreadystatechange = function(e) { 1363 | if (iframe.readyState == 'complete') completed(); 1364 | }; 1365 | return completed; 1366 | }; 1367 | 1368 | var createAjaxSender = function(AjaxObject) { 1369 | return function(url, payload, callback) { 1370 | var xo = new AjaxObject('POST', url + '/xhr_send', payload); 1371 | xo.onfinish = function(status, text) { 1372 | callback(status === 200 || status === 204, 1373 | 'http status ' + status); 1374 | }; 1375 | return function(abort_reason) { 1376 | callback(false, abort_reason); 1377 | }; 1378 | }; 1379 | }; 1380 | // [*] End of lib/trans-sender.js 1381 | 1382 | 1383 | // [*] Including lib/trans-jsonp-receiver.js 1384 | /* 1385 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1386 | * 1387 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1388 | */ 1389 | 1390 | // Parts derived from Socket.io: 1391 | // https://github.com/LearnBoost/socket.io/blob/0.6.17/lib/socket.io/transports/jsonp-polling.js 1392 | // and jQuery-JSONP: 1393 | // https://code.google.com/p/jquery-jsonp/source/browse/trunk/core/jquery.jsonp.js 1394 | var jsonPGenericReceiver = function(url, callback) { 1395 | var tref; 1396 | var script = _document.createElement('script'); 1397 | var script2; // Opera synchronous load trick. 1398 | var close_script = function(frame) { 1399 | if (script2) { 1400 | script2.parentNode.removeChild(script2); 1401 | script2 = null; 1402 | } 1403 | if (script) { 1404 | clearTimeout(tref); 1405 | // Unfortunately, you can't really abort script loading of 1406 | // the script. 1407 | script.parentNode.removeChild(script); 1408 | script.onreadystatechange = script.onerror = 1409 | script.onload = script.onclick = null; 1410 | script = null; 1411 | callback(frame); 1412 | callback = null; 1413 | } 1414 | }; 1415 | 1416 | // IE9 fires 'error' event after orsc or before, in random order. 1417 | var loaded_okay = false; 1418 | var error_timer = null; 1419 | 1420 | script.id = 'a' + utils.random_string(8); 1421 | script.src = url; 1422 | script.type = 'text/javascript'; 1423 | script.charset = 'UTF-8'; 1424 | script.onerror = function(e) { 1425 | if (!error_timer) { 1426 | // Delay firing close_script. 1427 | error_timer = setTimeout(function() { 1428 | if (!loaded_okay) { 1429 | close_script(utils.closeFrame( 1430 | 1006, 1431 | "JSONP script loaded abnormally (onerror)")); 1432 | } 1433 | }, 1000); 1434 | } 1435 | }; 1436 | script.onload = function(e) { 1437 | close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (onload)")); 1438 | }; 1439 | 1440 | script.onreadystatechange = function(e) { 1441 | if (/loaded|closed/.test(script.readyState)) { 1442 | if (script && script.htmlFor && script.onclick) { 1443 | loaded_okay = true; 1444 | try { 1445 | // In IE, actually execute the script. 1446 | script.onclick(); 1447 | } catch (x) {} 1448 | } 1449 | if (script) { 1450 | close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (onreadystatechange)")); 1451 | } 1452 | } 1453 | }; 1454 | // IE: event/htmlFor/onclick trick. 1455 | // One can't rely on proper order for onreadystatechange. In order to 1456 | // make sure, set a 'htmlFor' and 'event' properties, so that 1457 | // script code will be installed as 'onclick' handler for the 1458 | // script object. Later, onreadystatechange, manually execute this 1459 | // code. FF and Chrome doesn't work with 'event' and 'htmlFor' 1460 | // set. For reference see: 1461 | // http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html 1462 | // Also, read on that about script ordering: 1463 | // http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order 1464 | if (typeof script.async === 'undefined' && _document.attachEvent) { 1465 | // According to mozilla docs, in recent browsers script.async defaults 1466 | // to 'true', so we may use it to detect a good browser: 1467 | // https://developer.mozilla.org/en/HTML/Element/script 1468 | if (!/opera/i.test(navigator.userAgent)) { 1469 | // Naively assume we're in IE 1470 | try { 1471 | script.htmlFor = script.id; 1472 | script.event = "onclick"; 1473 | } catch (x) {} 1474 | script.async = true; 1475 | } else { 1476 | // Opera, second sync script hack 1477 | script2 = _document.createElement('script'); 1478 | script2.text = "try{var a = document.getElementById('"+script.id+"'); if(a)a.onerror();}catch(x){};"; 1479 | script.async = script2.async = false; 1480 | } 1481 | } 1482 | if (typeof script.async !== 'undefined') { 1483 | script.async = true; 1484 | } 1485 | 1486 | // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty. 1487 | tref = setTimeout(function() { 1488 | close_script(utils.closeFrame(1006, "JSONP script loaded abnormally (timeout)")); 1489 | }, 35000); 1490 | 1491 | var head = _document.getElementsByTagName('head')[0]; 1492 | head.insertBefore(script, head.firstChild); 1493 | if (script2) { 1494 | head.insertBefore(script2, head.firstChild); 1495 | } 1496 | return close_script; 1497 | }; 1498 | // [*] End of lib/trans-jsonp-receiver.js 1499 | 1500 | 1501 | // [*] Including lib/trans-jsonp-polling.js 1502 | /* 1503 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1504 | * 1505 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1506 | */ 1507 | 1508 | // The simplest and most robust transport, using the well-know cross 1509 | // domain hack - JSONP. This transport is quite inefficient - one 1510 | // mssage could use up to one http request. But at least it works almost 1511 | // everywhere. 1512 | // Known limitations: 1513 | // o you will get a spinning cursor 1514 | // o for Konqueror a dumb timer is needed to detect errors 1515 | 1516 | 1517 | var JsonPTransport = SockJS['jsonp-polling'] = function(ri, trans_url) { 1518 | utils.polluteGlobalNamespace(); 1519 | var that = this; 1520 | that.ri = ri; 1521 | that.trans_url = trans_url; 1522 | that.send_constructor(jsonPGenericSender); 1523 | that._schedule_recv(); 1524 | }; 1525 | 1526 | // Inheritnace 1527 | JsonPTransport.prototype = new BufferedSender(); 1528 | 1529 | JsonPTransport.prototype._schedule_recv = function() { 1530 | var that = this; 1531 | var callback = function(data) { 1532 | that._recv_stop = null; 1533 | if (data) { 1534 | // no data - heartbeat; 1535 | if (!that._is_closing) { 1536 | that.ri._didMessage(data); 1537 | } 1538 | } 1539 | // The message can be a close message, and change is_closing state. 1540 | if (!that._is_closing) { 1541 | that._schedule_recv(); 1542 | } 1543 | }; 1544 | that._recv_stop = jsonPReceiverWrapper(that.trans_url + '/jsonp', 1545 | jsonPGenericReceiver, callback); 1546 | }; 1547 | 1548 | JsonPTransport.enabled = function() { 1549 | return true; 1550 | }; 1551 | 1552 | JsonPTransport.need_body = true; 1553 | 1554 | 1555 | JsonPTransport.prototype.doCleanup = function() { 1556 | var that = this; 1557 | that._is_closing = true; 1558 | if (that._recv_stop) { 1559 | that._recv_stop(); 1560 | } 1561 | that.ri = that._recv_stop = null; 1562 | that.send_destructor(); 1563 | }; 1564 | 1565 | 1566 | // Abstract away code that handles global namespace pollution. 1567 | var jsonPReceiverWrapper = function(url, constructReceiver, user_callback) { 1568 | var id = 'a' + utils.random_string(6); 1569 | var url_id = url + '?c=' + escape(WPrefix + '.' + id); 1570 | 1571 | // Unfortunately it is not possible to abort loading of the 1572 | // script. We need to keep track of frake close frames. 1573 | var aborting = 0; 1574 | 1575 | // Callback will be called exactly once. 1576 | var callback = function(frame) { 1577 | switch(aborting) { 1578 | case 0: 1579 | // Normal behaviour - delete hook _and_ emit message. 1580 | delete _window[WPrefix][id]; 1581 | user_callback(frame); 1582 | break; 1583 | case 1: 1584 | // Fake close frame - emit but don't delete hook. 1585 | user_callback(frame); 1586 | aborting = 2; 1587 | break; 1588 | case 2: 1589 | // Got frame after connection was closed, delete hook, don't emit. 1590 | delete _window[WPrefix][id]; 1591 | break; 1592 | } 1593 | }; 1594 | 1595 | var close_script = constructReceiver(url_id, callback); 1596 | _window[WPrefix][id] = close_script; 1597 | var stop = function() { 1598 | if (_window[WPrefix][id]) { 1599 | aborting = 1; 1600 | _window[WPrefix][id](utils.closeFrame(1000, "JSONP user aborted read")); 1601 | } 1602 | }; 1603 | return stop; 1604 | }; 1605 | // [*] End of lib/trans-jsonp-polling.js 1606 | 1607 | 1608 | // [*] Including lib/trans-xhr.js 1609 | /* 1610 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1611 | * 1612 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1613 | */ 1614 | 1615 | var AjaxBasedTransport = function() {}; 1616 | AjaxBasedTransport.prototype = new BufferedSender(); 1617 | 1618 | AjaxBasedTransport.prototype.run = function(ri, trans_url, 1619 | url_suffix, Receiver, AjaxObject) { 1620 | var that = this; 1621 | that.ri = ri; 1622 | that.trans_url = trans_url; 1623 | that.send_constructor(createAjaxSender(AjaxObject)); 1624 | that.poll = new Polling(ri, Receiver, 1625 | trans_url + url_suffix, AjaxObject); 1626 | }; 1627 | 1628 | AjaxBasedTransport.prototype.doCleanup = function() { 1629 | var that = this; 1630 | if (that.poll) { 1631 | that.poll.abort(); 1632 | that.poll = null; 1633 | } 1634 | }; 1635 | 1636 | // xhr-streaming 1637 | var XhrStreamingTransport = SockJS['xhr-streaming'] = function(ri, trans_url) { 1638 | this.run(ri, trans_url, '/xhr_streaming', XhrReceiver, utils.XHRCorsObject); 1639 | }; 1640 | 1641 | XhrStreamingTransport.prototype = new AjaxBasedTransport(); 1642 | 1643 | XhrStreamingTransport.enabled = function() { 1644 | // Support for CORS Ajax aka Ajax2? Opera 12 claims CORS but 1645 | // doesn't do streaming. 1646 | return (_window.XMLHttpRequest && 1647 | 'withCredentials' in new XMLHttpRequest() && 1648 | (!/opera/i.test(navigator.userAgent))); 1649 | }; 1650 | XhrStreamingTransport.roundTrips = 2; // preflight, ajax 1651 | 1652 | // Safari gets confused when a streaming ajax request is started 1653 | // before onload. This causes the load indicator to spin indefinetely. 1654 | XhrStreamingTransport.need_body = true; 1655 | 1656 | 1657 | // According to: 1658 | // http://stackoverflow.com/questions/1641507/detect-browser-support-for-cross-domain-xmlhttprequests 1659 | // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ 1660 | 1661 | 1662 | // xdr-streaming 1663 | var XdrStreamingTransport = SockJS['xdr-streaming'] = function(ri, trans_url) { 1664 | this.run(ri, trans_url, '/xhr_streaming', XhrReceiver, utils.XDRObject); 1665 | }; 1666 | 1667 | XdrStreamingTransport.prototype = new AjaxBasedTransport(); 1668 | 1669 | XdrStreamingTransport.enabled = function() { 1670 | return !!_window.XDomainRequest; 1671 | }; 1672 | XdrStreamingTransport.roundTrips = 2; // preflight, ajax 1673 | 1674 | 1675 | 1676 | // xhr-polling 1677 | var XhrPollingTransport = SockJS['xhr-polling'] = function(ri, trans_url) { 1678 | this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XHRCorsObject); 1679 | }; 1680 | 1681 | XhrPollingTransport.prototype = new AjaxBasedTransport(); 1682 | 1683 | XhrPollingTransport.enabled = XhrStreamingTransport.enabled; 1684 | XhrPollingTransport.roundTrips = 2; // preflight, ajax 1685 | 1686 | 1687 | // xdr-polling 1688 | var XdrPollingTransport = SockJS['xdr-polling'] = function(ri, trans_url) { 1689 | this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XDRObject); 1690 | }; 1691 | 1692 | XdrPollingTransport.prototype = new AjaxBasedTransport(); 1693 | 1694 | XdrPollingTransport.enabled = XdrStreamingTransport.enabled; 1695 | XdrPollingTransport.roundTrips = 2; // preflight, ajax 1696 | // [*] End of lib/trans-xhr.js 1697 | 1698 | 1699 | // [*] Including lib/trans-iframe.js 1700 | /* 1701 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1702 | * 1703 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1704 | */ 1705 | 1706 | // Few cool transports do work only for same-origin. In order to make 1707 | // them working cross-domain we shall use iframe, served form the 1708 | // remote domain. New browsers, have capabilities to communicate with 1709 | // cross domain iframe, using postMessage(). In IE it was implemented 1710 | // from IE 8+, but of course, IE got some details wrong: 1711 | // http://msdn.microsoft.com/en-us/library/cc197015(v=VS.85).aspx 1712 | // http://stevesouders.com/misc/test-postmessage.php 1713 | 1714 | var IframeTransport = function() {}; 1715 | 1716 | IframeTransport.prototype.i_constructor = function(ri, trans_url, base_url) { 1717 | var that = this; 1718 | that.ri = ri; 1719 | that.origin = utils.getOrigin(base_url); 1720 | that.base_url = base_url; 1721 | that.trans_url = trans_url; 1722 | 1723 | var iframe_url = base_url + '/iframe.html'; 1724 | if (that.ri._options.devel) { 1725 | iframe_url += '?t=' + (+new Date); 1726 | } 1727 | that.window_id = utils.random_string(8); 1728 | iframe_url += '#' + that.window_id; 1729 | 1730 | that.iframeObj = utils.createIframe(iframe_url, function(r) { 1731 | that.ri._didClose(1006, "Unable to load an iframe (" + r + ")"); 1732 | }); 1733 | 1734 | that.onmessage_cb = utils.bind(that.onmessage, that); 1735 | utils.attachMessage(that.onmessage_cb); 1736 | }; 1737 | 1738 | IframeTransport.prototype.doCleanup = function() { 1739 | var that = this; 1740 | if (that.iframeObj) { 1741 | utils.detachMessage(that.onmessage_cb); 1742 | try { 1743 | // When the iframe is not loaded, IE raises an exception 1744 | // on 'contentWindow'. 1745 | if (that.iframeObj.iframe.contentWindow) { 1746 | that.postMessage('c'); 1747 | } 1748 | } catch (x) {} 1749 | that.iframeObj.cleanup(); 1750 | that.iframeObj = null; 1751 | that.onmessage_cb = that.iframeObj = null; 1752 | } 1753 | }; 1754 | 1755 | IframeTransport.prototype.onmessage = function(e) { 1756 | var that = this; 1757 | if (e.origin !== that.origin) return; 1758 | var window_id = e.data.slice(0, 8); 1759 | var type = e.data.slice(8, 9); 1760 | var data = e.data.slice(9); 1761 | 1762 | if (window_id !== that.window_id) return; 1763 | 1764 | switch(type) { 1765 | case 's': 1766 | that.iframeObj.loaded(); 1767 | that.postMessage('s', JSON.stringify([SockJS.version, that.protocol, that.trans_url, that.base_url])); 1768 | break; 1769 | case 't': 1770 | that.ri._didMessage(data); 1771 | break; 1772 | } 1773 | }; 1774 | 1775 | IframeTransport.prototype.postMessage = function(type, data) { 1776 | var that = this; 1777 | that.iframeObj.post(that.window_id + type + (data || ''), that.origin); 1778 | }; 1779 | 1780 | IframeTransport.prototype.doSend = function (message) { 1781 | this.postMessage('m', message); 1782 | }; 1783 | 1784 | IframeTransport.enabled = function() { 1785 | // postMessage misbehaves in konqueror 4.6.5 - the messages are delivered 1786 | // with 1787 | // huge delay, or not at all. 1788 | var konqueror = navigator && navigator.userAgent && navigator.userAgent.indexOf('Konqueror') !== -1; 1789 | return ((typeof _window.postMessage === 'function' || 1790 | typeof _window.postMessage === 'object') && (!konqueror)); 1791 | }; 1792 | // [*] End of lib/trans-iframe.js 1793 | 1794 | 1795 | // [*] Including lib/trans-iframe-within.js 1796 | /* 1797 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1798 | * 1799 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1800 | */ 1801 | 1802 | var curr_window_id; 1803 | 1804 | var postMessage = function (type, data) { 1805 | if(parent !== _window) { 1806 | parent.postMessage(curr_window_id + type + (data || ''), '*'); 1807 | } else { 1808 | utils.log("Can't postMessage, no parent window.", type, data); 1809 | } 1810 | }; 1811 | 1812 | var FacadeJS = function() {}; 1813 | FacadeJS.prototype._didClose = function (code, reason) { 1814 | postMessage('t', utils.closeFrame(code, reason)); 1815 | }; 1816 | FacadeJS.prototype._didMessage = function (frame) { 1817 | postMessage('t', frame); 1818 | }; 1819 | FacadeJS.prototype._doSend = function (data) { 1820 | this._transport.doSend(data); 1821 | }; 1822 | FacadeJS.prototype._doCleanup = function () { 1823 | this._transport.doCleanup(); 1824 | }; 1825 | 1826 | utils.parent_origin = undefined; 1827 | 1828 | SockJS.bootstrap_iframe = function() { 1829 | var facade; 1830 | curr_window_id = _document.location.hash.slice(1); 1831 | var onMessage = function(e) { 1832 | if(e.source !== parent) return; 1833 | if(typeof utils.parent_origin === 'undefined') 1834 | utils.parent_origin = e.origin; 1835 | if (e.origin !== utils.parent_origin) return; 1836 | 1837 | var window_id = e.data.slice(0, 8); 1838 | var type = e.data.slice(8, 9); 1839 | var data = e.data.slice(9); 1840 | if (window_id !== curr_window_id) return; 1841 | switch(type) { 1842 | case 's': 1843 | var p = JSON.parse(data); 1844 | var version = p[0]; 1845 | var protocol = p[1]; 1846 | var trans_url = p[2]; 1847 | var base_url = p[3]; 1848 | if (version !== SockJS.version) { 1849 | utils.log("Incompatibile SockJS! Main site uses:" + 1850 | " \"" + version + "\", the iframe:" + 1851 | " \"" + SockJS.version + "\"."); 1852 | } 1853 | if (!utils.flatUrl(trans_url) || !utils.flatUrl(base_url)) { 1854 | utils.log("Only basic urls are supported in SockJS"); 1855 | return; 1856 | } 1857 | 1858 | if (!utils.isSameOriginUrl(trans_url) || 1859 | !utils.isSameOriginUrl(base_url)) { 1860 | utils.log("Can't connect to different domain from within an " + 1861 | "iframe. (" + JSON.stringify([_window.location.href, trans_url, base_url]) + 1862 | ")"); 1863 | return; 1864 | } 1865 | facade = new FacadeJS(); 1866 | facade._transport = new FacadeJS[protocol](facade, trans_url, base_url); 1867 | break; 1868 | case 'm': 1869 | facade._doSend(data); 1870 | break; 1871 | case 'c': 1872 | if (facade) 1873 | facade._doCleanup(); 1874 | facade = null; 1875 | break; 1876 | } 1877 | }; 1878 | 1879 | // alert('test ticker'); 1880 | // facade = new FacadeJS(); 1881 | // facade._transport = new FacadeJS['w-iframe-xhr-polling'](facade, 1882 | // 'http://host.com:9999/ticker/12/basd'); 1883 | 1884 | utils.attachMessage(onMessage); 1885 | 1886 | // Start 1887 | postMessage('s'); 1888 | }; 1889 | // [*] End of lib/trans-iframe-within.js 1890 | 1891 | 1892 | // [*] Including lib/info.js 1893 | /* 1894 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 1895 | * 1896 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 1897 | */ 1898 | 1899 | var InfoReceiver = function(base_url, AjaxObject) { 1900 | var that = this; 1901 | utils.delay(function(){that.doXhr(base_url, AjaxObject);}); 1902 | }; 1903 | 1904 | InfoReceiver.prototype = new EventEmitter(['finish']); 1905 | 1906 | InfoReceiver.prototype.doXhr = function(base_url, AjaxObject) { 1907 | var that = this; 1908 | var t0 = (new Date()).getTime(); 1909 | var xo = new AjaxObject('GET', base_url + '/info'); 1910 | 1911 | var tref = utils.delay(8000, 1912 | function(){xo.ontimeout();}); 1913 | 1914 | xo.onfinish = function(status, text) { 1915 | clearTimeout(tref); 1916 | tref = null; 1917 | if (status === 200) { 1918 | var rtt = (new Date()).getTime() - t0; 1919 | var info = JSON.parse(text); 1920 | if (typeof info !== 'object') info = {}; 1921 | that.emit('finish', info, rtt); 1922 | } else { 1923 | that.emit('finish'); 1924 | } 1925 | }; 1926 | xo.ontimeout = function() { 1927 | xo.close(); 1928 | that.emit('finish'); 1929 | }; 1930 | }; 1931 | 1932 | var InfoReceiverIframe = function(base_url) { 1933 | var that = this; 1934 | var go = function() { 1935 | var ifr = new IframeTransport(); 1936 | ifr.protocol = 'w-iframe-info-receiver'; 1937 | var fun = function(r) { 1938 | if (typeof r === 'string' && r.substr(0,1) === 'm') { 1939 | var d = JSON.parse(r.substr(1)); 1940 | var info = d[0], rtt = d[1]; 1941 | that.emit('finish', info, rtt); 1942 | } else { 1943 | that.emit('finish'); 1944 | } 1945 | ifr.doCleanup(); 1946 | ifr = null; 1947 | }; 1948 | var mock_ri = { 1949 | _options: {}, 1950 | _didClose: fun, 1951 | _didMessage: fun 1952 | }; 1953 | ifr.i_constructor(mock_ri, base_url, base_url); 1954 | } 1955 | if(!_document.body) { 1956 | utils.attachEvent('load', go); 1957 | } else { 1958 | go(); 1959 | } 1960 | }; 1961 | InfoReceiverIframe.prototype = new EventEmitter(['finish']); 1962 | 1963 | 1964 | var InfoReceiverFake = function() { 1965 | // It may not be possible to do cross domain AJAX to get the info 1966 | // data, for example for IE7. But we want to run JSONP, so let's 1967 | // fake the response, with rtt=2s (rto=6s). 1968 | var that = this; 1969 | utils.delay(function() { 1970 | that.emit('finish', {}, 2000); 1971 | }); 1972 | }; 1973 | InfoReceiverFake.prototype = new EventEmitter(['finish']); 1974 | 1975 | var createInfoReceiver = function(base_url) { 1976 | if (utils.isSameOriginUrl(base_url)) { 1977 | // If, for some reason, we have SockJS locally - there's no 1978 | // need to start up the complex machinery. Just use ajax. 1979 | return new InfoReceiver(base_url, utils.XHRLocalObject); 1980 | } 1981 | switch (utils.isXHRCorsCapable()) { 1982 | case 1: 1983 | // XHRLocalObject -> no_credentials=true 1984 | return new InfoReceiver(base_url, utils.XHRLocalObject); 1985 | case 2: 1986 | return new InfoReceiver(base_url, utils.XDRObject); 1987 | case 3: 1988 | // Opera 1989 | return new InfoReceiverIframe(base_url); 1990 | default: 1991 | // IE 7 1992 | return new InfoReceiverFake(); 1993 | }; 1994 | }; 1995 | 1996 | 1997 | var WInfoReceiverIframe = FacadeJS['w-iframe-info-receiver'] = function(ri, _trans_url, base_url) { 1998 | var ir = new InfoReceiver(base_url, utils.XHRLocalObject); 1999 | ir.onfinish = function(info, rtt) { 2000 | ri._didMessage('m'+JSON.stringify([info, rtt])); 2001 | ri._didClose(); 2002 | } 2003 | }; 2004 | WInfoReceiverIframe.prototype.doCleanup = function() {}; 2005 | // [*] End of lib/info.js 2006 | 2007 | 2008 | // [*] Including lib/trans-iframe-eventsource.js 2009 | /* 2010 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2011 | * 2012 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2013 | */ 2014 | 2015 | var EventSourceIframeTransport = SockJS['iframe-eventsource'] = function () { 2016 | var that = this; 2017 | that.protocol = 'w-iframe-eventsource'; 2018 | that.i_constructor.apply(that, arguments); 2019 | }; 2020 | 2021 | EventSourceIframeTransport.prototype = new IframeTransport(); 2022 | 2023 | EventSourceIframeTransport.enabled = function () { 2024 | return ('EventSource' in _window) && IframeTransport.enabled(); 2025 | }; 2026 | 2027 | EventSourceIframeTransport.need_body = true; 2028 | EventSourceIframeTransport.roundTrips = 3; // html, javascript, eventsource 2029 | 2030 | 2031 | // w-iframe-eventsource 2032 | var EventSourceTransport = FacadeJS['w-iframe-eventsource'] = function(ri, trans_url) { 2033 | this.run(ri, trans_url, '/eventsource', EventSourceReceiver, utils.XHRLocalObject); 2034 | } 2035 | EventSourceTransport.prototype = new AjaxBasedTransport(); 2036 | // [*] End of lib/trans-iframe-eventsource.js 2037 | 2038 | 2039 | // [*] Including lib/trans-iframe-xhr-polling.js 2040 | /* 2041 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2042 | * 2043 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2044 | */ 2045 | 2046 | var XhrPollingIframeTransport = SockJS['iframe-xhr-polling'] = function () { 2047 | var that = this; 2048 | that.protocol = 'w-iframe-xhr-polling'; 2049 | that.i_constructor.apply(that, arguments); 2050 | }; 2051 | 2052 | XhrPollingIframeTransport.prototype = new IframeTransport(); 2053 | 2054 | XhrPollingIframeTransport.enabled = function () { 2055 | return _window.XMLHttpRequest && IframeTransport.enabled(); 2056 | }; 2057 | 2058 | XhrPollingIframeTransport.need_body = true; 2059 | XhrPollingIframeTransport.roundTrips = 3; // html, javascript, xhr 2060 | 2061 | 2062 | // w-iframe-xhr-polling 2063 | var XhrPollingITransport = FacadeJS['w-iframe-xhr-polling'] = function(ri, trans_url) { 2064 | this.run(ri, trans_url, '/xhr', XhrReceiver, utils.XHRLocalObject); 2065 | }; 2066 | 2067 | XhrPollingITransport.prototype = new AjaxBasedTransport(); 2068 | // [*] End of lib/trans-iframe-xhr-polling.js 2069 | 2070 | 2071 | // [*] Including lib/trans-iframe-htmlfile.js 2072 | /* 2073 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2074 | * 2075 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2076 | */ 2077 | 2078 | // This transport generally works in any browser, but will cause a 2079 | // spinning cursor to appear in any browser other than IE. 2080 | // We may test this transport in all browsers - why not, but in 2081 | // production it should be only run in IE. 2082 | 2083 | var HtmlFileIframeTransport = SockJS['iframe-htmlfile'] = function () { 2084 | var that = this; 2085 | that.protocol = 'w-iframe-htmlfile'; 2086 | that.i_constructor.apply(that, arguments); 2087 | }; 2088 | 2089 | // Inheritance. 2090 | HtmlFileIframeTransport.prototype = new IframeTransport(); 2091 | 2092 | HtmlFileIframeTransport.enabled = function() { 2093 | return IframeTransport.enabled(); 2094 | }; 2095 | 2096 | HtmlFileIframeTransport.need_body = true; 2097 | HtmlFileIframeTransport.roundTrips = 3; // html, javascript, htmlfile 2098 | 2099 | 2100 | // w-iframe-htmlfile 2101 | var HtmlFileTransport = FacadeJS['w-iframe-htmlfile'] = function(ri, trans_url) { 2102 | this.run(ri, trans_url, '/htmlfile', HtmlfileReceiver, utils.XHRLocalObject); 2103 | }; 2104 | HtmlFileTransport.prototype = new AjaxBasedTransport(); 2105 | // [*] End of lib/trans-iframe-htmlfile.js 2106 | 2107 | 2108 | // [*] Including lib/trans-polling.js 2109 | /* 2110 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2111 | * 2112 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2113 | */ 2114 | 2115 | var Polling = function(ri, Receiver, recv_url, AjaxObject) { 2116 | var that = this; 2117 | that.ri = ri; 2118 | that.Receiver = Receiver; 2119 | that.recv_url = recv_url; 2120 | that.AjaxObject = AjaxObject; 2121 | that._scheduleRecv(); 2122 | }; 2123 | 2124 | Polling.prototype._scheduleRecv = function() { 2125 | var that = this; 2126 | var poll = that.poll = new that.Receiver(that.recv_url, that.AjaxObject); 2127 | var msg_counter = 0; 2128 | poll.onmessage = function(e) { 2129 | msg_counter += 1; 2130 | that.ri._didMessage(e.data); 2131 | }; 2132 | poll.onclose = function(e) { 2133 | that.poll = poll = poll.onmessage = poll.onclose = null; 2134 | if (!that.poll_is_closing) { 2135 | if (e.reason === 'permanent') { 2136 | that.ri._didClose(1006, 'Polling error (' + e.reason + ')'); 2137 | } else { 2138 | that._scheduleRecv(); 2139 | } 2140 | } 2141 | }; 2142 | }; 2143 | 2144 | Polling.prototype.abort = function() { 2145 | var that = this; 2146 | that.poll_is_closing = true; 2147 | if (that.poll) { 2148 | that.poll.abort(); 2149 | } 2150 | }; 2151 | // [*] End of lib/trans-polling.js 2152 | 2153 | 2154 | // [*] Including lib/trans-receiver-eventsource.js 2155 | /* 2156 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2157 | * 2158 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2159 | */ 2160 | 2161 | var EventSourceReceiver = function(url) { 2162 | var that = this; 2163 | var es = new EventSource(url); 2164 | es.onmessage = function(e) { 2165 | that.dispatchEvent(new SimpleEvent('message', 2166 | {'data': unescape(e.data)})); 2167 | }; 2168 | that.es_close = es.onerror = function(e, abort_reason) { 2169 | // ES on reconnection has readyState = 0 or 1. 2170 | // on network error it's CLOSED = 2 2171 | var reason = abort_reason ? 'user' : 2172 | (es.readyState !== 2 ? 'network' : 'permanent'); 2173 | that.es_close = es.onmessage = es.onerror = null; 2174 | // EventSource reconnects automatically. 2175 | es.close(); 2176 | es = null; 2177 | // Safari and chrome < 15 crash if we close window before 2178 | // waiting for ES cleanup. See: 2179 | // https://code.google.com/p/chromium/issues/detail?id=89155 2180 | utils.delay(200, function() { 2181 | that.dispatchEvent(new SimpleEvent('close', {reason: reason})); 2182 | }); 2183 | }; 2184 | }; 2185 | 2186 | EventSourceReceiver.prototype = new REventTarget(); 2187 | 2188 | EventSourceReceiver.prototype.abort = function() { 2189 | var that = this; 2190 | if (that.es_close) { 2191 | that.es_close({}, true); 2192 | } 2193 | }; 2194 | // [*] End of lib/trans-receiver-eventsource.js 2195 | 2196 | 2197 | // [*] Including lib/trans-receiver-htmlfile.js 2198 | /* 2199 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2200 | * 2201 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2202 | */ 2203 | 2204 | var _is_ie_htmlfile_capable; 2205 | var isIeHtmlfileCapable = function() { 2206 | if (_is_ie_htmlfile_capable === undefined) { 2207 | if ('ActiveXObject' in _window) { 2208 | try { 2209 | _is_ie_htmlfile_capable = !!new ActiveXObject('htmlfile'); 2210 | } catch (x) {} 2211 | } else { 2212 | _is_ie_htmlfile_capable = false; 2213 | } 2214 | } 2215 | return _is_ie_htmlfile_capable; 2216 | }; 2217 | 2218 | 2219 | var HtmlfileReceiver = function(url) { 2220 | var that = this; 2221 | utils.polluteGlobalNamespace(); 2222 | 2223 | that.id = 'a' + utils.random_string(6, 26); 2224 | url += ((url.indexOf('?') === -1) ? '?' : '&') + 2225 | 'c=' + escape(WPrefix + '.' + that.id); 2226 | 2227 | var constructor = isIeHtmlfileCapable() ? 2228 | utils.createHtmlfile : utils.createIframe; 2229 | 2230 | var iframeObj; 2231 | _window[WPrefix][that.id] = { 2232 | start: function () { 2233 | iframeObj.loaded(); 2234 | }, 2235 | message: function (data) { 2236 | that.dispatchEvent(new SimpleEvent('message', {'data': data})); 2237 | }, 2238 | stop: function () { 2239 | that.iframe_close({}, 'network'); 2240 | } 2241 | }; 2242 | that.iframe_close = function(e, abort_reason) { 2243 | iframeObj.cleanup(); 2244 | that.iframe_close = iframeObj = null; 2245 | delete _window[WPrefix][that.id]; 2246 | that.dispatchEvent(new SimpleEvent('close', {reason: abort_reason})); 2247 | }; 2248 | iframeObj = constructor(url, function(e) { 2249 | that.iframe_close({}, 'permanent'); 2250 | }); 2251 | }; 2252 | 2253 | HtmlfileReceiver.prototype = new REventTarget(); 2254 | 2255 | HtmlfileReceiver.prototype.abort = function() { 2256 | var that = this; 2257 | if (that.iframe_close) { 2258 | that.iframe_close({}, 'user'); 2259 | } 2260 | }; 2261 | // [*] End of lib/trans-receiver-htmlfile.js 2262 | 2263 | 2264 | // [*] Including lib/trans-receiver-xhr.js 2265 | /* 2266 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2267 | * 2268 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2269 | */ 2270 | 2271 | var XhrReceiver = function(url, AjaxObject) { 2272 | var that = this; 2273 | var buf_pos = 0; 2274 | 2275 | that.xo = new AjaxObject('POST', url, null); 2276 | that.xo.onchunk = function(status, text) { 2277 | if (status !== 200) return; 2278 | while (1) { 2279 | var buf = text.slice(buf_pos); 2280 | var p = buf.indexOf('\n'); 2281 | if (p === -1) break; 2282 | buf_pos += p+1; 2283 | var msg = buf.slice(0, p); 2284 | that.dispatchEvent(new SimpleEvent('message', {data: msg})); 2285 | } 2286 | }; 2287 | that.xo.onfinish = function(status, text) { 2288 | that.xo.onchunk(status, text); 2289 | that.xo = null; 2290 | var reason = status === 200 ? 'network' : 'permanent'; 2291 | that.dispatchEvent(new SimpleEvent('close', {reason: reason})); 2292 | } 2293 | }; 2294 | 2295 | XhrReceiver.prototype = new REventTarget(); 2296 | 2297 | XhrReceiver.prototype.abort = function() { 2298 | var that = this; 2299 | if (that.xo) { 2300 | that.xo.close(); 2301 | that.dispatchEvent(new SimpleEvent('close', {reason: 'user'})); 2302 | that.xo = null; 2303 | } 2304 | }; 2305 | // [*] End of lib/trans-receiver-xhr.js 2306 | 2307 | 2308 | // [*] Including lib/test-hooks.js 2309 | /* 2310 | * ***** BEGIN LICENSE BLOCK ***** Copyright (c) 2011-2012 VMware, Inc. 2311 | * 2312 | * For the license see COPYING. ***** END LICENSE BLOCK ***** 2313 | */ 2314 | 2315 | // For testing 2316 | SockJS.getUtils = function(){ 2317 | return utils; 2318 | }; 2319 | 2320 | SockJS.getIframeTransport = function(){ 2321 | return IframeTransport; 2322 | }; 2323 | // [*] End of lib/test-hooks.js 2324 | 2325 | return SockJS; 2326 | })(); 2327 | if ('_sockjs_onload' in window) setTimeout(_sockjs_onload, 1); 2328 | 2329 | // AMD compliance 2330 | if (typeof define === 'function' && define.amd) { 2331 | define('sockjs', [], function(){return SockJS;}); 2332 | } 2333 | // [*] End of lib/index.js 2334 | 2335 | // [*] End of lib/all.js 2336 | -------------------------------------------------------------------------------- /src/main/resources/public/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-angular2-realtime-dashboard", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "start": "tsc && concurrently \"npm run tsc:w\" \"npm run lite\" ", 6 | "lite": "lite-server", 7 | "postinstall": "typings install", 8 | "tsc": "tsc", 9 | "tsc:w": "tsc -w", 10 | "typings": "typings" 11 | }, 12 | "license": "ISC", 13 | "dependencies": { 14 | "@angular/common": "2.0.0-rc.1", 15 | "@angular/compiler": "2.0.0-rc.1", 16 | "@angular/core": "2.0.0-rc.1", 17 | "@angular/http": "2.0.0-rc.1", 18 | "@angular/platform-browser": "2.0.0-rc.1", 19 | "@angular/platform-browser-dynamic": "2.0.0-rc.1", 20 | "@angular/router": "2.0.0-rc.1", 21 | "@angular/router-deprecated": "^2.0.0-rc.1", 22 | "angular2-in-memory-web-api": "0.0.7", 23 | "bootstrap": "^3.3.6", 24 | "es6-shim": "^0.35.0", 25 | "ng2-bootstrap": "^1.0.16", 26 | "ng2-nvd3": "^1.1.0", 27 | "plugin-typescript": "^4.0.15", 28 | "reflect-metadata": "^0.1.3", 29 | "rxjs": "5.0.0-beta.6", 30 | "stompjs": "^2.3.3", 31 | "systemjs": "0.19.27", 32 | "zone.js": "^0.6.12" 33 | }, 34 | "devDependencies": { 35 | "concurrently": "^2.0.0", 36 | "lite-server": "^2.2.0", 37 | "typescript": "^1.8.10", 38 | "typings": "^0.8.1" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/resources/public/systemjs.config.js: -------------------------------------------------------------------------------- 1 | (function(global) { 2 | 3 | var ngVer = '@2.0.0-rc.1'; // lock in the angular package version; do not let it float to current! 4 | 5 | //map tells the System loader where to look for things 6 | var map = { 7 | 'app': 'app', 8 | 9 | '@angular': 'node_modules/@angular', // sufficient if we didn't pin the version 10 | 'angular2-in-memory-web-api': 'node_modules/angular2-in-memory-web-api', // get latest 11 | 'rxjs': 'node_modules/rxjs', 12 | 'ng2-nvd3': 'node_modules/ng2-nvd3/build/lib/ng2-nvd3.js', 13 | 'ts': 'node_modules/plugin-typescript/lib/plugin.js', 14 | 'typescript': 'node_modules/typescript/lib/typescript.js', 15 | }; 16 | 17 | //packages tells the System loader how to load when no filename and/or no extension 18 | var packages = { 19 | 'app': { main: 'main.ts', defaultExtension: 'ts' }, 20 | 'rxjs': { defaultExtension: 'js' }, 21 | 'angular2-in-memory-web-api': { defaultExtension: 'js' }, 22 | 'zone.js': { defaultExtension: 'js' }, 23 | }; 24 | 25 | var ngPackageNames = [ 26 | 'common', 27 | 'compiler', 28 | 'core', 29 | 'http', 30 | 'platform-browser', 31 | 'platform-browser-dynamic', 32 | 'router', 33 | 'router-deprecated', 34 | 'upgrade', 35 | ]; 36 | 37 | // Add map entries for each angular package 38 | // only because we're pinning the version with `ngVer`. 39 | ngPackageNames.forEach(function(pkgName) { 40 | map['@angular/'+pkgName] = 'node_modules/@angular/' + pkgName; 41 | }); 42 | 43 | // Add package entries for angular packages 44 | ngPackageNames.forEach(function(pkgName) { 45 | 46 | // Bundled (~40 requests): 47 | packages['@angular/'+pkgName] = { main: pkgName + '.umd.js', defaultExtension: 'js' }; 48 | 49 | // Individual files (~300 requests): 50 | //packages['@angular/'+pkgName] = { main: 'index.js', defaultExtension: 'js' }; 51 | }); 52 | 53 | var config = { 54 | // DEMO ONLY! REAL CODE SHOULD NOT TRANSPILE IN THE BROWSER 55 | transpiler: 'typescript', 56 | typescriptOptions: { 57 | emitDecoratorMetadata: true 58 | }, 59 | 60 | map: map, 61 | packages: packages 62 | } 63 | 64 | System.config(config); 65 | 66 | })(this); 67 | 68 | 69 | /* 70 | Copyright 2016 Google Inc. All Rights Reserved. 71 | Use of this source code is governed by an MIT-style license that 72 | can be found in the LICENSE file at http://angular.io/license 73 | */ -------------------------------------------------------------------------------- /src/main/resources/public/templates/dashboard.html: -------------------------------------------------------------------------------- 1 | Dashboard 2 | 3 | 4 | 5 | {{ stat.name }} 6 | 7 | 8 | Number of votes: {{ stat.totalVote}} 9 | 10 | -------------------------------------------------------------------------------- /src/main/resources/public/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | Spring Boot - AngularJS 2 - Websocket demo 3 | 4 | 5 | 6 | 7 | 8 | 9 | Messages 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/public/templates/poll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ poll.name }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | {{ choice.choice }} 13 | 14 | 15 | 16 | 17 | Submit poll 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/public/templates/poll.list.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | {{ poll.name }} 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/resources/public/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "system", 5 | "moduleResolution": "node", 6 | "sourceMap": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "removeComments": false, 10 | "noImplicitAny": false 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | "typings/main", 15 | "typings/main.d.ts" 16 | ] 17 | } -------------------------------------------------------------------------------- /src/main/resources/public/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ambientDependencies": { 3 | "es6-shim": "github:DefinitelyTyped/DefinitelyTyped/es6-shim/es6-shim.d.ts#7de6c3dd94feaeb21f20054b9f30d5dabc5efabd", 4 | "jasmine": "github:DefinitelyTyped/DefinitelyTyped/jasmine/jasmine.d.ts#5c182b9af717f73146399c2485f70f1e2ac0ff2b" 5 | } 6 | } --------------------------------------------------------------------------------