SEC-2861: Add WebSocket Documentation & Sample
This commit is contained in:
parent
b9563f6102
commit
37740cd020
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package org.springframework.security.config.annotation.web.socket;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.MessageDeliveryException;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import static org.springframework.messaging.simp.SimpMessageType.*;
|
||||
|
||||
import org.springframework.messaging.simp.SimpMessageType;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.messaging.support.GenericMessage;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.mock.web.MockServletConfig;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.security.web.csrf.DefaultCsrfToken;
|
||||
import org.springframework.security.web.csrf.MissingCsrfTokenException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.HttpRequestHandler;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.server.HandshakeFailureException;
|
||||
import org.springframework.web.socket.server.HandshakeHandler;
|
||||
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
|
||||
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler;
|
||||
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.fest.assertions.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
public class AbstractSecurityWebSocketMessageBrokerConfigurerDocTests {
|
||||
AnnotationConfigWebApplicationContext context;
|
||||
|
||||
TestingAuthenticationToken messageUser;
|
||||
|
||||
CsrfToken token;
|
||||
|
||||
String sessionAttr;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
token = new DefaultCsrfToken("header", "param", "token");
|
||||
sessionAttr = "sessionAttr";
|
||||
messageUser = new TestingAuthenticationToken("user","pass","ROLE_USER");
|
||||
}
|
||||
|
||||
@After
|
||||
public void cleanup() {
|
||||
if(context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityMappings() {
|
||||
loadConfig(WebSocketSecurityConfig.class);
|
||||
|
||||
clientInboundChannel().send(message("/user/queue/errors",SimpMessageType.SUBSCRIBE));
|
||||
|
||||
try {
|
||||
clientInboundChannel().send(message("/denyAll", SimpMessageType.MESSAGE));
|
||||
fail("Expected Exception");
|
||||
} catch(MessageDeliveryException expected) {
|
||||
assertThat(expected.getCause()).isInstanceOf(AccessDeniedException.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void loadConfig(Class<?>... configs) {
|
||||
context = new AnnotationConfigWebApplicationContext();
|
||||
context.register(configs);
|
||||
context.register(WebSocketConfig.class,SyncExecutorConfig.class);
|
||||
context.setServletConfig(new MockServletConfig());
|
||||
context.refresh();
|
||||
}
|
||||
|
||||
private MessageChannel clientInboundChannel() {
|
||||
return context.getBean("clientInboundChannel", MessageChannel.class);
|
||||
}
|
||||
|
||||
private Message<String> message(String destination, SimpMessageType type) {
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type);
|
||||
return message(headers, destination);
|
||||
}
|
||||
|
||||
private Message<String> message(SimpMessageHeaderAccessor headers, String destination) {
|
||||
headers.setSessionId("123");
|
||||
headers.setSessionAttributes(new HashMap<String, Object>());
|
||||
if(destination != null) {
|
||||
headers.setDestination(destination);
|
||||
}
|
||||
if(messageUser != null) {
|
||||
headers.setUser(messageUser);
|
||||
}
|
||||
return new GenericMessage<String>("hi",headers.getMessageHeaders());
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MyController {
|
||||
|
||||
@MessageMapping("/authentication")
|
||||
public void authentication(@AuthenticationPrincipal String un) {
|
||||
// ... do something ...
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.nullDestMatcher().authenticated() // <1>
|
||||
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
|
||||
.simpDestMatchers("/app/**").hasRole("USER") // <3>
|
||||
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
|
||||
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
|
||||
.anyMessage().denyAll(); // <6>
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
static class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
|
||||
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/chat")
|
||||
.withSockJS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MyController myController() {
|
||||
return new MyController();
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class SyncExecutorConfig {
|
||||
@Bean
|
||||
public static SyncExecutorSubscribableChannelPostProcessor postProcessor() {
|
||||
return new SyncExecutorSubscribableChannelPostProcessor();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,400 @@
|
|||
[[websocket]]
|
||||
== WebSocket Security
|
||||
|
||||
Spring Security 4 added support for securing http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html[Spring's WebSocket support].
|
||||
This section describes how to use Spring Security's WebSocket support.
|
||||
|
||||
NOTE: You can find a complete working sample of WebSocket security in samples/chat-jc.
|
||||
|
||||
.Direct JSR-356 Support
|
||||
****
|
||||
Spring Security does not provide direct JSR-356 support because doing so would provide little value.
|
||||
This is because the format is unknown, so there is http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-intro-sub-protocol[little Spring can do to secure an unknown format].
|
||||
Additionally, JSR-356 does not provide a way to intercept messages, so security would be rather invasive.
|
||||
****
|
||||
|
||||
[[websocket-configuration]]
|
||||
=== WebSocket Configuration
|
||||
|
||||
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
|
||||
To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
|
||||
For example:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig
|
||||
extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2>
|
||||
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.simpDestMatchers("/user/*").authenticated() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
|
||||
<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
|
||||
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
|
||||
|
||||
Spring Security also provides <<nsa-websocket,XML Namespace>> support for securing WebSockets.
|
||||
A comparable XML based configuration looks like the following:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<websocket-message-broker> <!--1--> <!--2-->
|
||||
<!--3-->
|
||||
<intercept-message pattern="/user/**" access="hasRole('USER')" />
|
||||
</websocket-message-broker>
|
||||
----
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
|
||||
<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
|
||||
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
|
||||
|
||||
[[websocket-authentication]]
|
||||
=== WebSocket Authentication
|
||||
|
||||
WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.
|
||||
This means that the `Principal` on the `HttpServletRequest` will be handed off to WebSockets.
|
||||
If you are using Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically.
|
||||
|
||||
More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application.
|
||||
|
||||
[[websocket-authorization]]
|
||||
=== WebSocket Authorization
|
||||
|
||||
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
|
||||
To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
|
||||
For example:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.nullDestMatcher().authenticated() // <1>
|
||||
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
|
||||
.simpDestMatchers("/app/**").hasRole("USER") // <3>
|
||||
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
|
||||
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
|
||||
.anyMessage().denyAll(); // <6>
|
||||
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any message without a destination (i.e. anything other that Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated
|
||||
<2> Anyone can subscribe to /user/queue/errors
|
||||
<3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER
|
||||
<4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER
|
||||
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
|
||||
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
|
||||
|
||||
Spring Security also provides <<nsa-websocket,XML Namespace>> support for securing WebSockets.
|
||||
A comparable XML based configuration looks like the following:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<websocket-message-broker>
|
||||
<!--1-->
|
||||
<intercept-message type="CONNECT" access="permitAll" />
|
||||
<intercept-message type="UNSUBSCRIBE" access="permitAll" />
|
||||
<intercept-message type="DISCONNECT" access="permitAll" />
|
||||
|
||||
<intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> <!--2-->
|
||||
<intercept-message pattern="/app/**" access="hasRole('USER')" /> <!--3-->
|
||||
|
||||
<!--4-->
|
||||
<intercept-message pattern="/user/**" access="hasRole('USER')" />
|
||||
<intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />
|
||||
|
||||
<!--5-->
|
||||
<intercept-message type="MESSAGE" access="denyAll" />
|
||||
<intercept-message type="SUBSCRIBE" access="denyAll" />
|
||||
|
||||
<intercept-message pattern="/**" access="denyAll" /> <!--6-->
|
||||
</websocket-message-broker>
|
||||
----
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any message of type CONNECT, UNSUBSCRIBE, or DISCONNECT will require the user to be authenticated
|
||||
<2> Anyone can subscribe to /user/queue/errors
|
||||
<3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER
|
||||
<4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER
|
||||
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
|
||||
<6> Any other message with a destination is rejected. This is a good idea to ensure that you do not miss any messages.
|
||||
|
||||
[[websocket-authorization-notes]]
|
||||
==== WebSocket Authorization Notes
|
||||
|
||||
In order to properly secure your application it is important to understand Spring's WebSocket support.
|
||||
|
||||
[[websocket-authorization-notes-messagetypes]]
|
||||
===== WebSocket Authorization on Message Types
|
||||
|
||||
It is important to understand the distinction between SUBSCRIBE and MESSAGE types of messages and how it works within Spring.
|
||||
|
||||
Consider a chat application.
|
||||
|
||||
* The system can send notifications MESSAGE to all users through a destination of "/topic/system/notifications"
|
||||
* Clients can receive notifications by SUBSCRIBE to the "/topic/system/notifications".
|
||||
|
||||
While we want clients to be able to SUBSCRIBE to "/topic/system/notifications", we do not want to enable them to send a MESSAGE to that destination.
|
||||
If we allowed sending a MESSAGE to "/topic/system/notifications", then clients could send a message directly to that endpoint and impersonate the system.
|
||||
|
||||
In general, it is common for applications to deny any MESSAGE sent to a message that starts with the http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (i.e. "/topic/" or "/queue/").
|
||||
|
||||
[[websocket-authorization-notes-destinations]]
|
||||
===== WebSocket Authorization on Destinations
|
||||
|
||||
It is also is important to understand how destinations are transformed.
|
||||
|
||||
Consider a chat application.
|
||||
|
||||
* User's can send messages to a specific user by sending a message to the destination of "/app/chat".
|
||||
* The application sees the message, ensures that the "from" attribute is specified as the current user (we cannot trust the client).
|
||||
* The application then sends the message to the recipient using `SimpMessageSendingOperations.convertAndSendToUser("toUser", "/queue/messages", message)`.
|
||||
* The message gets turned into the destination of "/queue/user/messages-<sessionid>"
|
||||
|
||||
With the application above, we want to allow our client to listen to "/user/queue" which is transformed into "/queue/user/messages-<sessionid>".
|
||||
However, we do not want the client to be able to listen to "/queue/*" because that would allow the client to see messages for every user.
|
||||
|
||||
In general, it is common for applications to deny any SUBSCRIBE sent to a message that starts with the http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp[broker prefix] (i.e. "/topic/" or "/queue/").
|
||||
Of course we may provide exceptions to account for things like
|
||||
|
||||
[[websocket-authorization-notes-outbound]]
|
||||
==== Outbound Messages
|
||||
|
||||
Spring contains a section titled http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-message-flow[Flow of Messages] that describes how messages flow through the system.
|
||||
It is important to note that Spring Security only secures the `clientInboundChannel`.
|
||||
Spring Security does not attempt to secure the `clientOutboundChannel`.
|
||||
|
||||
The most important reason for this is performance.
|
||||
For every message that goes in, there are typically many many more that go out.
|
||||
Instead of securing the outbound messages, we encourage securing the subscription to the endpoints.
|
||||
|
||||
[[websocket-sameorigin]]
|
||||
=== Enforcing Same Origin Policy
|
||||
|
||||
It is important to emphasize that the browser does not enforce the http://en.wikipedia.org/wiki/Same-origin_policy[Same Origin Policy] for WebSocket connections.
|
||||
This is an extremely important consideration.
|
||||
|
||||
[[websocket-sameorigin-why]]
|
||||
==== Why Same Origin?
|
||||
|
||||
Consider the following scenario.
|
||||
A user visits bank.com and authenticates to their account.
|
||||
The same user opens another tab in their browser and visits evil.com.
|
||||
The Same Origin Policy ensures that evil.com cannot read or write data to bank.com.
|
||||
|
||||
With WebSockets the Same Origin Policy does not apply.
|
||||
In fact, unless bank.com explicitly forbids it, evil.com can read and write data on behalf of the user.
|
||||
This means that anything the user can do over the websocket (i.e. transfer money), evil.com can do on that users behalf.
|
||||
|
||||
Since SockJS tries to emulate WebSockets it also bypasses the Same Origin Policy.
|
||||
This means developers need to explicitly protect their applications from external domains when using SockJS.
|
||||
|
||||
[[websocket-sameorigin-spring]]
|
||||
==== Spring WebSocket Allowed Origin
|
||||
|
||||
Fortunately, since Spring 4.1.5 Spring's WebSocket and SockJS support restricts access to the http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-server-allowed-origins[current domain].
|
||||
Spring Security adds an additional layer of protection to provide http://en.wikipedia.org/wiki/Defense_in_depth_%28computing%29[defence in depth].
|
||||
|
||||
[[websocket-sameorigin-csrf]]
|
||||
==== Adding CSRF to Stomp Headers
|
||||
|
||||
By default Spring Security requires the <<csrf,CSRF token>> in any CONNECT message type.
|
||||
This ensures that only a site that has access to the CSRF token can connect.
|
||||
Since only the *Same Origin* can access the CSRF token, external domains are not allowed to make a connection.
|
||||
|
||||
Typically we need to include the CSRF token in an HTTP header or an HTTP parameter.
|
||||
However, SockJS does not allow for these options.
|
||||
Instead, we must include the token in the Stomp headers
|
||||
|
||||
Applications can <<csrf-include-csrf-token,obtain a CSRF token>> by accessing the request attribute named _csrf.
|
||||
For example, the following will allow accessing the `CsrfToken` in a JSP:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
var headerName = "${_csrf.headerName}";
|
||||
var token = "${_csrf.token}";
|
||||
----
|
||||
|
||||
If you are using static HTML, you can expose the `CsrfToken` on a REST endpoint.
|
||||
For example, the following would expose the `CsrfToken` on the URL /csrf
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@RestController
|
||||
public class CsrfController {
|
||||
|
||||
@RequestMapping("/csrf")
|
||||
public CsrfToken csrf(CsrfToken token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
The javascript can make a REST call to the endpoint and use the response to populate the headerName and the token.
|
||||
|
||||
We can now include the token in our Stomp client.
|
||||
For example:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
...
|
||||
var headers = {};
|
||||
headers[headerName] = token;
|
||||
stompClient.connect(headers, function(frame) {
|
||||
...
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
[[websocket-sameorigin-disable]]
|
||||
==== Disable CSRF within WebSockets
|
||||
|
||||
If you want to allow other domains to access your site, you can disable Spring Security's protection.
|
||||
For example, in Java Configuration you can use the following:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
...
|
||||
|
||||
@Override
|
||||
protected boolean sameOriginEnforced() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
[[websocket-sockjs]]
|
||||
=== Working with SockJS
|
||||
|
||||
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-fallback[SockJS] provides fallback transports to support older browsers.
|
||||
When using the fallback options we need to relax a few security constraints to allow SockJS to work with Spring Security.
|
||||
|
||||
[[websocket-sockjs-sameorigin]]
|
||||
==== SockJS & frame-options
|
||||
|
||||
SockJS may use an https://github.com/sockjs/sockjs-client/tree/v0.3.4[transport that leverages an iframe].
|
||||
By default Spring Security will <<headers-frame-options,deny>> the site from being framed to prevent Clickjacking attacks.
|
||||
To allow SockJS frame based transports to work, we need to configure Spring Security to allow the same origin to frame the content.
|
||||
|
||||
You can customize X-Frame-Options with the <<nsa-frame-options,frame-options>> element.
|
||||
For example, the following will instruct Spring Security to use "X-Frame-Options: SAMEORIGIN" which allows iframes within the same domain:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<http>
|
||||
<!-- ... -->
|
||||
|
||||
<headers>
|
||||
<frame-options
|
||||
policy="SAMEORIGIN" />
|
||||
</headers>
|
||||
</http>
|
||||
----
|
||||
|
||||
Similarly, you can customize frame options to use the same origin within Java Configuration using the following:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig extends
|
||||
WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// ...
|
||||
.headers()
|
||||
.frameOptions()
|
||||
.sameOrigin();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
[[websocket-sockjs-csrf]]
|
||||
==== SockJS & Relaxing CSRF
|
||||
|
||||
SockJS uses a POST on the CONNECT messages for any HTTP based transport.
|
||||
Typically we need to include the CSRF token in an HTTP header or an HTTP parameter.
|
||||
However, SockJS does not allow for these options.
|
||||
Instead, we must include the token in the Stomp headers as described in <<websocket-sameorigin-csrf>>.
|
||||
|
||||
It also means we need to relax our CSRF protection with the web layer.
|
||||
Specifically, we want to disable CSRF protection for our connect URLs.
|
||||
We do NOT want to disable CSRF protection for every URL.
|
||||
Otherwise our site will be vulnerable to CSRF attacks.
|
||||
|
||||
We can easily achieve this by providing a CSRF RequestMatcher.
|
||||
Our Java Configuration makes this extremely easy.
|
||||
For example, if our stomp endpoint is "/chat" we can disable CSRF protection for only URLs that start with "/chat/" using the following configuration:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityConfig
|
||||
extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
|
||||
http
|
||||
.csrf()
|
||||
// ignore our stomp endpoints since they are protected using Stomp headers
|
||||
.ignoringAntMatchers("/chat/**")
|
||||
.and()
|
||||
.headers()
|
||||
// allow same origin to frame our site to support iframe SockJS
|
||||
.frameOptions().sameOrigin()
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
|
||||
...
|
||||
----
|
||||
|
||||
If we are using XML based configuration, we can use the <<nsa-csrf-request-matcher-ref>>.
|
||||
For example:
|
||||
|
||||
[source,xml]
|
||||
----
|
||||
<http ...>
|
||||
<csrf request-matcher-ref="csrfMatcher"/>
|
||||
|
||||
<headers>
|
||||
<frame-options policy="SAMEORIGIN"/>
|
||||
</headers>
|
||||
|
||||
...
|
||||
</http>
|
||||
|
||||
<b:bean id="csrfMatcher"
|
||||
class="AndRequestMatcher">
|
||||
<b:constructor-arg value="#{T(org.springframework.security.web.csrf.CsrfFilter).DEFAULT_MATCHER}"/>
|
||||
<b:constructor-arg>
|
||||
<b:bean class="org.springframework.security.web.util.matcher.NegatedRequestMatcher">
|
||||
<b:bean class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
|
||||
<b:constructor-arg value="/chat/**"/>
|
||||
</b:bean>
|
||||
</b:bean>
|
||||
</b:constructor-arg>
|
||||
</b:bean>
|
||||
----
|
|
@ -4065,6 +4065,7 @@ Rounding out the anonymous authentication discussion is the `AuthenticationTrust
|
|||
|
||||
You will often see the `ROLE_ANONYMOUS` attribute in the above interceptor configuration replaced with `IS_AUTHENTICATED_ANONYMOUSLY`, which is effectively the same thing when defining access controls. This is an example of the use of the `AuthenticatedVoter` which we will see in the <<authz-authenticated-voter,authorization chapter>>. It uses an `AuthenticationTrustResolver` to process this particular configuration attribute and grant access to anonymous users. the `AuthenticatedVoter` approach is more powerful, since it allows you to differentiate between anonymous, remember-me and fully-authenticated users. If you don't need this functionality though, then you can stick with `ROLE_ANONYMOUS`, which will be processed by Spring Security's standard `RoleVoter`.
|
||||
|
||||
include::{include-dir}/websocket.adoc[]
|
||||
|
||||
[[authorization]]
|
||||
= Authorization
|
||||
|
|
|
@ -35,7 +35,10 @@ ext.slf4jVersion = '1.7.7'
|
|||
ext.spockVersion = '0.7-groovy-2.0'
|
||||
ext.springDataCommonsVersion = '1.9.1.RELEASE'
|
||||
ext.springDataJpaVersion = '1.7.1.RELEASE'
|
||||
ext.springDataRedisVersion = '1.4.1.RELEASE'
|
||||
ext.springSessionVersion = '1.0.0.RELEASE'
|
||||
ext.thymeleafVersion = '2.1.3.RELEASE'
|
||||
ext.thymeleafVersion = '1.2.7.RELEASE'
|
||||
|
||||
ext.spockDependencies = [
|
||||
dependencies.create("org.spockframework:spock-spring:$spockVersion") {
|
||||
|
|
|
@ -10,9 +10,10 @@ buildscript {
|
|||
apply plugin: 'tomcat'
|
||||
|
||||
dependencies {
|
||||
def tomcatVersion = '7.0.57'
|
||||
def tomcatVersion = '7.0.54'
|
||||
tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}",
|
||||
"org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
|
||||
"org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}",
|
||||
"org.apache.tomcat.embed:tomcat-embed-websocket:${tomcatVersion}"
|
||||
tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
|
||||
exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
apply from: WAR_SAMPLE_GRADLE
|
||||
|
||||
repositories {
|
||||
maven { url 'http://clojars.org/repo' }
|
||||
}
|
||||
|
||||
|
||||
dependencies {
|
||||
providedCompile "javax.servlet:javax.servlet-api:$servletApiVersion"
|
||||
|
||||
compile project(":spring-security-web"),
|
||||
project(":spring-security-config"),
|
||||
project(":spring-security-messaging"),
|
||||
project(":spring-security-data"),
|
||||
"org.springframework:spring-webmvc:${springVersion}",
|
||||
"org.springframework:spring-websocket:${springVersion}",
|
||||
"org.springframework:spring-messaging:${springVersion}",
|
||||
"org.springframework.session:spring-session:${springSessionVersion}",
|
||||
"org.springframework.data:spring-data-redis:${springDataRedisVersion}",
|
||||
"org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.0.Final",
|
||||
"org.hsqldb:hsqldb:$hsqlVersion",
|
||||
"javax.validation:validation-api:1.0.0.GA",
|
||||
"org.hibernate:hibernate-validator:4.2.0.Final",
|
||||
"redis.clients:jedis:2.4.2",
|
||||
"redis.embedded:embedded-redis:0.2",
|
||||
"org.apache.commons:commons-pool2:2.2",
|
||||
"org.thymeleaf:thymeleaf-spring4:$thymeleafVersion",
|
||||
"org.thymeleaf.extras:thymeleaf-extras-tiles2-spring4:2.1.1.RELEASE",
|
||||
"org.slf4j:slf4j-api:$slf4jVersion",
|
||||
"javax.servlet.jsp.jstl:javax.servlet.jsp.jstl-api:$jstlVersion",
|
||||
"com.fasterxml.jackson.core:jackson-databind:$jacksonDatavindVersion"
|
||||
compile('org.hibernate:hibernate-entitymanager:3.6.10.Final') {
|
||||
exclude group:'javassist', module: 'javassist'
|
||||
}
|
||||
compile("org.springframework.data:spring-data-jpa:$springDataJpaVersion") {
|
||||
exclude group:'org.aspectj', module:'aspectjrt'
|
||||
}
|
||||
|
||||
runtime "ch.qos.logback:logback-classic:$logbackVersion"
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package sample.config;
|
||||
|
||||
import org.springframework.web.filter.HiddenHttpMethodFilter;
|
||||
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
|
||||
|
||||
import javax.servlet.Filter;
|
||||
import javax.servlet.ServletRegistration;
|
||||
|
||||
public class ChatApplicationInitializer extends
|
||||
AbstractAnnotationConfigDispatcherServletInitializer {
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getRootConfigClasses() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?>[] getServletConfigClasses() {
|
||||
return new Class[] { WebMvcConfiguration.class };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getServletMappings() {
|
||||
return new String[] { "/" };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Filter[] getServletFilters() {
|
||||
return new Filter[] { new HiddenHttpMethodFilter() };
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
|
||||
registration.setInitParameter("dispatchOptionsRequest", "true");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
|
||||
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
|
||||
|
||||
/**
|
||||
* No customizations of {@link AbstractSecurityWebApplicationInitializer} are necessary.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class ChatSecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
|
||||
|
||||
@Override
|
||||
protected String getDispatcherWebApplicationContextSuffix() {
|
||||
return AbstractDispatcherServletInitializer.DEFAULT_SERVLET_NAME;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package sample.config;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.DependsOn;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import sample.data.User;
|
||||
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
|
||||
return builder.setType(EmbeddedDatabaseType.HSQL).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
|
||||
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
|
||||
vendorAdapter.setDatabase(Database.HSQL);
|
||||
vendorAdapter.setGenerateDdl(true);
|
||||
|
||||
LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
|
||||
factory.setJpaVendorAdapter(vendorAdapter);
|
||||
factory.setPackagesToScan(User.class.getPackage().getName());
|
||||
factory.setDataSource(dataSource());
|
||||
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn("entityManagerFactory")
|
||||
public ResourceDatabasePopulator initDatabase(DataSource dataSource) throws Exception {
|
||||
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
|
||||
populator.addScript(new ClassPathResource("data.sql"));
|
||||
populator.addScript(new ClassPathResource("password-encode.sql"));
|
||||
populator.populate(dataSource.getConnection());
|
||||
return populator;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PlatformTransactionManager transactionManager() {
|
||||
JpaTransactionManager txManager = new JpaTransactionManager();
|
||||
txManager.setEntityManagerFactory(entityManagerFactory().getObject());
|
||||
return txManager;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package sample.config;
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
/**
|
||||
* Runs an embedded Redis instance. This is only necessary since we do not want
|
||||
* users to have to setup a Redis instance. In a production environment, this
|
||||
* would not be used since a Redis Server would be setup.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Configuration
|
||||
public class EmbeddedRedisConfig {
|
||||
|
||||
@Bean
|
||||
public static RedisServerBean redisServer() {
|
||||
return new RedisServerBean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements BeanDefinitionRegistryPostProcessor to ensure this Bean
|
||||
* is initialized before any other Beans. Specifically, we want to ensure
|
||||
* that the Redis Server is started before RedisHttpSessionConfiguration
|
||||
* attempts to enable Keyspace notifications.
|
||||
*/
|
||||
static class RedisServerBean implements InitializingBean, DisposableBean, BeanDefinitionRegistryPostProcessor, RedisConnectionProperties {
|
||||
private RedisServer redisServer;
|
||||
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
redisServer = new RedisServer(getPort());
|
||||
redisServer.start();
|
||||
}
|
||||
|
||||
public void destroy() throws Exception {
|
||||
if(redisServer != null) {
|
||||
redisServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {}
|
||||
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
|
||||
|
||||
private Integer availablePort;
|
||||
|
||||
public int getPort() throws IOException {
|
||||
if(availablePort == null) {
|
||||
ServerSocket socket = new ServerSocket(0);
|
||||
availablePort = socket.getLocalPort();
|
||||
socket.close();
|
||||
}
|
||||
return availablePort;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Configuration
|
||||
@EnableRedisHttpSession
|
||||
public class RedisConfig {
|
||||
|
||||
@Bean
|
||||
public JedisConnectionFactory connectionFactory(RedisConnectionProperties conn) throws Exception {
|
||||
JedisConnectionFactory factory = new JedisConnectionFactory();
|
||||
factory.setPort(conn.getPort());
|
||||
return factory;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package sample.config;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public interface RedisConnectionProperties {
|
||||
int getPort() throws IOException;
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer;
|
||||
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class SessionAppInitializer
|
||||
extends AbstractHttpSessionApplicationInitializer {
|
||||
|
||||
@Override
|
||||
protected String getDispatcherWebApplicationContextSuffix() {
|
||||
return AbstractDispatcherServletInitializer.DEFAULT_SERVLET_NAME;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
import sample.data.UserRepository;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Configuration
|
||||
@EnableJpaRepositories(basePackageClasses = UserRepository.class)
|
||||
public class SpringDatatConfig {
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package sample.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.data.repository.support.DomainClassConverter;
|
||||
import org.springframework.format.support.FormattingConversionService;
|
||||
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
import org.thymeleaf.extras.tiles2.dialect.TilesDialect;
|
||||
import org.thymeleaf.extras.tiles2.spring4.web.configurer.ThymeleafTilesConfigurer;
|
||||
import org.thymeleaf.extras.tiles2.spring4.web.view.ThymeleafTilesView;
|
||||
import org.thymeleaf.spring4.SpringTemplateEngine;
|
||||
import org.thymeleaf.spring4.view.ThymeleafViewResolver;
|
||||
import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver;
|
||||
|
||||
@EnableWebMvc
|
||||
@ComponentScan("sample")
|
||||
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
|
||||
|
||||
@Autowired
|
||||
private FormattingConversionService mvcConversionService;
|
||||
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/login").setViewName("login");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/resources/**").addResourceLocations("classpath:/resources/").setCachePeriod(31556926);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
|
||||
configurer.enable();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ClassLoaderTemplateResolver templateResolver() {
|
||||
ClassLoaderTemplateResolver result = new ClassLoaderTemplateResolver();
|
||||
result.setPrefix("views/");
|
||||
result.setSuffix(".html");
|
||||
result.setTemplateMode("HTML5");
|
||||
return result;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThymeleafTilesConfigurer tilesConfigurer() {
|
||||
ThymeleafTilesConfigurer tilesConfigurer = new ThymeleafTilesConfigurer();
|
||||
tilesConfigurer.setDefinitions(new String[] { "classpath:tiles/tiles-def.xml"});
|
||||
return tilesConfigurer;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SpringTemplateEngine templateEngine(ClassLoaderTemplateResolver templateResolver) {
|
||||
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
|
||||
templateEngine.setTemplateResolver(templateResolver);
|
||||
templateEngine.addDialect(new TilesDialect());
|
||||
return templateEngine;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThymeleafViewResolver viewResolver(SpringTemplateEngine templateEngine) {
|
||||
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
|
||||
viewResolver.setTemplateEngine(templateEngine);
|
||||
viewResolver.setViewClass(ThymeleafTilesView.class);
|
||||
return viewResolver;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DomainClassConverter<?> domainClassConverter() {
|
||||
return new DomainClassConverter<FormattingConversionService>(mvcConversionService);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.data.repository.query.SecurityEvaluationContextExtension;
|
||||
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableGlobalMethodSecurity(prePostEnabled = true)
|
||||
public class WebSecurityConfig
|
||||
extends WebSecurityConfigurerAdapter {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
|
||||
http
|
||||
.headers()
|
||||
.frameOptions().sameOrigin()
|
||||
.and()
|
||||
.csrf()
|
||||
.ignoringAntMatchers("/chat/**")
|
||||
.and()
|
||||
.authorizeRequests()
|
||||
.antMatchers("/resources/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin()
|
||||
.loginPage("/login")
|
||||
.permitAll()
|
||||
.and()
|
||||
.logout()
|
||||
.permitAll();
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void configureGlobal(AuthenticationManagerBuilder auth, UserDetailsService userDetailsService) throws Exception {
|
||||
auth
|
||||
.userDetailsService(userDetailsService)
|
||||
.passwordEncoder(new BCryptPasswordEncoder());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
|
||||
return new SecurityEvaluationContextExtension();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package sample.config;
|
||||
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.session.ExpiringSession;
|
||||
import org.springframework.session.web.socket.config.annotation.AbstractSessionWebSocketMessageBrokerConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import sample.data.ActiveWebSocketUserRepository;
|
||||
import sample.websocket.WebSocketConnectHandler;
|
||||
import sample.websocket.WebSocketDisconnectHandler;
|
||||
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig<S extends ExpiringSession> extends AbstractSessionWebSocketMessageBrokerConfigurer<S> {
|
||||
|
||||
@Override
|
||||
protected void configureStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/chat")
|
||||
.withSockJS();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketConnectHandler webSocketConnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
|
||||
return new WebSocketConnectHandler(messagingTemplate, repository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebSocketDisconnectHandler webSocketDisconnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
|
||||
return new WebSocketDisconnectHandler(messagingTemplate, repository);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.config;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
|
||||
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
|
||||
|
||||
import static org.springframework.messaging.simp.SimpMessageType.MESSAGE;
|
||||
import static org.springframework.messaging.simp.SimpMessageType.SUBSCRIBE;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
// message types other than MESSAGE and SUBSCRIBE
|
||||
.nullDestMatcher().authenticated()
|
||||
// anyone can subscribe to the errors
|
||||
.simpSubscribeDestMatchers("/user/queue/errors").permitAll()
|
||||
// matches any destination that starts with /app/
|
||||
.simpDestMatchers("/app/**").authenticated()
|
||||
// matches any destination for SimpMessageType.SUBSCRIBE that starts with /user/ or /topic/friends/
|
||||
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").authenticated()
|
||||
|
||||
|
||||
// (i.e. cannot send messages directly to /topic/, /queue/)
|
||||
// (i.e. cannot subscribe to /topic/messages/* to get messages sent to /topic/messages-user<id>)
|
||||
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll()
|
||||
// catch all
|
||||
.anyMessage().denyAll();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package sample.data;
|
||||
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class ActiveWebSocketUser {
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
private String username;
|
||||
|
||||
private Calendar connectionTime;
|
||||
|
||||
public ActiveWebSocketUser() {}
|
||||
|
||||
public ActiveWebSocketUser(String id, String username, Calendar connectionTime) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.connectionTime = connectionTime;
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public Calendar getConnectionTime() {
|
||||
return connectionTime;
|
||||
}
|
||||
|
||||
public void setConnectionTime(Calendar connectionTime) {
|
||||
this.connectionTime = connectionTime;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package sample.data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
public interface ActiveWebSocketUserRepository extends CrudRepository<ActiveWebSocketUser,String> {
|
||||
|
||||
@Query("select DISTINCT(u.username) from ActiveWebSocketUser u where u.username != ?#{principal?.username}")
|
||||
List<String> findAllActiveUsers();
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package sample.data;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
public class InstantMessage {
|
||||
private String to;
|
||||
|
||||
private String from;
|
||||
|
||||
private String message;
|
||||
|
||||
private Calendar created = Calendar.getInstance();
|
||||
|
||||
public String getTo() {
|
||||
return to;
|
||||
}
|
||||
|
||||
public void setTo(String to) {
|
||||
this.to = to;
|
||||
}
|
||||
|
||||
public String getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public void setFrom(String from) {
|
||||
this.from = from;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public Calendar getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Calendar created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import javax.persistence.Column;
|
||||
import javax.persistence.Entity;
|
||||
import javax.persistence.GeneratedValue;
|
||||
import javax.persistence.GenerationType;
|
||||
import javax.persistence.Id;
|
||||
|
||||
import org.hibernate.validator.constraints.Email;
|
||||
import org.hibernate.validator.constraints.NotEmpty;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
/**
|
||||
* Represents a user in our system.
|
||||
*
|
||||
* <p>
|
||||
* In a real system use {@link PasswordEncoder} to ensure the password is secured
|
||||
* properly. This demonstration does not address this due to time restrictions.
|
||||
* </p>
|
||||
*
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Entity
|
||||
public class User implements Serializable {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@NotEmpty(message = "First name is required.")
|
||||
private String firstName;
|
||||
|
||||
@NotEmpty(message = "Last name is required.")
|
||||
private String lastName;
|
||||
|
||||
@Email(message = "Please provide a valid email address.")
|
||||
@NotEmpty(message = "Email is required.")
|
||||
@Column(unique=true, nullable = false)
|
||||
private String email;
|
||||
|
||||
@NotEmpty(message = "Password is required.")
|
||||
private String password;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(User user) {
|
||||
this.id = user.id;
|
||||
this.firstName = user.firstName;
|
||||
this.lastName = user.lastName;
|
||||
this.email = user.email;
|
||||
this.password = user.password;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((id == null) ? 0 : id.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
User other = (User) obj;
|
||||
if (id == null) {
|
||||
if (other.id != null)
|
||||
return false;
|
||||
} else if (!id.equals(other.id))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static final long serialVersionUID = 2738859149330833739L;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.data;
|
||||
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
|
||||
/**
|
||||
* Allows managing {@link User} instances.
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
public interface UserRepository extends CrudRepository<User, Long> {
|
||||
|
||||
User findByEmail(String email);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.mvc;
|
||||
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@RestController
|
||||
public class CsrfController {
|
||||
|
||||
@RequestMapping("/csrf")
|
||||
public CsrfToken csrf(CsrfToken csrfToken) {
|
||||
return csrfToken;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.mvc;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
@Controller
|
||||
public class LoginController {
|
||||
@RequestMapping("/login")
|
||||
public String login() {
|
||||
return "login";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.mvc;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.messaging.simp.annotation.SubscribeMapping;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import sample.data.ActiveWebSocketUserRepository;
|
||||
import sample.data.InstantMessage;
|
||||
import sample.data.User;
|
||||
import sample.security.CurrentUser;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@Controller
|
||||
public class MessageController {
|
||||
private SimpMessageSendingOperations messagingTemplate;
|
||||
private ActiveWebSocketUserRepository activeUserRepository;
|
||||
|
||||
@Autowired
|
||||
public MessageController(ActiveWebSocketUserRepository activeUserRepository,SimpMessageSendingOperations messagingTemplate) {
|
||||
this.activeUserRepository = activeUserRepository;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
@RequestMapping("/")
|
||||
public String chat() {
|
||||
return "chat";
|
||||
}
|
||||
|
||||
@MessageMapping("/im")
|
||||
public void im(InstantMessage im, @CurrentUser User user) {
|
||||
im.setFrom(user.getEmail());
|
||||
messagingTemplate.convertAndSendToUser(im.getTo(),"/queue/messages",im);
|
||||
messagingTemplate.convertAndSendToUser(im.getFrom(),"/queue/messages",im);
|
||||
}
|
||||
|
||||
@SubscribeMapping("/users")
|
||||
public List<String> subscribeMessages() throws Exception {
|
||||
return activeUserRepository.findAllActiveUsers();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.security;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
/**
|
||||
* Annotate Spring MVC method arguments with this annotation to indicate you
|
||||
* wish to specify the argument with the value of the current
|
||||
* {@link Authentication#getPrincipal()} found on the
|
||||
* {@link SecurityContextHolder}.
|
||||
*
|
||||
* <p>
|
||||
* Creating your own annotation that uses {@link AuthenticationPrincipal} as a
|
||||
* meta annotation creates a layer of indirection between your code and Spring
|
||||
* Security's. For simplicity, you could instead use the
|
||||
* {@link AuthenticationPrincipal} directly.
|
||||
* </p>
|
||||
*
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@Target(ElementType.PARAMETER)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@AuthenticationPrincipal
|
||||
public @interface CurrentUser {
|
||||
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package sample.security;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import sample.data.User;
|
||||
import sample.data.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*
|
||||
*/
|
||||
@Service
|
||||
public class UserRepositoryUserDetailsService implements UserDetailsService {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public UserRepositoryUserDetailsService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
|
||||
*/
|
||||
public UserDetails loadUserByUsername(String username)
|
||||
throws UsernameNotFoundException {
|
||||
User user = userRepository.findByEmail(username);
|
||||
if(user == null) {
|
||||
throw new UsernameNotFoundException("Could not find user " + username);
|
||||
}
|
||||
return new CustomUserDetails(user);
|
||||
}
|
||||
|
||||
private final static class CustomUserDetails extends User implements UserDetails {
|
||||
|
||||
private CustomUserDetails(User user) {
|
||||
super(user);
|
||||
}
|
||||
|
||||
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||
return AuthorityUtils.createAuthorityList("ROLE_USER");
|
||||
}
|
||||
|
||||
public String getUsername() {
|
||||
return getEmail();
|
||||
}
|
||||
|
||||
public boolean isAccountNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isAccountNonLocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isCredentialsNonExpired() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static final long serialVersionUID = 5639683223516504866L;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.websocket;
|
||||
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.messaging.MessageHeaders;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.web.socket.messaging.SessionConnectEvent;
|
||||
import sample.data.ActiveWebSocketUser;
|
||||
import sample.data.ActiveWebSocketUserRepository;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Arrays;
|
||||
import java.util.Calendar;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class WebSocketConnectHandler implements ApplicationListener<SessionConnectEvent> {
|
||||
private ActiveWebSocketUserRepository repository;
|
||||
private SimpMessageSendingOperations messagingTemplate;
|
||||
|
||||
public WebSocketConnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
|
||||
super();
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void onApplicationEvent(SessionConnectEvent event) {
|
||||
MessageHeaders headers = event.getMessage().getHeaders();
|
||||
Principal user = SimpMessageHeaderAccessor.getUser(headers);
|
||||
if(user == null) {
|
||||
return;
|
||||
}
|
||||
String id = SimpMessageHeaderAccessor.getSessionId(headers);
|
||||
repository.save(new ActiveWebSocketUser(id, user.getName(), Calendar.getInstance()));
|
||||
messagingTemplate.convertAndSend("/topic/friends/signin", Arrays.asList(user.getName()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2002-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package sample.websocket;
|
||||
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
|
||||
import sample.data.ActiveWebSocketUser;
|
||||
import sample.data.ActiveWebSocketUserRepository;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class WebSocketDisconnectHandler implements ApplicationListener<SessionDisconnectEvent> {
|
||||
private ActiveWebSocketUserRepository repository;
|
||||
private SimpMessageSendingOperations messagingTemplate;
|
||||
|
||||
public WebSocketDisconnectHandler(SimpMessageSendingOperations messagingTemplate, ActiveWebSocketUserRepository repository) {
|
||||
super();
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public void onApplicationEvent(SessionDisconnectEvent event) {
|
||||
String id = event.getSessionId();
|
||||
if(id == null) {
|
||||
return;
|
||||
}
|
||||
ActiveWebSocketUser user = repository.findOne(id);
|
||||
if(user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
repository.delete(id);
|
||||
|
||||
messagingTemplate.convertAndSend("/topic/friends/signout", Arrays.asList(user.getUsername()));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
insert into user(id,email,password,firstname,lastname) values (0,'rob@example.com','password','Rob','Winch');
|
||||
insert into user(id,email,password,firstname,lastname) values (1,'luke@example.com','password','Luke','Taylor');
|
||||
insert into user(id,email,password,firstname,lastname) values (2,'eve@example.com','password','Eve','Sdropper');
|
|
@ -0,0 +1 @@
|
|||
update user set password = '$2a$10$FBAKClV1zBIOOC9XMXf3AO8RoGXYVYsfvUdoLxGkd/BnXEn4tqT3u';
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"name": "bootstrap",
|
||||
"description": "Sleek, intuitive, and powerful front-end framework for faster and easier web development. This is the version that should be available to bower.",
|
||||
"version": "2.3.2",
|
||||
"keywords": [
|
||||
"bootstrap",
|
||||
"css",
|
||||
"bootstrap.js"
|
||||
],
|
||||
"homepage": "http://twitter.github.com/bootstrap/",
|
||||
"author": {
|
||||
"name": "Twitter Inc."
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/bowerjs/bootstrap.git"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "Apache-2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0"
|
||||
}
|
||||
],
|
||||
"readme": "[Twitter Bootstrap](http://twitter.github.com/bootstrap) [![Build Status](https://secure.travis-ci.org/twitter/bootstrap.png)](http://travis-ci.org/twitter/bootstrap)\n=================\n\nBootstrap is a sleek, intuitive, and powerful front-end framework for faster and easier web development, created and maintained by [Mark Otto](http://twitter.com/mdo) and [Jacob Thornton](http://twitter.com/fat) at Twitter.\n\nTo get started, checkout http://getbootstrap.com!\n\n\n\nQuick start\n-----------\n\nClone the repo, `git clone git://github.com/twitter/bootstrap.git`, or [download the latest release](https://github.com/twitter/bootstrap/zipball/master).\n\n\n\nVersioning\n----------\n\nFor transparency and insight into our release cycle, and for striving to maintain backward compatibility, Bootstrap will be maintained under the Semantic Versioning guidelines as much as possible.\n\nReleases will be numbered with the following format:\n\n`<major>.<minor>.<patch>`\n\nAnd constructed with the following guidelines:\n\n* Breaking backward compatibility bumps the major (and resets the minor and patch)\n* New additions without breaking backward compatibility bumps the minor (and resets the patch)\n* Bug fixes and misc changes bumps the patch\n\nFor more information on SemVer, please visit http://semver.org/.\n\n\n\nBug tracker\n-----------\n\nHave a bug? Please create an issue here on GitHub that conforms with [necolas's guidelines](https://github.com/necolas/issue-guidelines).\n\nhttps://github.com/twitter/bootstrap/issues\n\n\n\nTwitter account\n---------------\n\nKeep up to date on announcements and more by following Bootstrap on Twitter, [@TwBootstrap](http://twitter.com/TwBootstrap).\n\n\n\nBlog\n----\n\nRead more detailed announcements, discussions, and more on [The Official Twitter Bootstrap Blog](http://blog.getbootstrap.com).\n\n\n\nMailing list\n------------\n\nHave a question? Ask on our mailing list!\n\ntwitter-bootstrap@googlegroups.com\n\nhttp://groups.google.com/group/twitter-bootstrap\n\n\n\nIRC\n---\n\nServer: irc.freenode.net\n\nChannel: ##twitter-bootstrap (the double ## is not a typo)\n\n\n\nDevelopers\n----------\n\nWe have included a makefile with convenience methods for working with the Bootstrap library.\n\n+ **dependencies**\nOur makefile depends on you having recess, connect, uglify.js, and jshint installed. To install, just run the following command in npm:\n\n```\n$ npm install recess connect uglify-js jshint -g\n```\n\n+ **build** - `make`\nRuns the recess compiler to rebuild the `/less` files and compiles the docs pages. Requires recess and uglify-js. <a href=\"http://twitter.github.com/bootstrap/less.html#compiling\">Read more in our docs »</a>\n\n+ **test** - `make test`\nRuns jshint and qunit tests headlessly in [phantomjs](http://code.google.com/p/phantomjs/) (used for ci). Depends on having phantomjs installed.\n\n+ **watch** - `make watch`\nThis is a convenience method for watching just Less files and automatically building them whenever you save. Requires the Watchr gem.\n\n\n\nContributing\n------------\n\nPlease submit all pull requests against *-wip branches. If your unit test contains javascript patches or features, you must include relevant unit tests. Thanks!\n\n\n\nAuthors\n-------\n\n**Mark Otto**\n\n+ http://twitter.com/mdo\n+ http://github.com/markdotto\n\n**Jacob Thornton**\n\n+ http://twitter.com/fat\n+ http://github.com/fat\n\n\n\nCopyright and license\n---------------------\n\nCopyright 2012 Twitter, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this work except in compliance with the License.\nYou may obtain a copy of the License in the LICENSE file, or at:\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n",
|
||||
"_id": "bootstrap@2.1.1",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"components"
|
||||
],
|
||||
"_release": "2.3.2",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "v2.3.2",
|
||||
"commit": "48e1111cc7fbd6a1e6b0ecab37c6f5e07c2cc3ae"
|
||||
},
|
||||
"_source": "git://github.com/bowerjs/bootstrap.git",
|
||||
"_target": "~2.3",
|
||||
"_originalSource": "git://github.com/bowerjs/bootstrap.git"
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
[Twitter Bootstrap](http://twitter.github.com/bootstrap) [![Build Status](https://secure.travis-ci.org/twitter/bootstrap.png)](http://travis-ci.org/twitter/bootstrap)
|
||||
=================
|
||||
|
||||
This version of Bootstrap is maintained to be only the built version of bootstrap to be used with bower. If you are looking for the full source of bootstrap go to [bootstrap](https://github.com/twitter/bootstrap)
|
||||
|
||||
Bootstrap is a sleek, intuitive, and powerful front-end framework for faster and easier web development, created and maintained by [Mark Otto](http://twitter.com/mdo) and [Jacob Thornton](http://twitter.com/fat) at Twitter.
|
||||
|
||||
To get started, checkout http://getbootstrap.com!
|
||||
|
||||
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
Clone the repo, `git clone git://github.com/twitter/bootstrap.git`, or [download the latest release](https://github.com/twitter/bootstrap/zipball/master).
|
||||
|
||||
|
||||
|
||||
Versioning
|
||||
----------
|
||||
|
||||
For transparency and insight into our release cycle, and for striving to maintain backward compatibility, Bootstrap will be maintained under the Semantic Versioning guidelines as much as possible.
|
||||
|
||||
Releases will be numbered with the following format:
|
||||
|
||||
`<major>.<minor>.<patch>`
|
||||
|
||||
And constructed with the following guidelines:
|
||||
|
||||
* Breaking backward compatibility bumps the major (and resets the minor and patch)
|
||||
* New additions without breaking backward compatibility bumps the minor (and resets the patch)
|
||||
* Bug fixes and misc changes bumps the patch
|
||||
|
||||
For more information on SemVer, please visit http://semver.org/.
|
||||
|
||||
|
||||
|
||||
Bug tracker
|
||||
-----------
|
||||
|
||||
Have a bug? Please create an issue here on GitHub that conforms with [necolas's guidelines](https://github.com/necolas/issue-guidelines).
|
||||
|
||||
https://github.com/twitter/bootstrap/issues
|
||||
|
||||
|
||||
|
||||
Twitter account
|
||||
---------------
|
||||
|
||||
Keep up to date on announcements and more by following Bootstrap on Twitter, [@TwBootstrap](http://twitter.com/TwBootstrap).
|
||||
|
||||
|
||||
|
||||
Blog
|
||||
----
|
||||
|
||||
Read more detailed announcements, discussions, and more on [The Official Twitter Bootstrap Blog](http://blog.getbootstrap.com).
|
||||
|
||||
|
||||
|
||||
Mailing list
|
||||
------------
|
||||
|
||||
Have a question? Ask on our mailing list!
|
||||
|
||||
twitter-bootstrap@googlegroups.com
|
||||
|
||||
http://groups.google.com/group/twitter-bootstrap
|
||||
|
||||
|
||||
|
||||
IRC
|
||||
---
|
||||
|
||||
Server: irc.freenode.net
|
||||
|
||||
Channel: ##twitter-bootstrap (the double ## is not a typo)
|
||||
|
||||
|
||||
|
||||
Developers
|
||||
----------
|
||||
|
||||
We have included a makefile with convenience methods for working with the Bootstrap library.
|
||||
|
||||
+ **dependencies**
|
||||
Our makefile depends on you having recess, connect, uglify.js, and jshint installed. To install, just run the following command in npm:
|
||||
|
||||
```
|
||||
$ npm install recess connect uglify-js jshint -g
|
||||
```
|
||||
|
||||
+ **build** - `make`
|
||||
Runs the recess compiler to rebuild the `/less` files and compiles the docs pages. Requires recess and uglify-js. <a href="http://twitter.github.com/bootstrap/less.html#compiling">Read more in our docs »</a>
|
||||
|
||||
+ **test** - `make test`
|
||||
Runs jshint and qunit tests headlessly in [phantomjs](http://code.google.com/p/phantomjs/) (used for ci). Depends on having phantomjs installed.
|
||||
|
||||
+ **watch** - `make watch`
|
||||
This is a convenience method for watching just Less files and automatically building them whenever you save. Requires the Watchr gem.
|
||||
|
||||
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Please submit all pull requests against *-wip branches. If your unit test contains javascript patches or features, you must include relevant unit tests. Thanks!
|
||||
|
||||
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
**Mark Otto**
|
||||
|
||||
+ http://twitter.com/mdo
|
||||
+ http://github.com/markdotto
|
||||
|
||||
**Jacob Thornton**
|
||||
|
||||
+ http://twitter.com/fat
|
||||
+ http://github.com/fat
|
||||
|
||||
|
||||
|
||||
Copyright and license
|
||||
---------------------
|
||||
|
||||
Copyright 2012 Twitter, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this work except in compliance with the License.
|
||||
You may obtain a copy of the License in the LICENSE file, or at:
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "bootstrap",
|
||||
"description": "Sleek, intuitive, and powerful front-end framework for faster and easier web development. This is the version that should be available to bower.",
|
||||
"version": "2.3.2",
|
||||
"keywords": [
|
||||
"bootstrap",
|
||||
"css",
|
||||
"bootstrap.js"
|
||||
],
|
||||
"homepage": "http://twitter.github.com/bootstrap/",
|
||||
"author": {
|
||||
"name": "Twitter Inc."
|
||||
},
|
||||
"scripts": {
|
||||
"test": "make test"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/bowerjs/bootstrap.git"
|
||||
},
|
||||
"licenses": [
|
||||
{
|
||||
"type": "Apache-2.0",
|
||||
"url": "http://www.apache.org/licenses/LICENSE-2.0"
|
||||
}
|
||||
],
|
||||
"readme": "[Twitter Bootstrap](http://twitter.github.com/bootstrap) [![Build Status](https://secure.travis-ci.org/twitter/bootstrap.png)](http://travis-ci.org/twitter/bootstrap)\n=================\n\nBootstrap is a sleek, intuitive, and powerful front-end framework for faster and easier web development, created and maintained by [Mark Otto](http://twitter.com/mdo) and [Jacob Thornton](http://twitter.com/fat) at Twitter.\n\nTo get started, checkout http://getbootstrap.com!\n\n\n\nQuick start\n-----------\n\nClone the repo, `git clone git://github.com/twitter/bootstrap.git`, or [download the latest release](https://github.com/twitter/bootstrap/zipball/master).\n\n\n\nVersioning\n----------\n\nFor transparency and insight into our release cycle, and for striving to maintain backward compatibility, Bootstrap will be maintained under the Semantic Versioning guidelines as much as possible.\n\nReleases will be numbered with the following format:\n\n`<major>.<minor>.<patch>`\n\nAnd constructed with the following guidelines:\n\n* Breaking backward compatibility bumps the major (and resets the minor and patch)\n* New additions without breaking backward compatibility bumps the minor (and resets the patch)\n* Bug fixes and misc changes bumps the patch\n\nFor more information on SemVer, please visit http://semver.org/.\n\n\n\nBug tracker\n-----------\n\nHave a bug? Please create an issue here on GitHub that conforms with [necolas's guidelines](https://github.com/necolas/issue-guidelines).\n\nhttps://github.com/twitter/bootstrap/issues\n\n\n\nTwitter account\n---------------\n\nKeep up to date on announcements and more by following Bootstrap on Twitter, [@TwBootstrap](http://twitter.com/TwBootstrap).\n\n\n\nBlog\n----\n\nRead more detailed announcements, discussions, and more on [The Official Twitter Bootstrap Blog](http://blog.getbootstrap.com).\n\n\n\nMailing list\n------------\n\nHave a question? Ask on our mailing list!\n\ntwitter-bootstrap@googlegroups.com\n\nhttp://groups.google.com/group/twitter-bootstrap\n\n\n\nIRC\n---\n\nServer: irc.freenode.net\n\nChannel: ##twitter-bootstrap (the double ## is not a typo)\n\n\n\nDevelopers\n----------\n\nWe have included a makefile with convenience methods for working with the Bootstrap library.\n\n+ **dependencies**\nOur makefile depends on you having recess, connect, uglify.js, and jshint installed. To install, just run the following command in npm:\n\n```\n$ npm install recess connect uglify-js jshint -g\n```\n\n+ **build** - `make`\nRuns the recess compiler to rebuild the `/less` files and compiles the docs pages. Requires recess and uglify-js. <a href=\"http://twitter.github.com/bootstrap/less.html#compiling\">Read more in our docs »</a>\n\n+ **test** - `make test`\nRuns jshint and qunit tests headlessly in [phantomjs](http://code.google.com/p/phantomjs/) (used for ci). Depends on having phantomjs installed.\n\n+ **watch** - `make watch`\nThis is a convenience method for watching just Less files and automatically building them whenever you save. Requires the Watchr gem.\n\n\n\nContributing\n------------\n\nPlease submit all pull requests against *-wip branches. If your unit test contains javascript patches or features, you must include relevant unit tests. Thanks!\n\n\n\nAuthors\n-------\n\n**Mark Otto**\n\n+ http://twitter.com/mdo\n+ http://github.com/markdotto\n\n**Jacob Thornton**\n\n+ http://twitter.com/fat\n+ http://github.com/fat\n\n\n\nCopyright and license\n---------------------\n\nCopyright 2012 Twitter, Inc.\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this work except in compliance with the License.\nYou may obtain a copy of the License in the LICENSE file, or at:\n\n http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n",
|
||||
"_id": "bootstrap@2.1.1",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"components"
|
||||
]
|
||||
}
|
1109
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap-responsive.css
vendored
Normal file
1109
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap-responsive.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap-responsive.min.css
vendored
Normal file
9
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap-responsive.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
6167
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap.css
vendored
Normal file
6167
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
9
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap.min.css
vendored
Normal file
9
samples/chat-jc/src/main/resources/resources/js/bootstrap/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
After Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
File diff suppressed because it is too large
Load Diff
6
samples/chat-jc/src/main/resources/resources/js/bootstrap/js/bootstrap.min.js
vendored
Normal file
6
samples/chat-jc/src/main/resources/resources/js/bootstrap/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "jquery",
|
||||
"version": "1.8.2",
|
||||
"main": "./jquery.js",
|
||||
"dependencies": {},
|
||||
"homepage": "https://github.com/bowerjs/jquery",
|
||||
"_release": "1.8.2",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "v1.8.2",
|
||||
"commit": "761af8850c734e2d6da2434dc6d63c341974d8a0"
|
||||
},
|
||||
"_source": "git://github.com/bowerjs/jquery.git",
|
||||
"_target": "~1.8",
|
||||
"_originalSource": "git://github.com/bowerjs/jquery.git"
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"name" : "jquery",
|
||||
"version" : "1.8.2",
|
||||
"main" : "./jquery.js",
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"name": "knockout",
|
||||
"version": "2.3.0",
|
||||
"main": "knockout.js",
|
||||
"scripts": [
|
||||
"knockout.js"
|
||||
],
|
||||
"dependencies": {},
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"components"
|
||||
],
|
||||
"homepage": "https://github.com/bowerjs/knockout",
|
||||
"_release": "2.3.0",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "2.3.0",
|
||||
"commit": "cd433fbf32abab7da5b2df204dea22f862354992"
|
||||
},
|
||||
"_source": "git://github.com/bowerjs/knockout.git",
|
||||
"_target": "~2.3",
|
||||
"_originalSource": "git://github.com/bowerjs/knockout.git"
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
knockout
|
||||
========
|
||||
|
||||
compiled knockout libs for bowerjs
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"name": "knockout",
|
||||
"version": "2.3.0",
|
||||
"main": "knockout.js",
|
||||
"scripts": [
|
||||
"knockout.js"
|
||||
],
|
||||
"dependencies": {},
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"components"
|
||||
]
|
||||
}
|
3676
samples/chat-jc/src/main/resources/resources/js/knockout/knockout-2.3.0.debug.js
vendored
Normal file
3676
samples/chat-jc/src/main/resources/resources/js/knockout/knockout-2.3.0.debug.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,88 @@
|
|||
// Knockout JavaScript library v2.3.0
|
||||
// (c) Steven Sanderson - http://knockoutjs.com/
|
||||
// License: MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||
|
||||
(function() {function F(q){return function(){return q}};(function(q){var w=this||(0,eval)("this"),s=w.document,H=w.navigator,t=w.jQuery,y=w.JSON;(function(q){"function"===typeof require&&"object"===typeof exports&&"object"===typeof module?q(module.exports||exports):"function"===typeof define&&define.amd?define(["exports"],q):q(w.ko={})})(function(C){function G(b,c,d,f){a.d[b]={init:function(b){a.a.f.set(b,I,{});return{controlsDescendantBindings:!0}},update:function(b,e,m,h,k){m=a.a.f.get(b,I);e=a.a.c(e());h=!d!==!e;var l=!m.fb;if(l||c||h!==m.vb)l&&(m.fb=
|
||||
a.a.Oa(a.e.childNodes(b),!0)),h?(l||a.e.P(b,a.a.Oa(m.fb)),a.Ja(f?f(k,e):k,b)):a.e.ba(b),m.vb=h}};a.g.S[b]=!1;a.e.L[b]=!0}function J(b,c,d){d&&c!==a.h.n(b)&&a.h.W(b,c);c!==a.h.n(b)&&a.q.I(a.a.Ga,null,[b,"change"])}var a="undefined"!==typeof C?C:{};a.b=function(b,c){for(var d=b.split("."),f=a,g=0;g<d.length-1;g++)f=f[d[g]];f[d[d.length-1]]=c};a.r=function(a,c,d){a[c]=d};a.version="2.3.0";a.b("version",a.version);a.a=function(){function b(a,b){for(var e in a)a.hasOwnProperty(e)&&b(e,a[e])}function c(b,
|
||||
e){if("input"!==a.a.u(b)||!b.type||"click"!=e.toLowerCase())return!1;var k=b.type;return"checkbox"==k||"radio"==k}var d={},f={};d[H&&/Firefox\/2/i.test(H.userAgent)?"KeyboardEvent":"UIEvents"]=["keyup","keydown","keypress"];d.MouseEvents="click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave".split(" ");b(d,function(a,b){if(b.length)for(var e=0,c=b.length;e<c;e++)f[b[e]]=a});var g={propertychange:!0},e=s&&function(){for(var a=3,b=s.createElement("div"),e=b.getElementsByTagName("i");b.innerHTML=
|
||||
"\x3c!--[if gt IE "+ ++a+"]><i></i><![endif]--\x3e",e[0];);return 4<a?a:q}();return{Ta:["authenticity_token",/^__RequestVerificationToken(_.*)?$/],p:function(a,b){for(var e=0,c=a.length;e<c;e++)b(a[e])},k:function(a,b){if("function"==typeof Array.prototype.indexOf)return Array.prototype.indexOf.call(a,b);for(var e=0,c=a.length;e<c;e++)if(a[e]===b)return e;return-1},La:function(a,b,e){for(var c=0,d=a.length;c<d;c++)if(b.call(e,a[c]))return a[c];return null},ka:function(b,e){var c=a.a.k(b,e);0<=c&&
|
||||
b.splice(c,1)},Ma:function(b){b=b||[];for(var e=[],c=0,d=b.length;c<d;c++)0>a.a.k(e,b[c])&&e.push(b[c]);return e},Z:function(a,b){a=a||[];for(var e=[],c=0,d=a.length;c<d;c++)e.push(b(a[c]));return e},Y:function(a,b){a=a||[];for(var e=[],c=0,d=a.length;c<d;c++)b(a[c])&&e.push(a[c]);return e},R:function(a,b){if(b instanceof Array)a.push.apply(a,b);else for(var e=0,c=b.length;e<c;e++)a.push(b[e]);return a},ja:function(b,e,c){var d=b.indexOf?b.indexOf(e):a.a.k(b,e);0>d?c&&b.push(e):c||b.splice(d,1)},
|
||||
extend:function(a,b){if(b)for(var e in b)b.hasOwnProperty(e)&&(a[e]=b[e]);return a},w:b,oa:function(b){for(;b.firstChild;)a.removeNode(b.firstChild)},Mb:function(b){b=a.a.N(b);for(var e=s.createElement("div"),c=0,d=b.length;c<d;c++)e.appendChild(a.H(b[c]));return e},Oa:function(b,e){for(var c=0,d=b.length,g=[];c<d;c++){var f=b[c].cloneNode(!0);g.push(e?a.H(f):f)}return g},P:function(b,e){a.a.oa(b);if(e)for(var c=0,d=e.length;c<d;c++)b.appendChild(e[c])},eb:function(b,e){var c=b.nodeType?[b]:b;if(0<
|
||||
c.length){for(var d=c[0],g=d.parentNode,f=0,r=e.length;f<r;f++)g.insertBefore(e[f],d);f=0;for(r=c.length;f<r;f++)a.removeNode(c[f])}},hb:function(a,b){7>e?a.setAttribute("selected",b):a.selected=b},F:function(a){return null===a||a===q?"":a.trim?a.trim():a.toString().replace(/^[\s\xa0]+|[\s\xa0]+$/g,"")},Wb:function(b,e){for(var c=[],d=(b||"").split(e),g=0,f=d.length;g<f;g++){var r=a.a.F(d[g]);""!==r&&c.push(r)}return c},Tb:function(a,b){a=a||"";return b.length>a.length?!1:a.substring(0,b.length)===
|
||||
b},yb:function(a,b){if(b.compareDocumentPosition)return 16==(b.compareDocumentPosition(a)&16);for(;null!=a;){if(a==b)return!0;a=a.parentNode}return!1},aa:function(b){return a.a.yb(b,b.ownerDocument)},pb:function(b){return!!a.a.La(b,a.a.aa)},u:function(a){return a&&a.tagName&&a.tagName.toLowerCase()},o:function(b,d,k){var f=e&&g[d];if(f||"undefined"==typeof t)if(f||"function"!=typeof b.addEventListener)if("undefined"!=typeof b.attachEvent){var n=function(a){k.call(b,a)},p="on"+d;b.attachEvent(p,n);
|
||||
a.a.C.ia(b,function(){b.detachEvent(p,n)})}else throw Error("Browser doesn't support addEventListener or attachEvent");else b.addEventListener(d,k,!1);else{if(c(b,d)){var r=k;k=function(a,b){var e=this.checked;b&&(this.checked=!0!==b.sb);r.call(this,a);this.checked=e}}t(b).bind(d,k)}},Ga:function(a,b){if(!a||!a.nodeType)throw Error("element must be a DOM node when calling triggerEvent");if("undefined"!=typeof t){var e=[];c(a,b)&&e.push({sb:a.checked});t(a).trigger(b,e)}else if("function"==typeof s.createEvent)if("function"==
|
||||
typeof a.dispatchEvent)e=s.createEvent(f[b]||"HTMLEvents"),e.initEvent(b,!0,!0,w,0,0,0,0,0,!1,!1,!1,!1,0,a),a.dispatchEvent(e);else throw Error("The supplied element doesn't support dispatchEvent");else if("undefined"!=typeof a.fireEvent)c(a,b)&&(a.checked=!0!==a.checked),a.fireEvent("on"+b);else throw Error("Browser doesn't support triggering events");},c:function(b){return a.T(b)?b():b},ya:function(b){return a.T(b)?b.t():b},ga:function(b,e,c){if(e){var d=/\S+/g,g=b.className.match(d)||[];a.a.p(e.match(d),
|
||||
function(b){a.a.ja(g,b,c)});b.className=g.join(" ")}},ib:function(b,e){var c=a.a.c(e);if(null===c||c===q)c="";var d=a.e.firstChild(b);!d||3!=d.nodeType||a.e.nextSibling(d)?a.e.P(b,[s.createTextNode(c)]):d.data=c;a.a.Bb(b)},gb:function(a,b){a.name=b;if(7>=e)try{a.mergeAttributes(s.createElement("<input name='"+a.name+"'/>"),!1)}catch(c){}},Bb:function(a){9<=e&&(a=1==a.nodeType?a:a.parentNode,a.style&&(a.style.zoom=a.style.zoom))},zb:function(a){if(e){var b=a.style.width;a.style.width=0;a.style.width=
|
||||
b}},Qb:function(b,e){b=a.a.c(b);e=a.a.c(e);for(var c=[],d=b;d<=e;d++)c.push(d);return c},N:function(a){for(var b=[],e=0,c=a.length;e<c;e++)b.push(a[e]);return b},Ub:6===e,Vb:7===e,ca:e,Ua:function(b,e){for(var c=a.a.N(b.getElementsByTagName("input")).concat(a.a.N(b.getElementsByTagName("textarea"))),d="string"==typeof e?function(a){return a.name===e}:function(a){return e.test(a.name)},g=[],f=c.length-1;0<=f;f--)d(c[f])&&g.push(c[f]);return g},Nb:function(b){return"string"==typeof b&&(b=a.a.F(b))?
|
||||
y&&y.parse?y.parse(b):(new Function("return "+b))():null},Ca:function(b,e,c){if(!y||!y.stringify)throw Error("Cannot find JSON.stringify(). Some browsers (e.g., IE < 8) don't support it natively, but you can overcome this by adding a script reference to json2.js, downloadable from http://www.json.org/json2.js");return y.stringify(a.a.c(b),e,c)},Ob:function(e,c,d){d=d||{};var g=d.params||{},f=d.includeFields||this.Ta,p=e;if("object"==typeof e&&"form"===a.a.u(e))for(var p=e.action,r=f.length-1;0<=r;r--)for(var z=
|
||||
a.a.Ua(e,f[r]),D=z.length-1;0<=D;D--)g[z[D].name]=z[D].value;c=a.a.c(c);var q=s.createElement("form");q.style.display="none";q.action=p;q.method="post";for(var v in c)e=s.createElement("input"),e.name=v,e.value=a.a.Ca(a.a.c(c[v])),q.appendChild(e);b(g,function(a,b){var e=s.createElement("input");e.name=a;e.value=b;q.appendChild(e)});s.body.appendChild(q);d.submitter?d.submitter(q):q.submit();setTimeout(function(){q.parentNode.removeChild(q)},0)}}}();a.b("utils",a.a);a.b("utils.arrayForEach",a.a.p);
|
||||
a.b("utils.arrayFirst",a.a.La);a.b("utils.arrayFilter",a.a.Y);a.b("utils.arrayGetDistinctValues",a.a.Ma);a.b("utils.arrayIndexOf",a.a.k);a.b("utils.arrayMap",a.a.Z);a.b("utils.arrayPushAll",a.a.R);a.b("utils.arrayRemoveItem",a.a.ka);a.b("utils.extend",a.a.extend);a.b("utils.fieldsIncludedWithJsonPost",a.a.Ta);a.b("utils.getFormFields",a.a.Ua);a.b("utils.peekObservable",a.a.ya);a.b("utils.postJson",a.a.Ob);a.b("utils.parseJson",a.a.Nb);a.b("utils.registerEventHandler",a.a.o);a.b("utils.stringifyJson",
|
||||
a.a.Ca);a.b("utils.range",a.a.Qb);a.b("utils.toggleDomNodeCssClass",a.a.ga);a.b("utils.triggerEvent",a.a.Ga);a.b("utils.unwrapObservable",a.a.c);a.b("utils.objectForEach",a.a.w);a.b("utils.addOrRemoveItem",a.a.ja);a.b("unwrap",a.a.c);Function.prototype.bind||(Function.prototype.bind=function(a){var c=this,d=Array.prototype.slice.call(arguments);a=d.shift();return function(){return c.apply(a,d.concat(Array.prototype.slice.call(arguments)))}});a.a.f=new function(){var b=0,c="__ko__"+(new Date).getTime(),
|
||||
d={};return{get:function(b,c){var e=a.a.f.pa(b,!1);return e===q?q:e[c]},set:function(b,c,e){if(e!==q||a.a.f.pa(b,!1)!==q)a.a.f.pa(b,!0)[c]=e},pa:function(a,g){var e=a[c];if(!e||"null"===e||!d[e]){if(!g)return q;e=a[c]="ko"+b++;d[e]={}}return d[e]},clear:function(a){var b=a[c];return b?(delete d[b],a[c]=null,!0):!1}}};a.b("utils.domData",a.a.f);a.b("utils.domData.clear",a.a.f.clear);a.a.C=new function(){function b(b,c){var g=a.a.f.get(b,d);g===q&&c&&(g=[],a.a.f.set(b,d,g));return g}function c(e){var d=
|
||||
b(e,!1);if(d)for(var d=d.slice(0),f=0;f<d.length;f++)d[f](e);a.a.f.clear(e);"function"==typeof t&&"function"==typeof t.cleanData&&t.cleanData([e]);if(g[e.nodeType])for(d=e.firstChild;e=d;)d=e.nextSibling,8===e.nodeType&&c(e)}var d="__ko_domNodeDisposal__"+(new Date).getTime(),f={1:!0,8:!0,9:!0},g={1:!0,9:!0};return{ia:function(a,c){if("function"!=typeof c)throw Error("Callback must be a function");b(a,!0).push(c)},cb:function(e,c){var g=b(e,!1);g&&(a.a.ka(g,c),0==g.length&&a.a.f.set(e,d,q))},H:function(b){if(f[b.nodeType]&&
|
||||
(c(b),g[b.nodeType])){var d=[];a.a.R(d,b.getElementsByTagName("*"));for(var h=0,k=d.length;h<k;h++)c(d[h])}return b},removeNode:function(b){a.H(b);b.parentNode&&b.parentNode.removeChild(b)}}};a.H=a.a.C.H;a.removeNode=a.a.C.removeNode;a.b("cleanNode",a.H);a.b("removeNode",a.removeNode);a.b("utils.domNodeDisposal",a.a.C);a.b("utils.domNodeDisposal.addDisposeCallback",a.a.C.ia);a.b("utils.domNodeDisposal.removeDisposeCallback",a.a.C.cb);(function(){a.a.xa=function(b){var c;if("undefined"!=typeof t)if(t.parseHTML)c=
|
||||
t.parseHTML(b)||[];else{if((c=t.clean([b]))&&c[0]){for(b=c[0];b.parentNode&&11!==b.parentNode.nodeType;)b=b.parentNode;b.parentNode&&b.parentNode.removeChild(b)}}else{var d=a.a.F(b).toLowerCase();c=s.createElement("div");d=d.match(/^<(thead|tbody|tfoot)/)&&[1,"<table>","</table>"]||!d.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!d.indexOf("<td")||!d.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||[0,"",""];b="ignored<div>"+d[1]+b+d[2]+"</div>";for("function"==typeof w.innerShiv?
|
||||
c.appendChild(w.innerShiv(b)):c.innerHTML=b;d[0]--;)c=c.lastChild;c=a.a.N(c.lastChild.childNodes)}return c};a.a.fa=function(b,c){a.a.oa(b);c=a.a.c(c);if(null!==c&&c!==q)if("string"!=typeof c&&(c=c.toString()),"undefined"!=typeof t)t(b).html(c);else for(var d=a.a.xa(c),f=0;f<d.length;f++)b.appendChild(d[f])}})();a.b("utils.parseHtmlFragment",a.a.xa);a.b("utils.setHtml",a.a.fa);a.s=function(){function b(c,f){if(c)if(8==c.nodeType){var g=a.s.$a(c.nodeValue);null!=g&&f.push({xb:c,Kb:g})}else if(1==c.nodeType)for(var g=
|
||||
0,e=c.childNodes,m=e.length;g<m;g++)b(e[g],f)}var c={};return{va:function(a){if("function"!=typeof a)throw Error("You can only pass a function to ko.memoization.memoize()");var b=(4294967296*(1+Math.random())|0).toString(16).substring(1)+(4294967296*(1+Math.random())|0).toString(16).substring(1);c[b]=a;return"\x3c!--[ko_memo:"+b+"]--\x3e"},mb:function(a,b){var g=c[a];if(g===q)throw Error("Couldn't find any memo with ID "+a+". Perhaps it's already been unmemoized.");try{return g.apply(null,b||[]),
|
||||
!0}finally{delete c[a]}},nb:function(c,f){var g=[];b(c,g);for(var e=0,m=g.length;e<m;e++){var h=g[e].xb,k=[h];f&&a.a.R(k,f);a.s.mb(g[e].Kb,k);h.nodeValue="";h.parentNode&&h.parentNode.removeChild(h)}},$a:function(a){return(a=a.match(/^\[ko_memo\:(.*?)\]$/))?a[1]:null}}}();a.b("memoization",a.s);a.b("memoization.memoize",a.s.va);a.b("memoization.unmemoize",a.s.mb);a.b("memoization.parseMemoText",a.s.$a);a.b("memoization.unmemoizeDomNodeAndDescendants",a.s.nb);a.Sa={throttle:function(b,c){b.throttleEvaluation=
|
||||
c;var d=null;return a.j({read:b,write:function(a){clearTimeout(d);d=setTimeout(function(){b(a)},c)}})},notify:function(b,c){b.equalityComparer="always"==c?F(!1):a.m.fn.equalityComparer;return b}};a.b("extenders",a.Sa);a.kb=function(b,c,d){this.target=b;this.la=c;this.wb=d;a.r(this,"dispose",this.B)};a.kb.prototype.B=function(){this.Hb=!0;this.wb()};a.V=function(){this.G={};a.a.extend(this,a.V.fn);a.r(this,"subscribe",this.Da);a.r(this,"extend",this.extend);a.r(this,"getSubscriptionsCount",this.Db)};
|
||||
a.V.fn={Da:function(b,c,d){d=d||"change";var f=new a.kb(this,c?b.bind(c):b,function(){a.a.ka(this.G[d],f)}.bind(this));this.G[d]||(this.G[d]=[]);this.G[d].push(f);return f},notifySubscribers:function(b,c){c=c||"change";this.G[c]&&a.q.I(function(){a.a.p(this.G[c].slice(0),function(a){a&&!0!==a.Hb&&a.la(b)})},this)},Db:function(){var b=0;a.a.w(this.G,function(a,d){b+=d.length});return b},extend:function(b){var c=this;b&&a.a.w(b,function(b,f){var g=a.Sa[b];"function"==typeof g&&(c=g(c,f))});return c}};
|
||||
a.Wa=function(a){return null!=a&&"function"==typeof a.Da&&"function"==typeof a.notifySubscribers};a.b("subscribable",a.V);a.b("isSubscribable",a.Wa);a.q=function(){var b=[];return{rb:function(a){b.push({la:a,Ra:[]})},end:function(){b.pop()},bb:function(c){if(!a.Wa(c))throw Error("Only subscribable things can act as dependencies");if(0<b.length){var d=b[b.length-1];!d||0<=a.a.k(d.Ra,c)||(d.Ra.push(c),d.la(c))}},I:function(a,d,f){try{return b.push(null),a.apply(d,f||[])}finally{b.pop()}}}}();var L=
|
||||
{undefined:!0,"boolean":!0,number:!0,string:!0};a.m=function(b){function c(){if(0<arguments.length)return c.equalityComparer&&c.equalityComparer(d,arguments[0])||(c.K(),d=arguments[0],c.J()),this;a.q.bb(c);return d}var d=b;a.V.call(c);c.t=function(){return d};c.J=function(){c.notifySubscribers(d)};c.K=function(){c.notifySubscribers(d,"beforeChange")};a.a.extend(c,a.m.fn);a.r(c,"peek",c.t);a.r(c,"valueHasMutated",c.J);a.r(c,"valueWillMutate",c.K);return c};a.m.fn={equalityComparer:function(a,c){return null===
|
||||
a||typeof a in L?a===c:!1}};var A=a.m.Pb="__ko_proto__";a.m.fn[A]=a.m;a.qa=function(b,c){return null===b||b===q||b[A]===q?!1:b[A]===c?!0:a.qa(b[A],c)};a.T=function(b){return a.qa(b,a.m)};a.Xa=function(b){return"function"==typeof b&&b[A]===a.m||"function"==typeof b&&b[A]===a.j&&b.Eb?!0:!1};a.b("observable",a.m);a.b("isObservable",a.T);a.b("isWriteableObservable",a.Xa);a.U=function(b){b=b||[];if("object"!=typeof b||!("length"in b))throw Error("The argument passed when initializing an observable array must be an array, or null, or undefined.");
|
||||
b=a.m(b);a.a.extend(b,a.U.fn);return b};a.U.fn={remove:function(a){for(var c=this.t(),d=[],f="function"==typeof a?a:function(e){return e===a},g=0;g<c.length;g++){var e=c[g];f(e)&&(0===d.length&&this.K(),d.push(e),c.splice(g,1),g--)}d.length&&this.J();return d},removeAll:function(b){if(b===q){var c=this.t(),d=c.slice(0);this.K();c.splice(0,c.length);this.J();return d}return b?this.remove(function(c){return 0<=a.a.k(b,c)}):[]},destroy:function(a){var c=this.t(),d="function"==typeof a?a:function(c){return c===
|
||||
a};this.K();for(var f=c.length-1;0<=f;f--)d(c[f])&&(c[f]._destroy=!0);this.J()},destroyAll:function(b){return b===q?this.destroy(F(!0)):b?this.destroy(function(c){return 0<=a.a.k(b,c)}):[]},indexOf:function(b){var c=this();return a.a.k(c,b)},replace:function(a,c){var d=this.indexOf(a);0<=d&&(this.K(),this.t()[d]=c,this.J())}};a.a.p("pop push reverse shift sort splice unshift".split(" "),function(b){a.U.fn[b]=function(){var a=this.t();this.K();a=a[b].apply(a,arguments);this.J();return a}});a.a.p(["slice"],
|
||||
function(b){a.U.fn[b]=function(){var a=this();return a[b].apply(a,arguments)}});a.b("observableArray",a.U);a.j=function(b,c,d){function f(){a.a.p(v,function(a){a.B()});v=[]}function g(){var a=m.throttleEvaluation;a&&0<=a?(clearTimeout(t),t=setTimeout(e,a)):e()}function e(){if(!n)if(l&&D())x();else{n=!0;try{var b=a.a.Z(v,function(a){return a.target});a.q.rb(function(e){var c;0<=(c=a.a.k(b,e))?b[c]=q:v.push(e.Da(g))});for(var e=p.call(c),d=b.length-1;0<=d;d--)b[d]&&v.splice(d,1)[0].B();l=!0;m.notifySubscribers(k,
|
||||
"beforeChange");k=e;m.notifySubscribers(k)}finally{a.q.end(),n=!1}v.length||x()}}function m(){if(0<arguments.length){if("function"===typeof r)r.apply(c,arguments);else throw Error("Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.");return this}l||e();a.q.bb(m);return k}function h(){return!l||0<v.length}var k,l=!1,n=!1,p=b;p&&"object"==typeof p?(d=p,p=d.read):(d=d||{},p||(p=d.read));if("function"!=typeof p)throw Error("Pass a function that returns the value of the ko.computed");
|
||||
var r=d.write,z=d.disposeWhenNodeIsRemoved||d.$||null,D=d.disposeWhen||d.Qa||F(!1),x=f,v=[],t=null;c||(c=d.owner);m.t=function(){l||e();return k};m.Cb=function(){return v.length};m.Eb="function"===typeof d.write;m.B=function(){x()};m.ta=h;a.V.call(m);a.a.extend(m,a.j.fn);a.r(m,"peek",m.t);a.r(m,"dispose",m.B);a.r(m,"isActive",m.ta);a.r(m,"getDependenciesCount",m.Cb);!0!==d.deferEvaluation&&e();if(z&&h()){x=function(){a.a.C.cb(z,x);f()};a.a.C.ia(z,x);var s=D,D=function(){return!a.a.aa(z)||s()}}return m};
|
||||
a.Gb=function(b){return a.qa(b,a.j)};C=a.m.Pb;a.j[C]=a.m;a.j.fn={};a.j.fn[C]=a.j;a.b("dependentObservable",a.j);a.b("computed",a.j);a.b("isComputed",a.Gb);(function(){function b(a,g,e){e=e||new d;a=g(a);if("object"!=typeof a||null===a||a===q||a instanceof Date||a instanceof String||a instanceof Number||a instanceof Boolean)return a;var m=a instanceof Array?[]:{};e.save(a,m);c(a,function(c){var d=g(a[c]);switch(typeof d){case "boolean":case "number":case "string":case "function":m[c]=d;break;case "object":case "undefined":var l=
|
||||
e.get(d);m[c]=l!==q?l:b(d,g,e)}});return m}function c(a,b){if(a instanceof Array){for(var e=0;e<a.length;e++)b(e);"function"==typeof a.toJSON&&b("toJSON")}else for(e in a)b(e)}function d(){this.keys=[];this.Ha=[]}a.lb=function(c){if(0==arguments.length)throw Error("When calling ko.toJS, pass the object you want to convert.");return b(c,function(b){for(var e=0;a.T(b)&&10>e;e++)b=b();return b})};a.toJSON=function(b,c,e){b=a.lb(b);return a.a.Ca(b,c,e)};d.prototype={save:function(b,c){var e=a.a.k(this.keys,
|
||||
b);0<=e?this.Ha[e]=c:(this.keys.push(b),this.Ha.push(c))},get:function(b){b=a.a.k(this.keys,b);return 0<=b?this.Ha[b]:q}}})();a.b("toJS",a.lb);a.b("toJSON",a.toJSON);(function(){a.h={n:function(b){switch(a.a.u(b)){case "option":return!0===b.__ko__hasDomDataOptionValue__?a.a.f.get(b,a.d.options.wa):7>=a.a.ca?b.getAttributeNode("value")&&b.getAttributeNode("value").specified?b.value:b.text:b.value;case "select":return 0<=b.selectedIndex?a.h.n(b.options[b.selectedIndex]):q;default:return b.value}},W:function(b,
|
||||
c){switch(a.a.u(b)){case "option":switch(typeof c){case "string":a.a.f.set(b,a.d.options.wa,q);"__ko__hasDomDataOptionValue__"in b&&delete b.__ko__hasDomDataOptionValue__;b.value=c;break;default:a.a.f.set(b,a.d.options.wa,c),b.__ko__hasDomDataOptionValue__=!0,b.value="number"===typeof c?c:""}break;case "select":""===c&&(c=q);if(null===c||c===q)b.selectedIndex=-1;for(var d=b.options.length-1;0<=d;d--)if(a.h.n(b.options[d])==c){b.selectedIndex=d;break}1<b.size||-1!==b.selectedIndex||(b.selectedIndex=
|
||||
0);break;default:if(null===c||c===q)c="";b.value=c}}}})();a.b("selectExtensions",a.h);a.b("selectExtensions.readValue",a.h.n);a.b("selectExtensions.writeValue",a.h.W);a.g=function(){function b(a,b){for(var d=null;a!=d;)d=a,a=a.replace(c,function(a,c){return b[c]});return a}var c=/\@ko_token_(\d+)\@/g,d=["true","false","null","undefined"],f=/^(?:[$_a-z][$\w]*|(.+)(\.\s*[$_a-z][$\w]*|\[.+\]))$/i;return{S:[],da:function(c){var e=a.a.F(c);if(3>e.length)return[];"{"===e.charAt(0)&&(e=e.substring(1,e.length-
|
||||
1));c=[];for(var d=null,f,k=0;k<e.length;k++){var l=e.charAt(k);if(null===d)switch(l){case '"':case "'":case "/":d=k,f=l}else if(l==f&&"\\"!==e.charAt(k-1)){l=e.substring(d,k+1);c.push(l);var n="@ko_token_"+(c.length-1)+"@",e=e.substring(0,d)+n+e.substring(k+1),k=k-(l.length-n.length),d=null}}f=d=null;for(var p=0,r=null,k=0;k<e.length;k++){l=e.charAt(k);if(null===d)switch(l){case "{":d=k;r=l;f="}";break;case "(":d=k;r=l;f=")";break;case "[":d=k,r=l,f="]"}l===r?p++:l===f&&(p--,0===p&&(l=e.substring(d,
|
||||
k+1),c.push(l),n="@ko_token_"+(c.length-1)+"@",e=e.substring(0,d)+n+e.substring(k+1),k-=l.length-n.length,d=null))}f=[];e=e.split(",");d=0;for(k=e.length;d<k;d++)p=e[d],r=p.indexOf(":"),0<r&&r<p.length-1?(l=p.substring(r+1),f.push({key:b(p.substring(0,r),c),value:b(l,c)})):f.push({unknown:b(p,c)});return f},ea:function(b){var e="string"===typeof b?a.g.da(b):b,c=[];b=[];for(var h,k=0;h=e[k];k++)if(0<c.length&&c.push(","),h.key){var l;a:{l=h.key;var n=a.a.F(l);switch(n.length&&n.charAt(0)){case "'":case '"':break a;
|
||||
default:l="'"+n+"'"}}h=h.value;c.push(l);c.push(":");c.push(h);h=a.a.F(h);0<=a.a.k(d,a.a.F(h).toLowerCase())?h=!1:(n=h.match(f),h=null===n?!1:n[1]?"Object("+n[1]+")"+n[2]:h);h&&(0<b.length&&b.push(", "),b.push(l+" : function(__ko_value) { "+h+" = __ko_value; }"))}else h.unknown&&c.push(h.unknown);e=c.join("");0<b.length&&(e=e+", '_ko_property_writers' : { "+b.join("")+" } ");return e},Jb:function(b,c){for(var d=0;d<b.length;d++)if(a.a.F(b[d].key)==c)return!0;return!1},ha:function(b,c,d,f,k){if(b&&
|
||||
a.T(b))!a.Xa(b)||k&&b.t()===f||b(f);else if((b=c()._ko_property_writers)&&b[d])b[d](f)}}}();a.b("expressionRewriting",a.g);a.b("expressionRewriting.bindingRewriteValidators",a.g.S);a.b("expressionRewriting.parseObjectLiteral",a.g.da);a.b("expressionRewriting.preProcessBindings",a.g.ea);a.b("jsonExpressionRewriting",a.g);a.b("jsonExpressionRewriting.insertPropertyAccessorsIntoJson",a.g.ea);(function(){function b(a){return 8==a.nodeType&&(g?a.text:a.nodeValue).match(e)}function c(a){return 8==a.nodeType&&
|
||||
(g?a.text:a.nodeValue).match(m)}function d(a,e){for(var d=a,g=1,f=[];d=d.nextSibling;){if(c(d)&&(g--,0===g))return f;f.push(d);b(d)&&g++}if(!e)throw Error("Cannot find closing comment tag to match: "+a.nodeValue);return null}function f(a,b){var c=d(a,b);return c?0<c.length?c[c.length-1].nextSibling:a.nextSibling:null}var g=s&&"\x3c!--test--\x3e"===s.createComment("test").text,e=g?/^\x3c!--\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*--\x3e$/:/^\s*ko(?:\s+(.+\s*\:[\s\S]*))?\s*$/,m=g?/^\x3c!--\s*\/ko\s*--\x3e$/:
|
||||
/^\s*\/ko\s*$/,h={ul:!0,ol:!0};a.e={L:{},childNodes:function(a){return b(a)?d(a):a.childNodes},ba:function(c){if(b(c)){c=a.e.childNodes(c);for(var e=0,d=c.length;e<d;e++)a.removeNode(c[e])}else a.a.oa(c)},P:function(c,e){if(b(c)){a.e.ba(c);for(var d=c.nextSibling,g=0,f=e.length;g<f;g++)d.parentNode.insertBefore(e[g],d)}else a.a.P(c,e)},ab:function(a,c){b(a)?a.parentNode.insertBefore(c,a.nextSibling):a.firstChild?a.insertBefore(c,a.firstChild):a.appendChild(c)},Va:function(c,e,d){d?b(c)?c.parentNode.insertBefore(e,
|
||||
d.nextSibling):d.nextSibling?c.insertBefore(e,d.nextSibling):c.appendChild(e):a.e.ab(c,e)},firstChild:function(a){return b(a)?!a.nextSibling||c(a.nextSibling)?null:a.nextSibling:a.firstChild},nextSibling:function(a){b(a)&&(a=f(a));return a.nextSibling&&c(a.nextSibling)?null:a.nextSibling},ob:function(a){return(a=b(a))?a[1]:null},Za:function(e){if(h[a.a.u(e)]){var d=e.firstChild;if(d){do if(1===d.nodeType){var g;g=d.firstChild;var m=null;if(g){do if(m)m.push(g);else if(b(g)){var r=f(g,!0);r?g=r:m=
|
||||
[g]}else c(g)&&(m=[g]);while(g=g.nextSibling)}if(g=m)for(m=d.nextSibling,r=0;r<g.length;r++)m?e.insertBefore(g[r],m):e.appendChild(g[r])}while(d=d.nextSibling)}}}}})();a.b("virtualElements",a.e);a.b("virtualElements.allowedBindings",a.e.L);a.b("virtualElements.emptyNode",a.e.ba);a.b("virtualElements.insertAfter",a.e.Va);a.b("virtualElements.prepend",a.e.ab);a.b("virtualElements.setDomNodeChildren",a.e.P);(function(){a.M=function(){this.Na={}};a.a.extend(a.M.prototype,{nodeHasBindings:function(b){switch(b.nodeType){case 1:return null!=
|
||||
b.getAttribute("data-bind");case 8:return null!=a.e.ob(b);default:return!1}},getBindings:function(a,c){var d=this.getBindingsString(a,c);return d?this.parseBindingsString(d,c,a):null},getBindingsString:function(b){switch(b.nodeType){case 1:return b.getAttribute("data-bind");case 8:return a.e.ob(b);default:return null}},parseBindingsString:function(b,c,d){try{var f;if(!(f=this.Na[b])){var g=this.Na,e,m="with($context){with($data||{}){return{"+a.g.ea(b)+"}}}";e=new Function("$context","$element",m);
|
||||
f=g[b]=e}return f(c,d)}catch(h){throw h.message="Unable to parse bindings.\nBindings value: "+b+"\nMessage: "+h.message,h;}}});a.M.instance=new a.M})();a.b("bindingProvider",a.M);(function(){function b(b,e,d){for(var f=a.e.firstChild(e);e=f;)f=a.e.nextSibling(e),c(b,e,d)}function c(c,e,f){var h=!0,k=1===e.nodeType;k&&a.e.Za(e);if(k&&f||a.M.instance.nodeHasBindings(e))h=d(e,null,c,f).Sb;h&&b(c,e,!k)}function d(b,c,d,h){function k(a){return function(){return p[a]}}function l(){return p}var n=0,p,r,
|
||||
z=a.a.f.get(b,f);if(!c){if(z)throw Error("You cannot apply bindings multiple times to the same element.");a.a.f.set(b,f,!0)}a.j(function(){var f=d&&d instanceof a.A?d:new a.A(a.a.c(d)),x=f.$data;!z&&h&&a.jb(b,f);if(p=("function"==typeof c?c(f,b):c)||a.M.instance.getBindings(b,f))0===n&&(n=1,a.a.w(p,function(c){var e=a.d[c];if(e&&8===b.nodeType&&!a.e.L[c])throw Error("The binding '"+c+"' cannot be used with virtual elements");if(e&&"function"==typeof e.init&&(e=(0,e.init)(b,k(c),l,x,f))&&e.controlsDescendantBindings){if(r!==
|
||||
q)throw Error("Multiple bindings ("+r+" and "+c+") are trying to control descendant bindings of the same element. You cannot use these bindings together on the same element.");r=c}}),n=2),2===n&&a.a.w(p,function(c){var e=a.d[c];e&&"function"==typeof e.update&&(0,e.update)(b,k(c),l,x,f)})},null,{$:b});return{Sb:r===q}}a.d={};a.A=function(b,c,d){c?(a.a.extend(this,c),this.$parentContext=c,this.$parent=c.$data,this.$parents=(c.$parents||[]).slice(0),this.$parents.unshift(this.$parent)):(this.$parents=
|
||||
[],this.$root=b,this.ko=a);this.$data=b;d&&(this[d]=b)};a.A.prototype.createChildContext=function(b,c){return new a.A(b,this,c)};a.A.prototype.extend=function(b){var c=a.a.extend(new a.A,this);return a.a.extend(c,b)};var f="__ko_boundElement";a.jb=function(b,c){if(2==arguments.length)a.a.f.set(b,"__ko_bindingContext__",c);else return a.a.f.get(b,"__ko_bindingContext__")};a.Ka=function(b,c,f){1===b.nodeType&&a.e.Za(b);return d(b,c,f,!0)};a.Ja=function(a,c){1!==c.nodeType&&8!==c.nodeType||b(a,c,!0)};
|
||||
a.Ia=function(a,b){if(b&&1!==b.nodeType&&8!==b.nodeType)throw Error("ko.applyBindings: first parameter should be your view model; second parameter should be a DOM node");b=b||w.document.body;c(a,b,!0)};a.na=function(b){switch(b.nodeType){case 1:case 8:var c=a.jb(b);if(c)return c;if(b.parentNode)return a.na(b.parentNode)}return q};a.ub=function(b){return(b=a.na(b))?b.$data:q};a.b("bindingHandlers",a.d);a.b("applyBindings",a.Ia);a.b("applyBindingsToDescendants",a.Ja);a.b("applyBindingsToNode",a.Ka);
|
||||
a.b("contextFor",a.na);a.b("dataFor",a.ub)})();var K={"class":"className","for":"htmlFor"};a.d.attr={update:function(b,c){var d=a.a.c(c())||{};a.a.w(d,function(c,d){d=a.a.c(d);var e=!1===d||null===d||d===q;e&&b.removeAttribute(c);8>=a.a.ca&&c in K?(c=K[c],e?b.removeAttribute(c):b[c]=d):e||b.setAttribute(c,d.toString());"name"===c&&a.a.gb(b,e?"":d.toString())})}};a.d.checked={init:function(b,c,d){a.a.o(b,"click",function(){var f;if("checkbox"==b.type)f=b.checked;else if("radio"==b.type&&b.checked)f=
|
||||
b.value;else return;var g=c(),e=a.a.c(g);"checkbox"==b.type&&e instanceof Array?a.a.ja(g,b.value,b.checked):a.g.ha(g,d,"checked",f,!0)});"radio"!=b.type||b.name||a.d.uniqueName.init(b,F(!0))},update:function(b,c){var d=a.a.c(c());"checkbox"==b.type?b.checked=d instanceof Array?0<=a.a.k(d,b.value):d:"radio"==b.type&&(b.checked=b.value==d)}};a.d.css={update:function(b,c){var d=a.a.c(c());"object"==typeof d?a.a.w(d,function(c,d){d=a.a.c(d);a.a.ga(b,c,d)}):(d=String(d||""),a.a.ga(b,b.__ko__cssValue,!1),
|
||||
b.__ko__cssValue=d,a.a.ga(b,d,!0))}};a.d.enable={update:function(b,c){var d=a.a.c(c());d&&b.disabled?b.removeAttribute("disabled"):d||b.disabled||(b.disabled=!0)}};a.d.disable={update:function(b,c){a.d.enable.update(b,function(){return!a.a.c(c())})}};a.d.event={init:function(b,c,d,f){var g=c()||{};a.a.w(g,function(e){"string"==typeof e&&a.a.o(b,e,function(b){var g,k=c()[e];if(k){var l=d();try{var n=a.a.N(arguments);n.unshift(f);g=k.apply(f,n)}finally{!0!==g&&(b.preventDefault?b.preventDefault():b.returnValue=
|
||||
!1)}!1===l[e+"Bubble"]&&(b.cancelBubble=!0,b.stopPropagation&&b.stopPropagation())}})})}};a.d.foreach={Ya:function(b){return function(){var c=b(),d=a.a.ya(c);if(!d||"number"==typeof d.length)return{foreach:c,templateEngine:a.D.sa};a.a.c(c);return{foreach:d.data,as:d.as,includeDestroyed:d.includeDestroyed,afterAdd:d.afterAdd,beforeRemove:d.beforeRemove,afterRender:d.afterRender,beforeMove:d.beforeMove,afterMove:d.afterMove,templateEngine:a.D.sa}}},init:function(b,c){return a.d.template.init(b,a.d.foreach.Ya(c))},
|
||||
update:function(b,c,d,f,g){return a.d.template.update(b,a.d.foreach.Ya(c),d,f,g)}};a.g.S.foreach=!1;a.e.L.foreach=!0;a.d.hasfocus={init:function(b,c,d){function f(e){b.__ko_hasfocusUpdating=!0;var f=b.ownerDocument;if("activeElement"in f){var g;try{g=f.activeElement}catch(l){g=f.body}e=g===b}f=c();a.g.ha(f,d,"hasfocus",e,!0);b.__ko_hasfocusLastValue=e;b.__ko_hasfocusUpdating=!1}var g=f.bind(null,!0),e=f.bind(null,!1);a.a.o(b,"focus",g);a.a.o(b,"focusin",g);a.a.o(b,"blur",e);a.a.o(b,"focusout",e)},
|
||||
update:function(b,c){var d=!!a.a.c(c());b.__ko_hasfocusUpdating||b.__ko_hasfocusLastValue===d||(d?b.focus():b.blur(),a.q.I(a.a.Ga,null,[b,d?"focusin":"focusout"]))}};a.d.hasFocus=a.d.hasfocus;a.d.html={init:function(){return{controlsDescendantBindings:!0}},update:function(b,c){a.a.fa(b,c())}};var I="__ko_withIfBindingData";G("if");G("ifnot",!1,!0);G("with",!0,!1,function(a,c){return a.createChildContext(c)});a.d.options={init:function(b){if("select"!==a.a.u(b))throw Error("options binding applies only to SELECT elements");
|
||||
for(;0<b.length;)b.remove(0);return{controlsDescendantBindings:!0}},update:function(b,c,d){function f(a,b,c){var d=typeof b;return"function"==d?b(a):"string"==d?a[b]:c}function g(b,c){if(p){var d=0<=a.a.k(p,a.h.n(c[0]));a.a.hb(c[0],d)}}var e=0==b.length,m=!e&&b.multiple?b.scrollTop:null;c=a.a.c(c());var h=d(),k=h.optionsIncludeDestroyed,l={},n,p;b.multiple?p=a.a.Z(b.selectedOptions||a.a.Y(b.childNodes,function(b){return b.tagName&&"option"===a.a.u(b)&&b.selected}),function(b){return a.h.n(b)}):0<=
|
||||
b.selectedIndex&&(p=[a.h.n(b.options[b.selectedIndex])]);if(c){"undefined"==typeof c.length&&(c=[c]);var r=a.a.Y(c,function(b){return k||b===q||null===b||!a.a.c(b._destroy)});"optionsCaption"in h&&(n=a.a.c(h.optionsCaption),null!==n&&n!==q&&r.unshift(l))}else c=[];d=g;h.optionsAfterRender&&(d=function(b,c){g(0,c);a.q.I(h.optionsAfterRender,null,[c[0],b!==l?b:q])});a.a.Aa(b,r,function(b,c,d){d.length&&(p=d[0].selected&&[a.h.n(d[0])]);c=s.createElement("option");b===l?(a.a.fa(c,n),a.h.W(c,q)):(d=f(b,
|
||||
h.optionsValue,b),a.h.W(c,a.a.c(d)),b=f(b,h.optionsText,d),a.a.ib(c,b));return[c]},null,d);p=null;e&&"value"in h&&J(b,a.a.ya(h.value),!0);a.a.zb(b);m&&20<Math.abs(m-b.scrollTop)&&(b.scrollTop=m)}};a.d.options.wa="__ko.optionValueDomData__";a.d.selectedOptions={init:function(b,c,d){a.a.o(b,"change",function(){var f=c(),g=[];a.a.p(b.getElementsByTagName("option"),function(b){b.selected&&g.push(a.h.n(b))});a.g.ha(f,d,"selectedOptions",g)})},update:function(b,c){if("select"!=a.a.u(b))throw Error("values binding applies only to SELECT elements");
|
||||
var d=a.a.c(c());d&&"number"==typeof d.length&&a.a.p(b.getElementsByTagName("option"),function(b){var c=0<=a.a.k(d,a.h.n(b));a.a.hb(b,c)})}};a.d.style={update:function(b,c){var d=a.a.c(c()||{});a.a.w(d,function(c,d){d=a.a.c(d);b.style[c]=d||""})}};a.d.submit={init:function(b,c,d,f){if("function"!=typeof c())throw Error("The value for a submit binding must be a function");a.a.o(b,"submit",function(a){var d,m=c();try{d=m.call(f,b)}finally{!0!==d&&(a.preventDefault?a.preventDefault():a.returnValue=!1)}})}};
|
||||
a.d.text={update:function(b,c){a.a.ib(b,c())}};a.e.L.text=!0;a.d.uniqueName={init:function(b,c){if(c()){var d="ko_unique_"+ ++a.d.uniqueName.tb;a.a.gb(b,d)}}};a.d.uniqueName.tb=0;a.d.value={init:function(b,c,d){function f(){m=!1;var e=c(),f=a.h.n(b);a.g.ha(e,d,"value",f)}var g=["change"],e=d().valueUpdate,m=!1;e&&("string"==typeof e&&(e=[e]),a.a.R(g,e),g=a.a.Ma(g));!a.a.ca||("input"!=b.tagName.toLowerCase()||"text"!=b.type||"off"==b.autocomplete||b.form&&"off"==b.form.autocomplete)||-1!=a.a.k(g,"propertychange")||
|
||||
(a.a.o(b,"propertychange",function(){m=!0}),a.a.o(b,"blur",function(){m&&f()}));a.a.p(g,function(c){var d=f;a.a.Tb(c,"after")&&(d=function(){setTimeout(f,0)},c=c.substring(5));a.a.o(b,c,d)})},update:function(b,c){var d="select"===a.a.u(b),f=a.a.c(c()),g=a.h.n(b);f!==g&&(g=function(){a.h.W(b,f)},g(),d&&setTimeout(g,0));d&&0<b.length&&J(b,f,!1)}};a.d.visible={update:function(b,c){var d=a.a.c(c()),f="none"!=b.style.display;d&&!f?b.style.display="":!d&&f&&(b.style.display="none")}};(function(b){a.d[b]=
|
||||
{init:function(c,d,f,g){return a.d.event.init.call(this,c,function(){var a={};a[b]=d();return a},f,g)}}})("click");a.v=function(){};a.v.prototype.renderTemplateSource=function(){throw Error("Override renderTemplateSource");};a.v.prototype.createJavaScriptEvaluatorBlock=function(){throw Error("Override createJavaScriptEvaluatorBlock");};a.v.prototype.makeTemplateSource=function(b,c){if("string"==typeof b){c=c||s;var d=c.getElementById(b);if(!d)throw Error("Cannot find template with ID "+b);return new a.l.i(d)}if(1==
|
||||
b.nodeType||8==b.nodeType)return new a.l.Q(b);throw Error("Unknown template type: "+b);};a.v.prototype.renderTemplate=function(a,c,d,f){a=this.makeTemplateSource(a,f);return this.renderTemplateSource(a,c,d)};a.v.prototype.isTemplateRewritten=function(a,c){return!1===this.allowTemplateRewriting?!0:this.makeTemplateSource(a,c).data("isRewritten")};a.v.prototype.rewriteTemplate=function(a,c,d){a=this.makeTemplateSource(a,d);c=c(a.text());a.text(c);a.data("isRewritten",!0)};a.b("templateEngine",a.v);
|
||||
a.Ea=function(){function b(b,c,d,m){b=a.g.da(b);for(var h=a.g.S,k=0;k<b.length;k++){var l=b[k].key;if(h.hasOwnProperty(l)){var n=h[l];if("function"===typeof n){if(l=n(b[k].value))throw Error(l);}else if(!n)throw Error("This template engine does not support the '"+l+"' binding within its templates");}}d="ko.__tr_ambtns(function($context,$element){return(function(){return{ "+a.g.ea(b)+" } })()},'"+d.toLowerCase()+"')";return m.createJavaScriptEvaluatorBlock(d)+c}var c=/(<([a-z]+\d*)(?:\s+(?!data-bind\s*=\s*)[a-z0-9\-]+(?:=(?:\"[^\"]*\"|\'[^\']*\'))?)*\s+)data-bind\s*=\s*(["'])([\s\S]*?)\3/gi,
|
||||
d=/\x3c!--\s*ko\b\s*([\s\S]*?)\s*--\x3e/g;return{Ab:function(b,c,d){c.isTemplateRewritten(b,d)||c.rewriteTemplate(b,function(b){return a.Ea.Lb(b,c)},d)},Lb:function(a,g){return a.replace(c,function(a,c,d,f,l){return b(l,c,d,g)}).replace(d,function(a,c){return b(c,"\x3c!-- ko --\x3e","#comment",g)})},qb:function(b,c){return a.s.va(function(d,m){var h=d.nextSibling;h&&h.nodeName.toLowerCase()===c&&a.Ka(h,b,m)})}}}();a.b("__tr_ambtns",a.Ea.qb);(function(){a.l={};a.l.i=function(a){this.i=a};a.l.i.prototype.text=
|
||||
function(){var b=a.a.u(this.i),b="script"===b?"text":"textarea"===b?"value":"innerHTML";if(0==arguments.length)return this.i[b];var c=arguments[0];"innerHTML"===b?a.a.fa(this.i,c):this.i[b]=c};a.l.i.prototype.data=function(b){if(1===arguments.length)return a.a.f.get(this.i,"templateSourceData_"+b);a.a.f.set(this.i,"templateSourceData_"+b,arguments[1])};a.l.Q=function(a){this.i=a};a.l.Q.prototype=new a.l.i;a.l.Q.prototype.text=function(){if(0==arguments.length){var b=a.a.f.get(this.i,"__ko_anon_template__")||
|
||||
{};b.Fa===q&&b.ma&&(b.Fa=b.ma.innerHTML);return b.Fa}a.a.f.set(this.i,"__ko_anon_template__",{Fa:arguments[0]})};a.l.i.prototype.nodes=function(){if(0==arguments.length)return(a.a.f.get(this.i,"__ko_anon_template__")||{}).ma;a.a.f.set(this.i,"__ko_anon_template__",{ma:arguments[0]})};a.b("templateSources",a.l);a.b("templateSources.domElement",a.l.i);a.b("templateSources.anonymousTemplate",a.l.Q)})();(function(){function b(b,c,d){var f;for(c=a.e.nextSibling(c);b&&(f=b)!==c;)b=a.e.nextSibling(f),1!==
|
||||
f.nodeType&&8!==f.nodeType||d(f)}function c(c,d){if(c.length){var f=c[0],g=c[c.length-1];b(f,g,function(b){a.Ia(d,b)});b(f,g,function(b){a.s.nb(b,[d])})}}function d(a){return a.nodeType?a:0<a.length?a[0]:null}function f(b,f,h,k,l){l=l||{};var n=b&&d(b),n=n&&n.ownerDocument,p=l.templateEngine||g;a.Ea.Ab(h,p,n);h=p.renderTemplate(h,k,l,n);if("number"!=typeof h.length||0<h.length&&"number"!=typeof h[0].nodeType)throw Error("Template engine must return an array of DOM nodes");n=!1;switch(f){case "replaceChildren":a.e.P(b,
|
||||
h);n=!0;break;case "replaceNode":a.a.eb(b,h);n=!0;break;case "ignoreTargetNode":break;default:throw Error("Unknown renderMode: "+f);}n&&(c(h,k),l.afterRender&&a.q.I(l.afterRender,null,[h,k.$data]));return h}var g;a.Ba=function(b){if(b!=q&&!(b instanceof a.v))throw Error("templateEngine must inherit from ko.templateEngine");g=b};a.za=function(b,c,h,k,l){h=h||{};if((h.templateEngine||g)==q)throw Error("Set a template engine before calling renderTemplate");l=l||"replaceChildren";if(k){var n=d(k);return a.j(function(){var g=
|
||||
c&&c instanceof a.A?c:new a.A(a.a.c(c)),r="function"==typeof b?b(g.$data,g):b,g=f(k,l,r,g,h);"replaceNode"==l&&(k=g,n=d(k))},null,{Qa:function(){return!n||!a.a.aa(n)},$:n&&"replaceNode"==l?n.parentNode:n})}return a.s.va(function(d){a.za(b,c,h,d,"replaceNode")})};a.Rb=function(b,d,g,k,l){function n(a,b){c(b,r);g.afterRender&&g.afterRender(b,a)}function p(c,d){r=l.createChildContext(a.a.c(c),g.as);r.$index=d;var k="function"==typeof b?b(c,r):b;return f(null,"ignoreTargetNode",k,r,g)}var r;return a.j(function(){var b=
|
||||
a.a.c(d)||[];"undefined"==typeof b.length&&(b=[b]);b=a.a.Y(b,function(b){return g.includeDestroyed||b===q||null===b||!a.a.c(b._destroy)});a.q.I(a.a.Aa,null,[k,b,p,g,n])},null,{$:k})};a.d.template={init:function(b,c){var d=a.a.c(c());"string"==typeof d||(d.name||1!=b.nodeType&&8!=b.nodeType)||(d=1==b.nodeType?b.childNodes:a.e.childNodes(b),d=a.a.Mb(d),(new a.l.Q(b)).nodes(d));return{controlsDescendantBindings:!0}},update:function(b,c,d,f,g){c=a.a.c(c());d={};f=!0;var n,p=null;"string"!=typeof c&&(d=
|
||||
c,c=a.a.c(d.name),"if"in d&&(f=a.a.c(d["if"])),f&&"ifnot"in d&&(f=!a.a.c(d.ifnot)),n=a.a.c(d.data));"foreach"in d?p=a.Rb(c||b,f&&d.foreach||[],d,b,g):f?(g="data"in d?g.createChildContext(n,d.as):g,p=a.za(c||b,g,d,b)):a.e.ba(b);g=p;(n=a.a.f.get(b,"__ko__templateComputedDomDataKey__"))&&"function"==typeof n.B&&n.B();a.a.f.set(b,"__ko__templateComputedDomDataKey__",g&&g.ta()?g:q)}};a.g.S.template=function(b){b=a.g.da(b);return 1==b.length&&b[0].unknown||a.g.Jb(b,"name")?null:"This template engine does not support anonymous templates nested within its templates"};
|
||||
a.e.L.template=!0})();a.b("setTemplateEngine",a.Ba);a.b("renderTemplate",a.za);a.a.Pa=function(){function a(b,d,f,g,e){var m=Math.min,h=Math.max,k=[],l,n=b.length,p,r=d.length,q=r-n||1,t=n+r+1,s,v,w;for(l=0;l<=n;l++)for(v=s,k.push(s=[]),w=m(r,l+q),p=h(0,l-1);p<=w;p++)s[p]=p?l?b[l-1]===d[p-1]?v[p-1]:m(v[p]||t,s[p-1]||t)+1:p+1:l+1;m=[];h=[];q=[];l=n;for(p=r;l||p;)r=k[l][p]-1,p&&r===k[l][p-1]?h.push(m[m.length]={status:f,value:d[--p],index:p}):l&&r===k[l-1][p]?q.push(m[m.length]={status:g,value:b[--l],
|
||||
index:l}):(m.push({status:"retained",value:d[--p]}),--l);if(h.length&&q.length){b=10*n;var E;for(d=f=0;(e||d<b)&&(E=h[f]);f++){for(g=0;k=q[g];g++)if(E.value===k.value){E.moved=k.index;k.moved=E.index;q.splice(g,1);d=g=0;break}d+=g}}return m.reverse()}return function(c,d,f){c=c||[];d=d||[];return c.length<=d.length?a(c,d,"added","deleted",f):a(d,c,"deleted","added",f)}}();a.b("utils.compareArrays",a.a.Pa);(function(){function b(b){for(;b.length&&!a.a.aa(b[0]);)b.splice(0,1);if(1<b.length){for(var c=
|
||||
b[0],g=b[b.length-1],e=[c];c!==g;){c=c.nextSibling;if(!c)return;e.push(c)}Array.prototype.splice.apply(b,[0,b.length].concat(e))}return b}function c(c,f,g,e,m){var h=[];c=a.j(function(){var c=f(g,m,b(h))||[];0<h.length&&(a.a.eb(h,c),e&&a.q.I(e,null,[g,c,m]));h.splice(0,h.length);a.a.R(h,c)},null,{$:c,Qa:function(){return!a.a.pb(h)}});return{O:h,j:c.ta()?c:q}}a.a.Aa=function(d,f,g,e,m){function h(a,c){u=n[c];x!==c&&(E[a]=u);u.ra(x++);b(u.O);t.push(u);w.push(u)}function k(b,c){if(b)for(var d=0,e=c.length;d<
|
||||
e;d++)c[d]&&a.a.p(c[d].O,function(a){b(a,d,c[d].X)})}f=f||[];e=e||{};var l=a.a.f.get(d,"setDomNodeChildrenFromArrayMapping_lastMappingResult")===q,n=a.a.f.get(d,"setDomNodeChildrenFromArrayMapping_lastMappingResult")||[],p=a.a.Z(n,function(a){return a.X}),r=a.a.Pa(p,f,e.dontLimitMoves),t=[],s=0,x=0,v=[],w=[];f=[];for(var E=[],p=[],u,B=0,y,A;y=r[B];B++)switch(A=y.moved,y.status){case "deleted":A===q&&(u=n[s],u.j&&u.j.B(),v.push.apply(v,b(u.O)),e.beforeRemove&&(f[B]=u,w.push(u)));s++;break;case "retained":h(B,
|
||||
s++);break;case "added":A!==q?h(B,A):(u={X:y.value,ra:a.m(x++)},t.push(u),w.push(u),l||(p[B]=u))}k(e.beforeMove,E);a.a.p(v,e.beforeRemove?a.H:a.removeNode);for(var B=0,l=a.e.firstChild(d),C;u=w[B];B++){u.O||a.a.extend(u,c(d,g,u.X,m,u.ra));for(s=0;r=u.O[s];l=r.nextSibling,C=r,s++)r!==l&&a.e.Va(d,r,C);!u.Fb&&m&&(m(u.X,u.O,u.ra),u.Fb=!0)}k(e.beforeRemove,f);k(e.afterMove,E);k(e.afterAdd,p);a.a.f.set(d,"setDomNodeChildrenFromArrayMapping_lastMappingResult",t)}})();a.b("utils.setDomNodeChildrenFromArrayMapping",
|
||||
a.a.Aa);a.D=function(){this.allowTemplateRewriting=!1};a.D.prototype=new a.v;a.D.prototype.renderTemplateSource=function(b){var c=(9>a.a.ca?0:b.nodes)?b.nodes():null;if(c)return a.a.N(c.cloneNode(!0).childNodes);b=b.text();return a.a.xa(b)};a.D.sa=new a.D;a.Ba(a.D.sa);a.b("nativeTemplateEngine",a.D);(function(){a.ua=function(){var a=this.Ib=function(){if("undefined"==typeof t||!t.tmpl)return 0;try{if(0<=t.tmpl.tag.tmpl.open.toString().indexOf("__"))return 2}catch(a){}return 1}();this.renderTemplateSource=
|
||||
function(b,f,g){g=g||{};if(2>a)throw Error("Your version of jQuery.tmpl is too old. Please upgrade to jQuery.tmpl 1.0.0pre or later.");var e=b.data("precompiled");e||(e=b.text()||"",e=t.template(null,"{{ko_with $item.koBindingContext}}"+e+"{{/ko_with}}"),b.data("precompiled",e));b=[f.$data];f=t.extend({koBindingContext:f},g.templateOptions);f=t.tmpl(e,b,f);f.appendTo(s.createElement("div"));t.fragments={};return f};this.createJavaScriptEvaluatorBlock=function(a){return"{{ko_code ((function() { return "+
|
||||
a+" })()) }}"};this.addTemplate=function(a,b){s.write("<script type='text/html' id='"+a+"'>"+b+"\x3c/script>")};0<a&&(t.tmpl.tag.ko_code={open:"__.push($1 || '');"},t.tmpl.tag.ko_with={open:"with($1) {",close:"} "})};a.ua.prototype=new a.v;var b=new a.ua;0<b.Ib&&a.Ba(b);a.b("jqueryTmplTemplateEngine",a.ua)})()})})();
|
||||
})();
|
|
@ -0,0 +1,149 @@
|
|||
function ApplicationModel(stompClient) {
|
||||
var self = this;
|
||||
|
||||
self.stompClient = stompClient;
|
||||
self.friends = ko.observableArray();
|
||||
self.username = ko.observable();
|
||||
self.conversation = ko.observable(new ImConversationModel(stompClient,this.username));
|
||||
self.notifications = ko.observableArray();
|
||||
self.csrfToken = ko.computed(function() {
|
||||
return JSON.parse($.ajax({
|
||||
type: 'GET',
|
||||
url: 'csrf',
|
||||
dataType: 'json',
|
||||
success: function() { },
|
||||
data: {},
|
||||
async: false
|
||||
}).responseText);
|
||||
}, this);
|
||||
|
||||
self.connect = function() {
|
||||
var csrf = self.csrfToken();
|
||||
var headers = {};
|
||||
headers[csrf.headerName] = csrf.token;
|
||||
stompClient.connect(headers, function(frame) {
|
||||
|
||||
console.log('Connected ' + frame);
|
||||
self.username(frame.headers['user-name']);
|
||||
|
||||
stompClient.subscribe("/user/queue/errors", function(message) {
|
||||
self.pushNotification("Error " + message.body);
|
||||
});
|
||||
stompClient.subscribe("/app/users", function(message) {
|
||||
var friends = JSON.parse(message.body);
|
||||
|
||||
for(var i=0;i<friends.length;i++) {
|
||||
self.friendSignin({"username": friends[i]});
|
||||
}
|
||||
});
|
||||
stompClient.subscribe("/topic/friends/signin", function(message) {
|
||||
var friends = JSON.parse(message.body);
|
||||
|
||||
for(var i=0;i<friends.length;i++) {
|
||||
self.friendSignin(new ImFriend({"username": friends[i]}));
|
||||
}
|
||||
});
|
||||
stompClient.subscribe("/topic/friends/signout", function(message) {
|
||||
var friends = JSON.parse(message.body);
|
||||
|
||||
for(var i=0;i<friends.length;i++) {
|
||||
self.friendSignout(new ImFriend({"username": friends[i]}));
|
||||
}
|
||||
});
|
||||
var destination = "/user/queue/messages"; // "/queue/*"
|
||||
stompClient.subscribe(destination, function(message) {
|
||||
self.conversation().receiveMessage(JSON.parse(message.body));
|
||||
});
|
||||
}, function(error) {
|
||||
self.pushNotification(error)
|
||||
console.log("STOMP protocol error " + error);
|
||||
});
|
||||
}
|
||||
|
||||
self.pushNotification = function(text) {
|
||||
self.notifications.push({notification: text});
|
||||
if (self.notifications().length > 5) {
|
||||
self.notifications.shift();
|
||||
}
|
||||
}
|
||||
|
||||
self.logout = function() {
|
||||
stompClient.disconnect();
|
||||
window.location.href = "../logout.html";
|
||||
}
|
||||
|
||||
self.friendSignin = function(friend) {
|
||||
self.friends.push(friend);
|
||||
}
|
||||
|
||||
self.friendSignout = function(friend) {
|
||||
var r = self.friends.remove(
|
||||
function(item) {
|
||||
item.username == friend.username
|
||||
}
|
||||
);
|
||||
self.friends(r);
|
||||
}
|
||||
}
|
||||
|
||||
function ImFriend(data) {
|
||||
var self = this;
|
||||
|
||||
self.username = data.username;
|
||||
}
|
||||
|
||||
function ImConversationModel(stompClient,from) {
|
||||
var self = this;
|
||||
self.stompClient = stompClient;
|
||||
self.from = from;
|
||||
self.to = ko.observable(new ImFriend('null'));
|
||||
self.draft = ko.observable('')
|
||||
|
||||
self.messages = ko.observableArray();
|
||||
self.messages.subscribe(function(v) {
|
||||
var elem = $('#chat');
|
||||
elem.scrollTop(elem[0].scrollHeight);
|
||||
});
|
||||
|
||||
self.receiveMessage = function(message) {
|
||||
var isFromSelf = self.from() == message.from;
|
||||
var isFromTo = self.to().username == message.from;
|
||||
if(!(isFromTo || isFromSelf)) {
|
||||
self.chat(new ImFriend({"username":message.from}))
|
||||
}
|
||||
|
||||
self.messages.push(new ImModel(message));
|
||||
};
|
||||
|
||||
self.chat = function(to) {
|
||||
self.to(to);
|
||||
self.draft('');
|
||||
self.messages.removeAll()
|
||||
$('#trade-dialog').modal();
|
||||
}
|
||||
|
||||
self.send = function() {
|
||||
var data = {
|
||||
"created" : new Date(),
|
||||
"from" : self.from(),
|
||||
"to" : self.to().username,
|
||||
"message" : self.draft()
|
||||
};
|
||||
var destination = "/app/im"; // /queue/messages-user1
|
||||
stompClient.send(destination, {}, JSON.stringify(data));
|
||||
self.draft('');
|
||||
}
|
||||
};
|
||||
|
||||
function ImModel(data) {
|
||||
var self = this;
|
||||
|
||||
self.created = new Date(data.created);
|
||||
self.to = data.to;
|
||||
self.message = data.message;
|
||||
self.from = data.from;
|
||||
self.messageFormatted = ko.computed(function() {
|
||||
return self.created.getHours() + ":" + self.created.getMinutes() + ":" + self.created.getSeconds() + " - " + self.from + " - " + self.message;
|
||||
})
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "sockjs",
|
||||
"version": "0.3.4",
|
||||
"main": "sockjs.js",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"components"
|
||||
],
|
||||
"homepage": "https://github.com/myguidingstar/bower-sockjs",
|
||||
"_release": "0.3.4",
|
||||
"_resolution": {
|
||||
"type": "version",
|
||||
"tag": "0.3.4",
|
||||
"commit": "ae96e770ab85caf9073a8806a9dcd7c0ce316623"
|
||||
},
|
||||
"_source": "git://github.com/myguidingstar/bower-sockjs.git",
|
||||
"_target": "~0.3",
|
||||
"_originalSource": "sockjs"
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"name": "sockjs",
|
||||
"version": "0.3.4",
|
||||
"main": "sockjs.js",
|
||||
"ignore": [
|
||||
"**/.*",
|
||||
"node_modules",
|
||||
"components"
|
||||
]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,467 @@
|
|||
// Generated by CoffeeScript 1.6.3
|
||||
/*
|
||||
Stomp Over WebSocket http://www.jmesnil.net/stomp-websocket/doc/ | Apache License V2.0
|
||||
|
||||
Copyright (C) 2010-2013 [Jeff Mesnil](http://jmesnil.net/)
|
||||
Copyright (C) 2012 [FuseSource, Inc.](http://fusesource.com)
|
||||
*/
|
||||
|
||||
|
||||
(function() {
|
||||
var Byte, Client, Frame, Stomp,
|
||||
__hasProp = {}.hasOwnProperty,
|
||||
__slice = [].slice;
|
||||
|
||||
Byte = {
|
||||
LF: '\x0A',
|
||||
NULL: '\x00'
|
||||
};
|
||||
|
||||
Frame = (function() {
|
||||
var unmarshallSingle;
|
||||
|
||||
function Frame(command, headers, body) {
|
||||
this.command = command;
|
||||
this.headers = headers != null ? headers : {};
|
||||
this.body = body != null ? body : '';
|
||||
}
|
||||
|
||||
Frame.prototype.toString = function() {
|
||||
var lines, name, value, _ref;
|
||||
lines = [this.command];
|
||||
_ref = this.headers;
|
||||
for (name in _ref) {
|
||||
if (!__hasProp.call(_ref, name)) continue;
|
||||
value = _ref[name];
|
||||
lines.push("" + name + ":" + value);
|
||||
}
|
||||
if (this.body) {
|
||||
lines.push("content-length:" + (Frame.sizeOfUTF8(this.body)));
|
||||
}
|
||||
lines.push(Byte.LF + this.body);
|
||||
return lines.join(Byte.LF);
|
||||
};
|
||||
|
||||
Frame.sizeOfUTF8 = function(s) {
|
||||
if (s) {
|
||||
return encodeURI(s).split(/%..|./).length - 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
unmarshallSingle = function(data) {
|
||||
var body, chr, command, divider, headerLines, headers, i, idx, len, line, start, trim, _i, _j, _len, _ref, _ref1;
|
||||
divider = data.search(RegExp("" + Byte.LF + Byte.LF));
|
||||
headerLines = data.substring(0, divider).split(Byte.LF);
|
||||
command = headerLines.shift();
|
||||
headers = {};
|
||||
trim = function(str) {
|
||||
return str.replace(/^\s+|\s+$/g, '');
|
||||
};
|
||||
_ref = headerLines.reverse();
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
line = _ref[_i];
|
||||
idx = line.indexOf(':');
|
||||
headers[trim(line.substring(0, idx))] = trim(line.substring(idx + 1));
|
||||
}
|
||||
body = '';
|
||||
start = divider + 2;
|
||||
if (headers['content-length']) {
|
||||
len = parseInt(headers['content-length']);
|
||||
body = ('' + data).substring(start, start + len);
|
||||
} else {
|
||||
chr = null;
|
||||
for (i = _j = start, _ref1 = data.length; start <= _ref1 ? _j < _ref1 : _j > _ref1; i = start <= _ref1 ? ++_j : --_j) {
|
||||
chr = data.charAt(i);
|
||||
if (chr === Byte.NULL) {
|
||||
break;
|
||||
}
|
||||
body += chr;
|
||||
}
|
||||
}
|
||||
return new Frame(command, headers, body);
|
||||
};
|
||||
|
||||
Frame.unmarshall = function(datas) {
|
||||
var data;
|
||||
return (function() {
|
||||
var _i, _len, _ref, _results;
|
||||
_ref = datas.split(RegExp("" + Byte.NULL + Byte.LF + "*"));
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
data = _ref[_i];
|
||||
if ((data != null ? data.length : void 0) > 0) {
|
||||
_results.push(unmarshallSingle(data));
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
})();
|
||||
};
|
||||
|
||||
Frame.marshall = function(command, headers, body) {
|
||||
var frame;
|
||||
frame = new Frame(command, headers, body);
|
||||
return frame.toString() + Byte.NULL;
|
||||
};
|
||||
|
||||
return Frame;
|
||||
|
||||
})();
|
||||
|
||||
Client = (function() {
|
||||
var now;
|
||||
|
||||
function Client(ws) {
|
||||
this.ws = ws;
|
||||
this.ws.binaryType = "arraybuffer";
|
||||
this.counter = 0;
|
||||
this.connected = false;
|
||||
this.heartbeat = {
|
||||
outgoing: 10000,
|
||||
incoming: 10000
|
||||
};
|
||||
this.maxWebSocketFrameSize = 16 * 1024;
|
||||
this.subscriptions = {};
|
||||
}
|
||||
|
||||
Client.prototype.debug = function(message) {
|
||||
var _ref;
|
||||
return typeof window !== "undefined" && window !== null ? (_ref = window.console) != null ? _ref.log(message) : void 0 : void 0;
|
||||
};
|
||||
|
||||
now = function() {
|
||||
return Date.now || new Date().valueOf;
|
||||
};
|
||||
|
||||
Client.prototype._transmit = function(command, headers, body) {
|
||||
var out;
|
||||
out = Frame.marshall(command, headers, body);
|
||||
if (typeof this.debug === "function") {
|
||||
this.debug(">>> " + out);
|
||||
}
|
||||
while (true) {
|
||||
if (out.length > this.maxWebSocketFrameSize) {
|
||||
this.ws.send(out.substring(0, this.maxWebSocketFrameSize));
|
||||
out = out.substring(this.maxWebSocketFrameSize);
|
||||
if (typeof this.debug === "function") {
|
||||
this.debug("remaining = " + out.length);
|
||||
}
|
||||
} else {
|
||||
return this.ws.send(out);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype._setupHeartbeat = function(headers) {
|
||||
var serverIncoming, serverOutgoing, ttl, v, _ref, _ref1,
|
||||
_this = this;
|
||||
if ((_ref = headers.version) !== Stomp.VERSIONS.V1_1 && _ref !== Stomp.VERSIONS.V1_2) {
|
||||
return;
|
||||
}
|
||||
_ref1 = (function() {
|
||||
var _i, _len, _ref1, _results;
|
||||
_ref1 = headers['heart-beat'].split(",");
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
|
||||
v = _ref1[_i];
|
||||
_results.push(parseInt(v));
|
||||
}
|
||||
return _results;
|
||||
})(), serverOutgoing = _ref1[0], serverIncoming = _ref1[1];
|
||||
if (!(this.heartbeat.outgoing === 0 || serverIncoming === 0)) {
|
||||
ttl = Math.max(this.heartbeat.outgoing, serverIncoming);
|
||||
if (typeof this.debug === "function") {
|
||||
this.debug("send PING every " + ttl + "ms");
|
||||
}
|
||||
this.pinger = Stomp.setInterval(ttl, function() {
|
||||
_this.ws.send(Byte.LF);
|
||||
return typeof _this.debug === "function" ? _this.debug(">>> PING") : void 0;
|
||||
});
|
||||
}
|
||||
if (!(this.heartbeat.incoming === 0 || serverOutgoing === 0)) {
|
||||
ttl = Math.max(this.heartbeat.incoming, serverOutgoing);
|
||||
if (typeof this.debug === "function") {
|
||||
this.debug("check PONG every " + ttl + "ms");
|
||||
}
|
||||
return this.ponger = Stomp.setInterval(ttl, function() {
|
||||
var delta;
|
||||
delta = now() - _this.serverActivity;
|
||||
if (delta > ttl * 2) {
|
||||
if (typeof _this.debug === "function") {
|
||||
_this.debug("did not receive server activity for the last " + delta + "ms");
|
||||
}
|
||||
return _this.ws.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype._parseConnect = function() {
|
||||
var args, connectCallback, errorCallback, headers;
|
||||
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
||||
headers = {};
|
||||
switch (args.length) {
|
||||
case 2:
|
||||
headers = args[0], connectCallback = args[1];
|
||||
break;
|
||||
case 3:
|
||||
if (args[1] instanceof Function) {
|
||||
headers = args[0], connectCallback = args[1], errorCallback = args[2];
|
||||
} else {
|
||||
headers.login = args[0], headers.passcode = args[1], connectCallback = args[2];
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3];
|
||||
break;
|
||||
default:
|
||||
headers.login = args[0], headers.passcode = args[1], connectCallback = args[2], errorCallback = args[3], headers.host = args[4];
|
||||
}
|
||||
return [headers, connectCallback, errorCallback];
|
||||
};
|
||||
|
||||
Client.prototype.connect = function() {
|
||||
var args, errorCallback, headers, out,
|
||||
_this = this;
|
||||
args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
|
||||
out = this._parseConnect.apply(this, args);
|
||||
headers = out[0], this.connectCallback = out[1], errorCallback = out[2];
|
||||
if (typeof this.debug === "function") {
|
||||
this.debug("Opening Web Socket...");
|
||||
}
|
||||
this.ws.onmessage = function(evt) {
|
||||
var arr, c, client, data, frame, messageID, onreceive, subscription, _i, _len, _ref, _results;
|
||||
data = typeof ArrayBuffer !== 'undefined' && evt.data instanceof ArrayBuffer ? (arr = new Uint8Array(evt.data), typeof _this.debug === "function" ? _this.debug("--- got data length: " + arr.length) : void 0, ((function() {
|
||||
var _i, _len, _results;
|
||||
_results = [];
|
||||
for (_i = 0, _len = arr.length; _i < _len; _i++) {
|
||||
c = arr[_i];
|
||||
_results.push(String.fromCharCode(c));
|
||||
}
|
||||
return _results;
|
||||
})()).join('')) : evt.data;
|
||||
_this.serverActivity = now();
|
||||
if (data === Byte.LF) {
|
||||
if (typeof _this.debug === "function") {
|
||||
_this.debug("<<< PONG");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (typeof _this.debug === "function") {
|
||||
_this.debug("<<< " + data);
|
||||
}
|
||||
_ref = Frame.unmarshall(data);
|
||||
_results = [];
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
frame = _ref[_i];
|
||||
switch (frame.command) {
|
||||
case "CONNECTED":
|
||||
if (typeof _this.debug === "function") {
|
||||
_this.debug("connected to server " + frame.headers.server);
|
||||
}
|
||||
_this.connected = true;
|
||||
_this._setupHeartbeat(frame.headers);
|
||||
_results.push(typeof _this.connectCallback === "function" ? _this.connectCallback(frame) : void 0);
|
||||
break;
|
||||
case "MESSAGE":
|
||||
subscription = frame.headers.subscription;
|
||||
onreceive = _this.subscriptions[subscription] || _this.onreceive;
|
||||
if (onreceive) {
|
||||
client = _this;
|
||||
messageID = frame.headers["message-id"];
|
||||
frame.ack = function(headers) {
|
||||
if (headers == null) {
|
||||
headers = {};
|
||||
}
|
||||
return client.ack(messageID, subscription, headers);
|
||||
};
|
||||
frame.nack = function(headers) {
|
||||
if (headers == null) {
|
||||
headers = {};
|
||||
}
|
||||
return client.nack(messageID, subscription, headers);
|
||||
};
|
||||
_results.push(onreceive(frame));
|
||||
} else {
|
||||
_results.push(typeof _this.debug === "function" ? _this.debug("Unhandled received MESSAGE: " + frame) : void 0);
|
||||
}
|
||||
break;
|
||||
case "RECEIPT":
|
||||
_results.push(typeof _this.onreceipt === "function" ? _this.onreceipt(frame) : void 0);
|
||||
break;
|
||||
case "ERROR":
|
||||
_results.push(typeof errorCallback === "function" ? errorCallback(frame) : void 0);
|
||||
break;
|
||||
default:
|
||||
_results.push(typeof _this.debug === "function" ? _this.debug("Unhandled frame: " + frame) : void 0);
|
||||
}
|
||||
}
|
||||
return _results;
|
||||
};
|
||||
this.ws.onclose = function() {
|
||||
var msg;
|
||||
msg = "Whoops! Lost connection to " + _this.ws.url;
|
||||
if (typeof _this.debug === "function") {
|
||||
_this.debug(msg);
|
||||
}
|
||||
_this._cleanUp();
|
||||
return typeof errorCallback === "function" ? errorCallback(msg) : void 0;
|
||||
};
|
||||
return this.ws.onopen = function() {
|
||||
if (typeof _this.debug === "function") {
|
||||
_this.debug('Web Socket Opened...');
|
||||
}
|
||||
headers["accept-version"] = Stomp.VERSIONS.supportedVersions();
|
||||
headers["heart-beat"] = [_this.heartbeat.outgoing, _this.heartbeat.incoming].join(',');
|
||||
return _this._transmit("CONNECT", headers);
|
||||
};
|
||||
};
|
||||
|
||||
Client.prototype.disconnect = function(disconnectCallback) {
|
||||
this._transmit("DISCONNECT");
|
||||
this.ws.onclose = null;
|
||||
this.ws.close();
|
||||
this._cleanUp();
|
||||
return typeof disconnectCallback === "function" ? disconnectCallback() : void 0;
|
||||
};
|
||||
|
||||
Client.prototype._cleanUp = function() {
|
||||
this.connected = false;
|
||||
if (this.pinger) {
|
||||
Stomp.clearInterval(this.pinger);
|
||||
}
|
||||
if (this.ponger) {
|
||||
return Stomp.clearInterval(this.ponger);
|
||||
}
|
||||
};
|
||||
|
||||
Client.prototype.send = function(destination, headers, body) {
|
||||
if (headers == null) {
|
||||
headers = {};
|
||||
}
|
||||
if (body == null) {
|
||||
body = '';
|
||||
}
|
||||
headers.destination = destination;
|
||||
return this._transmit("SEND", headers, body);
|
||||
};
|
||||
|
||||
Client.prototype.subscribe = function(destination, callback, headers) {
|
||||
var client;
|
||||
if (headers == null) {
|
||||
headers = {};
|
||||
}
|
||||
if (!headers.id) {
|
||||
headers.id = "sub-" + this.counter++;
|
||||
}
|
||||
headers.destination = destination;
|
||||
this.subscriptions[headers.id] = callback;
|
||||
this._transmit("SUBSCRIBE", headers);
|
||||
client = this;
|
||||
return {
|
||||
id: headers.id,
|
||||
unsubscribe: function() {
|
||||
return client.unsubscribe(headers.id);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Client.prototype.unsubscribe = function(id) {
|
||||
delete this.subscriptions[id];
|
||||
return this._transmit("UNSUBSCRIBE", {
|
||||
id: id
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.begin = function(transaction) {
|
||||
var client, txid;
|
||||
txid = transaction || "tx-" + this.counter++;
|
||||
this._transmit("BEGIN", {
|
||||
transaction: txid
|
||||
});
|
||||
client = this;
|
||||
return {
|
||||
id: txid,
|
||||
commit: function() {
|
||||
return client.commit(txid);
|
||||
},
|
||||
abort: function() {
|
||||
return client.abort(txid);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Client.prototype.commit = function(transaction) {
|
||||
return this._transmit("COMMIT", {
|
||||
transaction: transaction
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.abort = function(transaction) {
|
||||
return this._transmit("ABORT", {
|
||||
transaction: transaction
|
||||
});
|
||||
};
|
||||
|
||||
Client.prototype.ack = function(messageID, subscription, headers) {
|
||||
if (headers == null) {
|
||||
headers = {};
|
||||
}
|
||||
headers["message-id"] = messageID;
|
||||
headers.subscription = subscription;
|
||||
return this._transmit("ACK", headers);
|
||||
};
|
||||
|
||||
Client.prototype.nack = function(messageID, subscription, headers) {
|
||||
if (headers == null) {
|
||||
headers = {};
|
||||
}
|
||||
headers["message-id"] = messageID;
|
||||
headers.subscription = subscription;
|
||||
return this._transmit("NACK", headers);
|
||||
};
|
||||
|
||||
return Client;
|
||||
|
||||
})();
|
||||
|
||||
Stomp = {
|
||||
VERSIONS: {
|
||||
V1_0: '1.0',
|
||||
V1_1: '1.1',
|
||||
V1_2: '1.2',
|
||||
supportedVersions: function() {
|
||||
return '1.1,1.0';
|
||||
}
|
||||
},
|
||||
client: function(url, protocols) {
|
||||
var klass, ws;
|
||||
if (protocols == null) {
|
||||
protocols = ['v10.stomp', 'v11.stomp'];
|
||||
}
|
||||
klass = Stomp.WebSocketClass || WebSocket;
|
||||
ws = new klass(url, protocols);
|
||||
return new Client(ws);
|
||||
},
|
||||
over: function(ws) {
|
||||
return new Client(ws);
|
||||
},
|
||||
Frame: Frame
|
||||
};
|
||||
|
||||
if (typeof window !== "undefined" && window !== null) {
|
||||
Stomp.setInterval = function(interval, f) {
|
||||
return window.setInterval(f, interval);
|
||||
};
|
||||
Stomp.clearInterval = function(id) {
|
||||
return window.clearInterval(id);
|
||||
};
|
||||
window.Stomp = Stomp;
|
||||
} else if (typeof exports !== "undefined" && exports !== null) {
|
||||
exports.Stomp = Stomp;
|
||||
} else {
|
||||
self.Stomp = Stomp;
|
||||
}
|
||||
|
||||
}).call(this);
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,57 @@
|
|||
<?xml version="1.0" encoding="ISO-8859-1" ?>
|
||||
<!DOCTYPE tiles-definitions PUBLIC
|
||||
"-//Apache Software Foundation//DTD Tiles Configuration 2.1//EN"
|
||||
"http://tiles.apache.org/dtds/tiles-config_2_1.dtd">
|
||||
<tiles-definitions>
|
||||
|
||||
<definition name="messages/*"
|
||||
template="layout">
|
||||
<put-attribute name="content"
|
||||
value="content/messages/{1}"/>
|
||||
<put-attribute name="title"
|
||||
value="title/messages/{1}"/>
|
||||
<put-attribute name="head"
|
||||
value="head/messages/{1}"/>
|
||||
</definition>
|
||||
|
||||
<definition name="content/messages/*"
|
||||
template="messages/{1} :: content"/>
|
||||
<definition name="title/messages/*"
|
||||
template="messages/{1} :: title"/>
|
||||
<definition name="head/messages/*"
|
||||
template="messages/{1} :: /html/head/link"/>
|
||||
|
||||
<definition name="user/*"
|
||||
template="layout">
|
||||
<put-attribute name="content"
|
||||
value="content/user/{1}"/>
|
||||
<put-attribute name="title"
|
||||
value="title/user/{1}"/>
|
||||
<put-attribute name="head"
|
||||
value="head/user/{1}"/>
|
||||
</definition>
|
||||
|
||||
<definition name="content/user/*"
|
||||
template="user/{1} :: content"/>
|
||||
<definition name="title/user/*"
|
||||
template="user/{1} :: title"/>
|
||||
<definition name="head/user/*"
|
||||
template="user/{1} :: /html/head/link"/>
|
||||
|
||||
<definition name="*"
|
||||
template="layout">
|
||||
<put-attribute name="content"
|
||||
value="content/{1}"/>
|
||||
<put-attribute name="title"
|
||||
value="title/{1}"/>
|
||||
<put-attribute name="head"
|
||||
value="head/{1}"/>
|
||||
</definition>
|
||||
|
||||
<definition name="content/*"
|
||||
template="{1} :: content"/>
|
||||
<definition name="title/*"
|
||||
template="{1} :: title"/>
|
||||
<definition name="head/*"
|
||||
template="{1} :: /html/head/link"/>
|
||||
</tiles-definitions>
|
|
@ -0,0 +1,89 @@
|
|||
<html xmlns:th="http://www.thymeleaf.org" xmlns:tiles="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title tiles:fragment="title">Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<div tiles:fragment="content">
|
||||
<div class="container">
|
||||
<div id="heading" class="masthead">
|
||||
<h3 class="muted">Chat Application</h3>
|
||||
</div>
|
||||
<div>
|
||||
<!--<form>-->
|
||||
<!--<select id="protocol">-->
|
||||
<!--<option value="auto">Automatic</option>-->
|
||||
<!--<option value="websocket">websocket</option>-->
|
||||
<!--<option value="xhr-streaming">xhr-streaming</option>-->
|
||||
<!--<option value="xhr-polling">xhr-polling</option>-->
|
||||
<!--<option value="iframe-eventsource">iframe-eventsource</option>-->
|
||||
<!--<option value="iframe-htmlfile">iframe-htmlfile</option>-->
|
||||
<!--<option value="iframe-xhr-polling">iframe-xhr-polling</option>-->
|
||||
<!--<option value="jsonp-polling">jsonp-polling</option>-->
|
||||
<!--<option value="xdr-streaming">xdr-streaming</option>-->
|
||||
<!--<option value="xdr-polling">xdr-polling</option>-->
|
||||
<!--</select>-->
|
||||
<!--</form>-->
|
||||
</div>
|
||||
<div id="main-content">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: friends()">
|
||||
<tr>
|
||||
<td data-bind="text: username"></td>
|
||||
<td class="trade-buttons">
|
||||
<button class="btn btn-primary" data-bind="click: $root.conversation().chat">Chat</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div id="trade-dialog" class="modal hide fade" tabindex="-1">
|
||||
<div class="modal-body">
|
||||
<h3 id="chat-title">Chat with <span data-bind="text: conversation().to().username"></span></h3>
|
||||
<div id="chat" data-bind="foreach: conversation().messages()">
|
||||
<div data-bind="text: messageFormatted"></div>
|
||||
</div>
|
||||
<form class="form-horizontal" data-bind="submit: conversation().send">
|
||||
<input type="text" data-bind="value: conversation().draft"/>
|
||||
<button class="btn btn-primary" type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<h5>Notifications</h5>
|
||||
<ul data-bind="foreach: notifications">
|
||||
<li data-bind="text: notification"></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 3rd party -->
|
||||
<script src="resources/js/jquery/jquery.js"></script>
|
||||
<script src="resources/js/bootstrap/js/bootstrap.js"></script>
|
||||
<script src="resources/js/knockout/knockout.js"></script>
|
||||
<script src="resources/js/sockjs/sockjs.js"></script>
|
||||
<script src="resources/js/stomp/lib/stomp.js"></script>
|
||||
|
||||
<!-- application -->
|
||||
<script src="resources/js/message.js"></script>
|
||||
<script type="text/javascript">
|
||||
(function() {
|
||||
var socket = new SockJS('/sample/chat');
|
||||
var stompClient = Stomp.over(socket);
|
||||
|
||||
var appModel = new ApplicationModel(stompClient);
|
||||
ko.applyBindings(appModel);
|
||||
|
||||
appModel.connect();
|
||||
})();
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,128 @@
|
|||
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-3.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:tiles="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title tiles:include="title">SecureMail:</title>
|
||||
<link rel="icon" type="image/x-icon" th:href="@{/resources/img/favicon.ico}" href="../resources/img/favicon.ico"/>
|
||||
<link th:href="@{/resources/css/bootstrap.css}" href="../resources/css/bootstrap.css" rel="stylesheet"></link>
|
||||
<style type="text/css">
|
||||
/* Sticky footer styles
|
||||
-------------------------------------------------- */
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
/* The html and body elements cannot have any padding or margin. */
|
||||
}
|
||||
|
||||
/* Wrapper for page content to push down footer */
|
||||
#wrap {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
/* Negative indent footer by it's height */
|
||||
margin: 0 auto -60px;
|
||||
}
|
||||
|
||||
/* Set the fixed height of the footer here */
|
||||
#push,
|
||||
#footer {
|
||||
height: 60px;
|
||||
}
|
||||
#footer {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* Lastly, apply responsive CSS fixes as necessary */
|
||||
@media (max-width: 767px) {
|
||||
#footer {
|
||||
margin-left: -20px;
|
||||
margin-right: -20px;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Custom page CSS
|
||||
-------------------------------------------------- */
|
||||
/* Not required for template or sticky footer method. */
|
||||
|
||||
.container {
|
||||
width: auto;
|
||||
max-width: 680px;
|
||||
}
|
||||
.container .credit {
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
a {
|
||||
color: green;
|
||||
}
|
||||
.navbar-form {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
#chat {
|
||||
height: 15em;
|
||||
max-height: 200px;
|
||||
overflow:scroll
|
||||
}
|
||||
</style>
|
||||
<link th:href="@{resources/css/bootstrap-responsive.css}" href="/resources/css/bootstrap-responsive.css" rel="stylesheet"></link>
|
||||
|
||||
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
<script tiles:replace="head"></script>
|
||||
</head>
|
||||
|
||||
|
||||
<body>
|
||||
<div id="wrap">
|
||||
<div class="navbar navbar-inverse navbar-static-top">
|
||||
<div class="navbar-inner">
|
||||
<div class="container">
|
||||
<a class="brand" th:href="@{/}"><img th:src="@{/resources/img/logo.png}" alt="Spring Security Sample"/></a>
|
||||
<div class="nav-collapse collapse">
|
||||
<div th:if="${#httpServletRequest.remoteUser != null}">
|
||||
<form class="navbar-form pull-right" th:action="@{/logout}" method="post">
|
||||
<input type="submit" value="Log out" />
|
||||
</form>
|
||||
<p class="navbar-text pull-right" th:text="${#httpServletRequest.remoteUser}">
|
||||
sample_user
|
||||
</p>
|
||||
</div>
|
||||
<ul class="nav">
|
||||
<li><a th:href="@{/}">Chat</a></li>
|
||||
<li><a th:href="@{/hack}">For Hackers</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Begin page content -->
|
||||
<div class="container">
|
||||
<div class="alert alert-success"
|
||||
th:if="${globalMessage}"
|
||||
th:text="${globalMessage}">
|
||||
Some Success message
|
||||
</div>
|
||||
<div tiles:substituteby="content">
|
||||
Fake content
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="push"><!-- --></div>
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
<div class="container">
|
||||
<p class="muted credit">Visit the <a href="http://spring.io/spring-security">Spring Security</a> site for more <a href="https://github.com/spring-projects/spring-security/blob/master/samples/">samples</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,27 @@
|
|||
<html xmlns:th="http://www.thymeleaf.org" xmlns:tiles="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title tiles:fragment="title">Log In</title>
|
||||
</head>
|
||||
<body>
|
||||
<div tiles:fragment="content">
|
||||
<form name="f" th:action="@{/login}" method="post">
|
||||
<fieldset>
|
||||
<legend>Please Login</legend>
|
||||
<div th:if="${param.error}" class="alert alert-error">
|
||||
Invalid username and password.
|
||||
</div>
|
||||
<div th:if="${param.logout}" class="alert alert-success">
|
||||
You have been logged out.
|
||||
</div>
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username"/>
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password"/>
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn">Log in</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -18,6 +18,7 @@ def String[] modules = [
|
|||
def String[] samples = [
|
||||
'tutorial-xml',
|
||||
'contacts-xml',
|
||||
'chat-jc',
|
||||
'openid-xml',
|
||||
'aspectj-xml',
|
||||
'aspectj-jc',
|
||||
|
|
Loading…
Reference in New Issue