11 |
16 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/support/WebSocketTestServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2014 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.support;
18 |
19 | import org.springframework.web.context.WebApplicationContext;
20 |
21 |
22 | public interface WebSocketTestServer {
23 |
24 | int getPort();
25 |
26 | void deployDispatcherServlet(WebApplicationContext cxt);
27 |
28 | void undeployConfig();
29 |
30 | void start() throws Exception;
31 |
32 | void stop() throws Exception;
33 |
34 | }
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/support/TestPrincipal.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.support;
18 |
19 | import java.security.Principal;
20 |
21 | /**
22 | * A simple simple implementation of {@link java.security.Principal}.
23 | */
24 | public class TestPrincipal implements Principal {
25 |
26 | private final String name;
27 |
28 |
29 | public TestPrincipal(String name) {
30 | this.name = name;
31 | }
32 |
33 | @Override
34 | public String getName() {
35 | return this.name;
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/config/WebConfig.java:
--------------------------------------------------------------------------------
1 | package org.springframework.samples.portfolio.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 | import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
5 | import org.springframework.web.servlet.config.annotation.EnableWebMvc;
6 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
7 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
8 | import org.springframework.web.servlet.resource.PathResourceResolver;
9 | import org.springframework.web.servlet.resource.WebJarsResourceResolver;
10 |
11 |
12 | @Configuration
13 | @EnableWebMvc
14 | public class WebConfig implements WebMvcConfigurer {
15 |
16 | @Override
17 | public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
18 | configurer.enable();
19 | }
20 |
21 | @Override
22 | public void addResourceHandlers(ResourceHandlerRegistry registry) {
23 | registry.addResourceHandler("/webjars/**").addResourceLocations("/webjars/")
24 | .resourceChain(false)
25 | .addResolver(new WebJarsResourceResolver())
26 | .addResolver(new PathResourceResolver());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/config/WebSecurityInitializer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.config;
17 |
18 | import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
19 |
20 |
21 | /**
22 | * ServletContext initializer for Spring Security specific configuration such as
23 | * the chain of Spring Security filters.
24 | *
25 | * The Spring Security configuration is customized with
26 | * {@link org.springframework.samples.portfolio.config.WebSecurityConfig}.
27 | *
28 | * @author Rob Winch
29 | */
30 | public class WebSecurityInitializer extends AbstractSecurityWebApplicationInitializer {
31 | }
32 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/standalone/TestTradeService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.standalone;
18 |
19 | import org.springframework.samples.portfolio.service.Trade;
20 | import org.springframework.samples.portfolio.service.TradeService;
21 |
22 | import java.util.ArrayList;
23 | import java.util.List;
24 |
25 |
26 | public class TestTradeService implements TradeService {
27 |
28 | private final List trades = new ArrayList<>();
29 |
30 | public List getTrades() {
31 | return this.trades;
32 | }
33 |
34 | @Override
35 | public void executeTrade(Trade trade) {
36 | this.trades.add(trade);
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/config/WebSocketConfig.java:
--------------------------------------------------------------------------------
1 | package org.springframework.samples.portfolio.config;
2 |
3 | import org.springframework.context.annotation.ComponentScan;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.messaging.simp.config.MessageBrokerRegistry;
6 | import org.springframework.scheduling.annotation.EnableScheduling;
7 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
8 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
9 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
10 |
11 | @Configuration
12 | @EnableScheduling
13 | @ComponentScan("org.springframework.samples")
14 | @EnableWebSocketMessageBroker
15 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
16 |
17 | @Override
18 | public void registerStompEndpoints(StompEndpointRegistry registry) {
19 | registry.addEndpoint("/portfolio").withSockJS();
20 | }
21 |
22 | @Override
23 | public void configureMessageBroker(MessageBrokerRegistry registry) {
24 | registry.enableSimpleBroker("/queue/", "/topic/");
25 | // registry.enableStompBrokerRelay("/queue/", "/topic/");
26 | registry.setApplicationDestinationPrefixes("/app");
27 | registry.setPreservePublishOrder(true);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/test/resources/log4j.detailed:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/service/Quote.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.service;
17 |
18 | import java.math.BigDecimal;
19 |
20 |
21 | public class Quote {
22 |
23 | private String ticker;
24 |
25 | private BigDecimal price;
26 |
27 | public Quote(String ticker, BigDecimal price) {
28 | this.ticker = ticker;
29 | this.price = price;
30 | }
31 |
32 | private Quote() {
33 | }
34 |
35 | public String getTicker() {
36 | return this.ticker;
37 | }
38 |
39 | public void setTicker(String ticker) {
40 | this.ticker = ticker;
41 | }
42 |
43 | public BigDecimal getPrice() {
44 | return this.price;
45 | }
46 |
47 | public void setPrice(BigDecimal price) {
48 | this.price = price;
49 | }
50 |
51 | @Override
52 | public String toString() {
53 | return "Quote [ticker=" + this.ticker + ", price=" + this.price + "]";
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/test/resources/log4j.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/config/DispatcherServletInitializer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.config;
18 |
19 | import javax.servlet.ServletRegistration.Dynamic;
20 |
21 | import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
22 |
23 |
24 | public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
25 |
26 | @Override
27 | protected Class>[] getRootConfigClasses() {
28 | return new Class>[] { WebSecurityConfig.class };
29 | }
30 |
31 | @Override
32 | protected Class>[] getServletConfigClasses() {
33 | return new Class>[] { WebConfig.class, WebSocketConfig.class, WebSocketSecurityConfig.class };
34 | }
35 |
36 | @Override
37 | protected String[] getServletMappings() {
38 | return new String[] { "/" };
39 | }
40 |
41 | @Override
42 | protected void customizeRegistration(Dynamic registration) {
43 | registration.setInitParameter("dispatchOptionsRequest", "true");
44 | }
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/standalone/TestMessageChannel.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.standalone;
18 |
19 | import org.springframework.messaging.Message;
20 | import org.springframework.messaging.MessageChannel;
21 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
22 | import org.springframework.messaging.support.AbstractSubscribableChannel;
23 |
24 | import java.util.ArrayList;
25 | import java.util.List;
26 | import java.util.concurrent.ArrayBlockingQueue;
27 | import java.util.concurrent.BlockingQueue;
28 | import java.util.concurrent.TimeUnit;
29 |
30 | /**
31 | *
32 | *
33 | */
34 | public class TestMessageChannel extends AbstractSubscribableChannel {
35 |
36 | private final List> messages = new ArrayList<>();
37 |
38 |
39 | public List> getMessages() {
40 | return this.messages;
41 | }
42 |
43 | @Override
44 | protected boolean sendInternal(Message> message, long timeout) {
45 | this.messages.add(message);
46 | return true;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/web/UserController.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2015 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.web;
17 |
18 | import java.util.ArrayList;
19 | import java.util.List;
20 |
21 | import org.springframework.beans.factory.annotation.Autowired;
22 | import org.springframework.messaging.simp.user.SimpUser;
23 | import org.springframework.messaging.simp.user.SimpUserRegistry;
24 | import org.springframework.web.bind.annotation.RequestMapping;
25 | import org.springframework.web.bind.annotation.RequestMethod;
26 | import org.springframework.web.bind.annotation.RestController;
27 |
28 | @RestController
29 | public class UserController {
30 |
31 | private SimpUserRegistry userRegistry;
32 |
33 |
34 | @Autowired
35 | public UserController(SimpUserRegistry userRegistry) {
36 | this.userRegistry = userRegistry;
37 | }
38 |
39 |
40 | @RequestMapping(path = "/users", method = RequestMethod.GET)
41 | public List listUsers() {
42 | List result = new ArrayList<>();
43 | for (SimpUser user : this.userRegistry.getUsers()) {
44 | result.add(user.toString());
45 | }
46 | return result;
47 | }
48 |
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/config/WebSocketSecurityConfig.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2019 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.config;
17 |
18 | import org.springframework.context.annotation.Configuration;
19 | import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
20 | import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;
21 |
22 | import static org.springframework.messaging.simp.SimpMessageType.*;
23 |
24 | @Configuration
25 | public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
26 |
27 | @Override
28 | protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
29 | messages
30 | .nullDestMatcher().authenticated()
31 | .simpSubscribeDestMatchers("/user/queue/errors").permitAll()
32 | .simpSubscribeDestMatchers("/topic/*", "/user/**").hasRole("USER")
33 | .simpDestMatchers("/app/**").hasRole("USER")
34 | .simpTypeMatchers(SUBSCRIBE, MESSAGE).denyAll()
35 | .anyMessage().denyAll();
36 | }
37 |
38 | @Override
39 | protected boolean sameOriginDisabled() {
40 | // While CSRF is disabled..
41 | return true;
42 | }
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/service/Trade.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.service;
17 |
18 |
19 | public class Trade {
20 |
21 | private String ticker;
22 |
23 | private int shares;
24 |
25 | private TradeAction action;
26 |
27 | private String username;
28 |
29 |
30 | public String getTicker() {
31 | return this.ticker;
32 | }
33 |
34 | public void setTicker(String ticker) {
35 | this.ticker = ticker;
36 | }
37 |
38 | public int getShares() {
39 | return this.shares;
40 | }
41 |
42 | public void setShares(int shares) {
43 | this.shares = shares;
44 | }
45 |
46 | public TradeAction getAction() {
47 | return this.action;
48 | }
49 |
50 | public void setAction(TradeAction action) {
51 | this.action = action;
52 | }
53 |
54 | public String getUsername() {
55 | return this.username;
56 | }
57 |
58 | public void setUsername(String username) {
59 | this.username = username;
60 | }
61 |
62 | @Override
63 | public String toString() {
64 | return "[ticker=" + this.ticker + ", shares=" + this.shares
65 | + ", action=" + this.action + ", username=" + this.username + "]";
66 | }
67 |
68 |
69 | public enum TradeAction {
70 | Buy, Sell;
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/support/JettyWebSocketTestServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.support;
18 |
19 | import org.eclipse.jetty.server.Server;
20 | import org.eclipse.jetty.servlet.ServletContextHandler;
21 | import org.eclipse.jetty.servlet.ServletHolder;
22 | import org.springframework.web.context.WebApplicationContext;
23 | import org.springframework.web.servlet.DispatcherServlet;
24 |
25 | /**
26 | * Jetty based {@link WebSocketTestServer}.
27 | *
28 | * @author Rossen Stoyanchev
29 | */
30 | public class JettyWebSocketTestServer implements WebSocketTestServer {
31 |
32 | private final Server jettyServer;
33 |
34 | private final int port;
35 |
36 |
37 | public JettyWebSocketTestServer(int port) {
38 | this.port = port;
39 | this.jettyServer = new Server(this.port);
40 | }
41 |
42 | @Override
43 | public int getPort() {
44 | return this.port;
45 | }
46 |
47 | @Override
48 | public void deployDispatcherServlet(WebApplicationContext cxt) {
49 | ServletContextHandler contextHandler = new ServletContextHandler();
50 | ServletHolder servletHolder = new ServletHolder(new DispatcherServlet(cxt));
51 | contextHandler.addServlet(servletHolder, "/");
52 | this.jettyServer.setHandler(contextHandler);
53 | }
54 |
55 | @Override
56 | public void undeployConfig() {
57 | // Stopping jetty will undeploy the servlet
58 | }
59 |
60 | @Override
61 | public void start() throws Exception {
62 | this.jettyServer.start();
63 | }
64 |
65 | @Override
66 | public void stop() throws Exception {
67 | if (this.jettyServer.isRunning()) {
68 | this.jettyServer.stop();
69 | }
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/Portfolio.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio;
17 |
18 | import java.util.ArrayList;
19 | import java.util.LinkedHashMap;
20 | import java.util.List;
21 | import java.util.Map;
22 |
23 |
24 | public class Portfolio {
25 |
26 | private final Map positionLookup = new LinkedHashMap<>();
27 |
28 |
29 | public List getPositions() {
30 | return new ArrayList(positionLookup.values());
31 | }
32 |
33 | public void addPosition(PortfolioPosition position) {
34 | this.positionLookup.put(position.getTicker(), position);
35 | }
36 |
37 | public PortfolioPosition getPortfolioPosition(String ticker) {
38 | return this.positionLookup.get(ticker);
39 | }
40 |
41 | /**
42 | * @return the updated position or null
43 | */
44 | public PortfolioPosition buy(String ticker, int sharesToBuy) {
45 | PortfolioPosition position = this.positionLookup.get(ticker);
46 | if ((position == null) || (sharesToBuy < 1)) {
47 | return null;
48 | }
49 | position = new PortfolioPosition(position, sharesToBuy);
50 | this.positionLookup.put(ticker, position);
51 | return position;
52 | }
53 |
54 | /**
55 | * @return the updated position or null
56 | */
57 | public PortfolioPosition sell(String ticker, int sharesToSell) {
58 | PortfolioPosition position = this.positionLookup.get(ticker);
59 | if ((position == null) || (sharesToSell < 1) || (position.getShares() < sharesToSell)) {
60 | return null;
61 | }
62 | position = new PortfolioPosition(position, -sharesToSell);
63 | this.positionLookup.put(ticker, position);
64 | return position;
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/service/PortfolioServiceImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.service;
18 |
19 | import org.springframework.samples.portfolio.Portfolio;
20 | import org.springframework.samples.portfolio.PortfolioPosition;
21 | import org.springframework.stereotype.Service;
22 |
23 | import java.util.HashMap;
24 | import java.util.Map;
25 |
26 | /**
27 | * @author Rob Winch
28 | */
29 | @Service
30 | public class PortfolioServiceImpl implements PortfolioService {
31 |
32 | // user -> Portfolio
33 | private final Map portfolioLookup = new HashMap<>();
34 |
35 |
36 | public PortfolioServiceImpl() {
37 |
38 | Portfolio portfolio = new Portfolio();
39 | portfolio.addPosition(new PortfolioPosition("Citrix Systems, Inc.", "CTXS", 24.30, 75));
40 | portfolio.addPosition(new PortfolioPosition("Dell Inc.", "DELL", 13.44, 50));
41 | portfolio.addPosition(new PortfolioPosition("Microsoft", "MSFT", 34.15, 33));
42 | portfolio.addPosition(new PortfolioPosition("Oracle", "ORCL", 31.22, 45));
43 | this.portfolioLookup.put("fabrice", portfolio);
44 |
45 | portfolio = new Portfolio();
46 | portfolio.addPosition(new PortfolioPosition("EMC Corporation", "EMC", 24.30, 75));
47 | portfolio.addPosition(new PortfolioPosition("Google Inc", "GOOG", 905.09, 5));
48 | portfolio.addPosition(new PortfolioPosition("VMware, Inc.", "VMW", 65.58, 23));
49 | portfolio.addPosition(new PortfolioPosition("Red Hat", "RHT", 48.30, 15));
50 | this.portfolioLookup.put("paulson", portfolio);
51 | }
52 |
53 |
54 | public Portfolio findPortfolio(String username) {
55 | Portfolio portfolio = this.portfolioLookup.get(username);
56 | if (portfolio == null) {
57 | throw new IllegalArgumentException(username);
58 | }
59 | return portfolio;
60 | }
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/PortfolioPosition.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio;
17 |
18 |
19 | public class PortfolioPosition {
20 |
21 | private String company;
22 |
23 | private String ticker;
24 |
25 | private double price;
26 |
27 | private int shares;
28 |
29 | private long updateTime;
30 |
31 |
32 | public PortfolioPosition(String company, String ticker, double price, int shares) {
33 | this.company = company;
34 | this.ticker = ticker;
35 | this.price = price;
36 | this.shares = shares;
37 | this.updateTime = System.currentTimeMillis();
38 | }
39 |
40 | public PortfolioPosition(PortfolioPosition other, int sharesToAddOrSubtract) {
41 | this.company = other.company;
42 | this.ticker = other.ticker;
43 | this.price = other.price;
44 | this.shares = other.shares + sharesToAddOrSubtract;
45 | this.updateTime = System.currentTimeMillis();
46 | }
47 |
48 | private PortfolioPosition() {
49 | }
50 |
51 | public String getCompany() {
52 | return this.company;
53 | }
54 |
55 | public void setCompany(String company) {
56 | this.company = company;
57 | }
58 |
59 | public String getTicker() {
60 | return this.ticker;
61 | }
62 |
63 | public void setTicker(String ticker) {
64 | this.ticker = ticker;
65 | }
66 |
67 | public double getPrice() {
68 | return this.price;
69 | }
70 |
71 | public void setPrice(double price) {
72 | this.price = price;
73 | }
74 |
75 | public int getShares() {
76 | return this.shares;
77 | }
78 |
79 | public void setShares(int shares) {
80 | this.shares = shares;
81 | }
82 |
83 | public long getUpdateTime() {
84 | return this.updateTime;
85 | }
86 |
87 | public void setUpdateTime(long updateTime) {
88 | this.updateTime = updateTime;
89 | }
90 |
91 | @Override
92 | public String toString() {
93 | return "PortfolioPosition [company=" + this.company + ", ticker=" + this.ticker
94 | + ", price=" + this.price + ", shares=" + this.shares + "]";
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/context/TestChannelInterceptor.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2015 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.context;
18 |
19 | import org.springframework.messaging.Message;
20 | import org.springframework.messaging.MessageChannel;
21 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
22 | import org.springframework.messaging.support.ChannelInterceptorAdapter;
23 | import org.springframework.util.AntPathMatcher;
24 | import org.springframework.util.PathMatcher;
25 |
26 | import java.util.ArrayList;
27 | import java.util.Arrays;
28 | import java.util.List;
29 | import java.util.concurrent.ArrayBlockingQueue;
30 | import java.util.concurrent.BlockingQueue;
31 | import java.util.concurrent.TimeUnit;
32 |
33 | /**
34 | * A ChannelInterceptor that caches messages.
35 | */
36 | public class TestChannelInterceptor extends ChannelInterceptorAdapter {
37 |
38 | private final BlockingQueue> messages = new ArrayBlockingQueue<>(100);
39 |
40 | private final List destinationPatterns = new ArrayList<>();
41 |
42 | private final PathMatcher matcher = new AntPathMatcher();
43 |
44 |
45 | public void setIncludedDestinations(String... patterns) {
46 | this.destinationPatterns.addAll(Arrays.asList(patterns));
47 | }
48 |
49 | /**
50 | * @return the next received message or {@code null} if the specified time elapses
51 | */
52 | public Message> awaitMessage(long timeoutInSeconds) throws InterruptedException {
53 | return this.messages.poll(timeoutInSeconds, TimeUnit.SECONDS);
54 | }
55 |
56 | @Override
57 | public Message> preSend(Message> message, MessageChannel channel) {
58 | if (this.destinationPatterns.isEmpty()) {
59 | this.messages.add(message);
60 | }
61 | else {
62 | StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
63 | if (headers.getDestination() != null) {
64 | for (String pattern : this.destinationPatterns) {
65 | if (this.matcher.match(pattern, headers.getDestination())) {
66 | this.messages.add(message);
67 | break;
68 | }
69 | }
70 | }
71 | }
72 | return message;
73 | }
74 |
75 | }
76 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/web/PortfolioController.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.web;
17 |
18 | import java.security.Principal;
19 | import java.util.List;
20 |
21 | import org.apache.commons.logging.Log;
22 | import org.apache.commons.logging.LogFactory;
23 | import org.springframework.beans.factory.annotation.Autowired;
24 | import org.springframework.messaging.handler.annotation.MessageExceptionHandler;
25 | import org.springframework.messaging.handler.annotation.MessageMapping;
26 | import org.springframework.messaging.simp.annotation.SendToUser;
27 | import org.springframework.messaging.simp.annotation.SubscribeMapping;
28 | import org.springframework.samples.portfolio.Portfolio;
29 | import org.springframework.samples.portfolio.PortfolioPosition;
30 | import org.springframework.samples.portfolio.service.PortfolioService;
31 | import org.springframework.samples.portfolio.service.Trade;
32 | import org.springframework.samples.portfolio.service.TradeService;
33 | import org.springframework.stereotype.Controller;
34 |
35 |
36 | @Controller
37 | public class PortfolioController {
38 |
39 | private static final Log logger = LogFactory.getLog(PortfolioController.class);
40 |
41 | private final PortfolioService portfolioService;
42 |
43 | private final TradeService tradeService;
44 |
45 |
46 | @Autowired
47 | public PortfolioController(PortfolioService portfolioService, TradeService tradeService) {
48 | this.portfolioService = portfolioService;
49 | this.tradeService = tradeService;
50 | }
51 |
52 | @SubscribeMapping("/positions")
53 | public List getPositions(Principal principal) {
54 | logger.debug("Positions for " + principal.getName());
55 | Portfolio portfolio = this.portfolioService.findPortfolio(principal.getName());
56 | return portfolio.getPositions();
57 | }
58 |
59 | @MessageMapping("/trade")
60 | public void executeTrade(Trade trade, Principal principal) {
61 | trade.setUsername(principal.getName());
62 | logger.debug("Trade: " + trade);
63 | this.tradeService.executeTrade(trade);
64 | }
65 |
66 | @MessageExceptionHandler
67 | @SendToUser("/queue/errors")
68 | public String handleException(Throwable exception) {
69 | return exception.getMessage();
70 | }
71 |
72 | }
73 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/config/WebSecurityConfig.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.config;
17 |
18 | import org.springframework.context.annotation.Configuration;
19 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
20 | import org.springframework.security.config.annotation.web.builders.HttpSecurity;
21 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
22 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
23 | import org.springframework.security.crypto.factory.PasswordEncoderFactories;
24 | import org.springframework.security.crypto.password.PasswordEncoder;
25 | import org.springframework.security.web.header.writers.frameoptions.XFrameOptionsHeaderWriter;
26 |
27 | /**
28 | * Customizes Spring Security configuration.
29 | * @author Rob Winch
30 | */
31 | @EnableWebSecurity
32 | @Configuration
33 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
34 |
35 | @Override
36 | protected void configure(HttpSecurity http) throws Exception {
37 | http
38 | .csrf().disable() // Refactor login form
39 |
40 | // See https://jira.springsource.org/browse/SPR-11496
41 | .headers().addHeaderWriter(
42 | new XFrameOptionsHeaderWriter(
43 | XFrameOptionsHeaderWriter.XFrameOptionsMode.SAMEORIGIN)).and()
44 |
45 | .formLogin()
46 | .defaultSuccessUrl("/index.html")
47 | .loginPage("/login.html")
48 | .failureUrl("/login.html?error")
49 | .permitAll()
50 | .and()
51 | .logout()
52 | .logoutSuccessUrl("/login.html?logout")
53 | .logoutUrl("/logout.html")
54 | .permitAll()
55 | .and()
56 | .authorizeRequests()
57 | .antMatchers("/static/**").permitAll()
58 | .antMatchers("/webjars/**").permitAll()
59 | .anyRequest().authenticated()
60 | .and();
61 | }
62 |
63 |
64 | @Override
65 | protected void configure(AuthenticationManagerBuilder auth) throws Exception {
66 | PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
67 | auth
68 | .inMemoryAuthentication()
69 | .withUser("fabrice").password(encoder.encode("fab123")).roles("USER").and()
70 | .withUser("paulson").password(encoder.encode("bond")).roles("ADMIN","USER");
71 | }
72 | }
--------------------------------------------------------------------------------
/src/main/webapp/static/js/services.js:
--------------------------------------------------------------------------------
1 | angular.module('springPortfolio.services', [])
2 | //.constant('sockJsProtocols', ["xhr-streaming", "xhr-polling"]) // only allow XHR protocols
3 | .constant('sockJsProtocols', [])
4 | .factory('StompClient', ['sockJsProtocols', '$q', function (sockJsProtocols, $q) {
5 | var stompClient;
6 | var wrappedSocket = {
7 | init: function (url) {
8 | if (sockJsProtocols.length > 0) {
9 | stompClient = webstomp.over(new SockJS(url, null, {transports: sockJsProtocols}));
10 | }
11 | else {
12 | stompClient = webstomp.over(new SockJS(url));
13 | }
14 | },
15 | connect: function () {
16 | return $q(function (resolve, reject) {
17 | if (!stompClient) {
18 | reject("STOMP client not created");
19 | } else {
20 | stompClient.connect({}, function (frame) {
21 | resolve(frame);
22 | }, function (error) {
23 | reject("STOMP protocol error " + error);
24 | });
25 | }
26 | });
27 | },
28 | disconnect: function() {
29 | stompClient.disconnect();
30 | },
31 | subscribe: function (destination) {
32 | var deferred = $q.defer();
33 | if (!stompClient) {
34 | deferred.reject("STOMP client not created");
35 | } else {
36 | stompClient.subscribe(destination, function (message) {
37 | deferred.notify(JSON.parse(message.body));
38 | });
39 | }
40 | return deferred.promise;
41 | },
42 | subscribeSingle: function (destination) {
43 | return $q(function (resolve, reject) {
44 | if (!stompClient) {
45 | reject("STOMP client not created");
46 | } else {
47 | stompClient.subscribe(destination, function (message) {
48 | resolve(JSON.parse(message.body));
49 | });
50 | }
51 | });
52 | },
53 | send: function (destination, object, headers) {
54 | stompClient.send(destination, object, headers);
55 | }
56 | };
57 | return wrappedSocket;
58 | }])
59 | .factory('TradeService', ['StompClient', '$q', function (stompClient, $q) {
60 |
61 | return {
62 | connect: function (url) {
63 | stompClient.init(url);
64 | return stompClient.connect().then(function (frame) {
65 | return frame.headers['user-name'];
66 | });
67 | },
68 | disconnect: function() {
69 | stompClient.disconnect();
70 | },
71 | loadPositions: function() {
72 | return stompClient.subscribeSingle("/app/positions");
73 | },
74 | fetchQuoteStream: function () {
75 | return stompClient.subscribe("/topic/price.stock.*");
76 | },
77 | fetchPositionUpdateStream: function () {
78 | return stompClient.subscribe("/user/queue/position-updates");
79 | },
80 | fetchErrorStream: function () {
81 | return stompClient.subscribe("/user/queue/errors");
82 | },
83 | sendTradeOrder: function(tradeOrder) {
84 | return stompClient.send("/app/trade", JSON.stringify(tradeOrder), {});
85 | }
86 | };
87 |
88 | }]);
89 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/service/TradeServiceImpl.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.service;
17 |
18 | import org.apache.commons.logging.Log;
19 | import org.apache.commons.logging.LogFactory;
20 | import org.springframework.beans.factory.annotation.Autowired;
21 | import org.springframework.messaging.MessageHeaders;
22 | import org.springframework.messaging.simp.SimpMessageSendingOperations;
23 | import org.springframework.samples.portfolio.Portfolio;
24 | import org.springframework.samples.portfolio.PortfolioPosition;
25 | import org.springframework.samples.portfolio.service.Trade.TradeAction;
26 | import org.springframework.scheduling.annotation.Scheduled;
27 | import org.springframework.stereotype.Service;
28 | import org.springframework.util.MimeTypeUtils;
29 |
30 | import java.util.HashMap;
31 | import java.util.List;
32 | import java.util.Map;
33 | import java.util.concurrent.CopyOnWriteArrayList;
34 |
35 |
36 | @Service
37 | public class TradeServiceImpl implements TradeService {
38 |
39 | private static final Log logger = LogFactory.getLog(TradeServiceImpl.class);
40 |
41 | private final SimpMessageSendingOperations messagingTemplate;
42 |
43 | private final PortfolioService portfolioService;
44 |
45 | private final List tradeResults = new CopyOnWriteArrayList<>();
46 |
47 |
48 | @Autowired
49 | public TradeServiceImpl(SimpMessageSendingOperations messagingTemplate, PortfolioService portfolioService) {
50 | this.messagingTemplate = messagingTemplate;
51 | this.portfolioService = portfolioService;
52 | }
53 |
54 | /**
55 | * In real application a trade is probably executed in an external system, i.e. asynchronously.
56 | */
57 | public void executeTrade(Trade trade) {
58 |
59 | Portfolio portfolio = this.portfolioService.findPortfolio(trade.getUsername());
60 | String ticker = trade.getTicker();
61 | int sharesToTrade = trade.getShares();
62 |
63 | PortfolioPosition newPosition = (trade.getAction() == TradeAction.Buy) ?
64 | portfolio.buy(ticker, sharesToTrade) : portfolio.sell(ticker, sharesToTrade);
65 |
66 | if (newPosition == null) {
67 | String payload = "Rejected trade " + trade;
68 | this.messagingTemplate.convertAndSendToUser(trade.getUsername(), "/queue/errors", payload);
69 | return;
70 | }
71 |
72 | this.tradeResults.add(new TradeResult(trade.getUsername(), newPosition));
73 | }
74 |
75 | @Scheduled(fixedDelay=1500)
76 | public void sendTradeNotifications() {
77 |
78 | Map map = new HashMap<>();
79 | map.put(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON);
80 |
81 | for (TradeResult result : this.tradeResults) {
82 | if (System.currentTimeMillis() >= (result.timestamp + 1500)) {
83 | logger.debug("Sending position update: " + result.position);
84 | this.messagingTemplate.convertAndSendToUser(result.user, "/queue/position-updates", result.position, map);
85 | this.tradeResults.remove(result);
86 | }
87 | }
88 | }
89 |
90 |
91 | private static class TradeResult {
92 |
93 | private final String user;
94 | private final PortfolioPosition position;
95 | private final long timestamp;
96 |
97 | public TradeResult(String user, PortfolioPosition position) {
98 | this.user = user;
99 | this.position = position;
100 | this.timestamp = System.currentTimeMillis();
101 | }
102 | }
103 |
104 | }
105 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/samples/portfolio/service/QuoteService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package org.springframework.samples.portfolio.service;
17 |
18 | import java.math.BigDecimal;
19 | import java.math.MathContext;
20 | import java.util.HashSet;
21 | import java.util.Map;
22 | import java.util.Random;
23 | import java.util.Set;
24 | import java.util.concurrent.ConcurrentHashMap;
25 | import java.util.concurrent.atomic.AtomicBoolean;
26 |
27 | import org.apache.commons.logging.Log;
28 | import org.apache.commons.logging.LogFactory;
29 | import org.springframework.beans.factory.annotation.Autowired;
30 | import org.springframework.context.ApplicationListener;
31 | import org.springframework.messaging.core.MessageSendingOperations;
32 | import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent;
33 | import org.springframework.scheduling.annotation.Scheduled;
34 | import org.springframework.stereotype.Service;
35 |
36 |
37 | @Service
38 | public class QuoteService implements ApplicationListener {
39 |
40 | private static Log logger = LogFactory.getLog(QuoteService.class);
41 |
42 | private final MessageSendingOperations messagingTemplate;
43 |
44 | private final StockQuoteGenerator quoteGenerator = new StockQuoteGenerator();
45 |
46 | private AtomicBoolean brokerAvailable = new AtomicBoolean();
47 |
48 |
49 | @Autowired
50 | public QuoteService(MessageSendingOperations messagingTemplate) {
51 | this.messagingTemplate = messagingTemplate;
52 | }
53 |
54 | @Override
55 | public void onApplicationEvent(BrokerAvailabilityEvent event) {
56 | this.brokerAvailable.set(event.isBrokerAvailable());
57 | }
58 |
59 | @Scheduled(fixedDelay=2000)
60 | public void sendQuotes() {
61 | for (Quote quote : this.quoteGenerator.generateQuotes()) {
62 | if (logger.isTraceEnabled()) {
63 | logger.trace("Sending quote " + quote);
64 | }
65 | if (this.brokerAvailable.get()) {
66 | this.messagingTemplate.convertAndSend("/topic/price.stock." + quote.getTicker(), quote);
67 | }
68 | }
69 | }
70 |
71 |
72 | private static class StockQuoteGenerator {
73 |
74 | private static final MathContext mathContext = new MathContext(2);
75 |
76 | private final Random random = new Random();
77 |
78 | private final Map prices = new ConcurrentHashMap<>();
79 |
80 |
81 | public StockQuoteGenerator() {
82 | this.prices.put("CTXS", "24.30");
83 | this.prices.put("DELL", "13.03");
84 | this.prices.put("EMC", "24.13");
85 | this.prices.put("GOOG", "893.49");
86 | this.prices.put("MSFT", "34.21");
87 | this.prices.put("ORCL", "31.22");
88 | this.prices.put("RHT", "48.30");
89 | this.prices.put("VMW", "66.98");
90 | }
91 |
92 | public Set generateQuotes() {
93 | Set quotes = new HashSet<>();
94 | for (String ticker : this.prices.keySet()) {
95 | BigDecimal price = getPrice(ticker);
96 | quotes.add(new Quote(ticker, price));
97 | }
98 | return quotes;
99 | }
100 |
101 | private BigDecimal getPrice(String ticker) {
102 | BigDecimal seedPrice = new BigDecimal(this.prices.get(ticker), mathContext);
103 | double range = seedPrice.multiply(new BigDecimal(0.02)).doubleValue();
104 | BigDecimal priceChange = new BigDecimal(String.valueOf(this.random.nextDouble() * range), mathContext);
105 | return seedPrice.add(priceChange);
106 | }
107 |
108 | }
109 |
110 | }
111 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/support/TomcatWebSocketTestServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2022 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.support;
18 |
19 | import java.io.File;
20 | import java.io.IOException;
21 | import java.nio.file.Files;
22 | import java.util.Arrays;
23 | import java.util.HashSet;
24 |
25 | import org.apache.catalina.Context;
26 | import org.apache.catalina.Wrapper;
27 | import org.apache.catalina.connector.Connector;
28 | import org.apache.catalina.startup.Tomcat;
29 | import org.apache.coyote.http11.Http11NioProtocol;
30 | import org.apache.tomcat.websocket.server.WsContextListener;
31 |
32 | import org.springframework.web.SpringServletContainerInitializer;
33 | import org.springframework.web.WebApplicationInitializer;
34 | import org.springframework.web.context.WebApplicationContext;
35 | import org.springframework.web.servlet.DispatcherServlet;
36 |
37 | /**
38 | * A wrapper around an embedded {@link org.apache.catalina.startup.Tomcat} server
39 | * for use in testing Spring WebSocket applications.
40 | *
41 | * Ensures the Tomcat's WsContextListener is deployed and helps with loading
42 | * Spring configuration and deploying Spring MVC's DispatcherServlet.
43 | *
44 | * @author Rossen Stoyanchev
45 | */
46 | public class TomcatWebSocketTestServer implements WebSocketTestServer {
47 |
48 | private final Tomcat tomcatServer;
49 |
50 | private final int port;
51 |
52 | private final File baseDir;
53 |
54 | private Context context;
55 |
56 |
57 | public TomcatWebSocketTestServer(int port) {
58 |
59 | this.port = port;
60 | this.baseDir = createBaseDir(port);
61 |
62 | Connector connector = new Connector(Http11NioProtocol.class.getName());
63 | connector.setPort(this.port);
64 |
65 | this.tomcatServer = new Tomcat();
66 | this.tomcatServer.setBaseDir(this.baseDir.getAbsolutePath());
67 | this.tomcatServer.setPort(this.port);
68 | this.tomcatServer.getService().addConnector(connector);
69 | this.tomcatServer.setConnector(connector);
70 | }
71 |
72 | private static File createBaseDir(int port) {
73 | try {
74 | File file = Files.createTempDirectory("tomcat." + "." + port).toFile();
75 | file.deleteOnExit();
76 | return file;
77 | }
78 | catch (IOException ex) {
79 | throw new RuntimeException("Unable to create temp directory", ex);
80 | }
81 | }
82 |
83 | public int getPort() {
84 | return this.port;
85 | }
86 |
87 | @Override
88 | public void deployDispatcherServlet(WebApplicationContext cxt) {
89 | this.context = this.tomcatServer.addContext("", this.baseDir.getAbsolutePath());
90 | Tomcat.addServlet(context, "dispatcherServlet", new DispatcherServlet(cxt));
91 | this.context.addServletMappingDecoded("/", "dispatcherServlet");
92 | this.context.addApplicationListener(WsContextListener.class.getName());
93 | }
94 |
95 | public void deployWithInitializer(Class extends WebApplicationInitializer>... initializers) {
96 | this.context = this.tomcatServer.addContext("", this.baseDir.getAbsolutePath());
97 | Tomcat.addServlet(this.context, "default", "org.apache.catalina.servlets.DefaultServlet");
98 | this.context.addApplicationListener(WsContextListener.class.getName());
99 | this.context.addServletContainerInitializer(new SpringServletContainerInitializer(),
100 | new HashSet<>(Arrays.asList(initializers)));
101 | }
102 |
103 | public void undeployConfig() {
104 | if (this.context != null) {
105 | this.tomcatServer.getHost().removeChild(this.context);
106 | }
107 | }
108 |
109 | public void start() throws Exception {
110 | this.tomcatServer.start();
111 | }
112 |
113 | public void stop() throws Exception {
114 | this.tomcatServer.stop();
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Overview
2 |
3 | A sample demonstrating capabilities in the Spring Framework to build WebSocket-style messaging applications. The application uses [STOMP](http://stomp.github.io/) (over WebSocket) for messaging between browsers and server and [SockJS](https://github.com/sockjs/sockjs-protocol) for WebSocket fallback options.
4 |
5 | Client-side libraries used:
6 | * [stomp-websocket](https://github.com/jmesnil/stomp-websocket/)
7 | * [sockjs-client](https://github.com/sockjs/sockjs-client)
8 | * [Twitter Bootstrap](http://twitter.github.io/bootstrap/)
9 | * [Knockout.js](http://knockoutjs.com/)
10 |
11 | Server-side runs on Tomcat, Jetty, WildFly, Glassfish, and other Servlet 3.0+ containers with WebSocket support.
12 |
13 | ### Tomcat
14 |
15 | For Tomcat, set `TOMCAT_HOME` as an environment variable and use [deployTomcat.sh](https://github.com/rstoyanchev/spring-websocket-portfolio/blob/master/deployTomcat.sh) and [shutdownTomcat.sh](https://github.com/rstoyanchev/spring-websocket-portfolio/blob/master/shutdownTomcat.sh) in this directory.
16 |
17 | Open a browser and go to
18 |
19 | ### Jetty
20 |
21 | The easiest way to run on Jetty is with `mvn jetty:run`.
22 |
23 | Open a browser and go to
24 |
25 | **Note:** To deploy to a Jetty installation, add this to Jetty's `start.ini`:
26 |
27 | OPTIONS=plus
28 | etc/jetty-plus.xml
29 | OPTIONS=annotations
30 | etc/jetty-annotations.xml
31 |
32 | ### WildFly 10+
33 |
34 | Unzip the WildFly server.
35 |
36 | Set `WILDFLY_HOME` as an environment variable and use [deployWildFly.sh](https://github.com/rstoyanchev/spring-websocket-portfolio/blob/master/deployWildFly.sh) in this directory.
37 |
38 | Open a browser and go to
39 |
40 | ### WebSphere Liberty 16+
41 |
42 | Build and deploy with the following server configuration:
43 |
44 |
45 |
46 |
47 |
48 | jsp-2.3
49 | webSocket-1.1
50 |
51 |
52 |
55 |
56 |
57 |
58 |
59 | ### Glassfish
60 |
61 | After unzipping Glassfish 4 start the server:
62 |
63 | /glassfish4/bin/asadmin start-domain
64 |
65 | Set `GLASSFISH4_HOME` as an environment variable and use [deployGlassfish.sh](https://github.com/rstoyanchev/spring-websocket-portfolio/blob/master/deployGlassfish.sh) in this directory.
66 |
67 | Open a browser and go to
68 |
69 |
70 | ### Using a Message Broker
71 |
72 | Out of the box, a _"simple" message broker_ is used to send messages to subscribers (e.g. stock quotes) but you can optionally use a fully featured STOMP message broker such as `RabbitMQ`, `ActiveMQ`, and others, by following these steps:
73 |
74 | 1. Install and start the message broker. For RabbitMQ make sure you've also installed the [RabbitMQ STOMP plugin](http://www.rabbitmq.com/stomp.html). For ActiveMQ you need to configure a [STOMP transport connnector](http://activemq.apache.org/stomp.html).
75 | 2. Use the `MessageBrokerConfigurer` in [WebSocketConfig.java](https://github.com/rstoyanchev/spring-websocket-portfolio/blob/master/src/main/java/org/springframework/samples/portfolio/config/WebSocketConfig.java) to enable the STOMP broker relay instead of the simple broker.
76 | 3. You may also need to configure additional STOMP broker relay properties such as `relayHost`, `relayPort`, `systemLogin`, `systemPassword`, depending on your message broker. The default settings should work for RabbitMQ and ActiveMQ.
77 |
78 |
79 | ### Logging
80 |
81 | To see all logging, enable TRACE for `org.springframework.messaging` and `org.springframework.samples` in [log4j.xml](https://github.com/rstoyanchev/spring-websocket-portfolio/blob/master/src/main/resources/log4j.xml).
82 |
83 | Keep in mind that will generate a lot of information as messages flow through the application. The [QuoteService](https://github.com/rstoyanchev/spring-websocket-portfolio/blob/master/src/main/java/org/springframework/samples/portfolio/service/QuoteService.java) for example generates a lot of messages frequently. You can modify it to send quotes less frequently or simply comment out the `@Scheduled` annotation.
84 |
85 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/src/main/webapp/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Stock Trading Portfolio
6 |
7 |
8 |
9 |
10 |
11 |
12 |
78 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/src/main/webapp/static/js/controllers.js:
--------------------------------------------------------------------------------
1 | angular.module('springPortfolio.controllers', ['ui.bootstrap'])
2 | .constant("buy", "Buy")
3 | .constant("sell", "Sell")
4 | .controller('PortfolioController',
5 | ['$scope', '$uibModal', 'TradeService',
6 | function ($scope, $uibModal, tradeService) {
7 | $scope.notifications = [];
8 | $scope.positions = {};
9 |
10 | var processQuote = function(quote) {
11 | var existing = $scope.positions[quote.ticker];
12 | if(existing) {
13 | existing.change = quote.price - existing.price;
14 | existing.price = quote.price;
15 | }
16 | };
17 | var udpatePosition = function(position) {
18 | var existing = $scope.positions[position.ticker];
19 | if(existing) {
20 | existing.shares = position.shares;
21 | }
22 | };
23 | var pushNotification = function(message) {
24 | $scope.notifications.unshift(message);
25 | };
26 |
27 | var validateTrade = function(trade) {
28 | if (isNaN(trade.shares) || (trade.shares < 1)) {
29 | $scope.notifications.push("Trade Error: Invalid number of shares");
30 | return false;
31 | }
32 | if ((trade.action === "Sell") && (trade.shares > $scope.positions[trade.ticker].shares)) {
33 | $scope.notifications.push("Trade Error: Not enough shares");
34 | return false;
35 | }
36 | return true;
37 | }
38 |
39 | $scope.openTradeModal = function (action, position) {
40 | var modalInstance = $uibModal.open({
41 | templateUrl: 'tradeModal.html',
42 | controller: 'TradeModalController',
43 | size: "sm",
44 | resolve: {
45 | action: action,
46 | position: position
47 | }
48 | });
49 | modalInstance.result.then(function (result) {
50 | var trade = {
51 | "action" : result.action,
52 | "ticker" : result.position.ticker,
53 | "shares" : result.numberOfShares
54 | };
55 | if(validateTrade(trade)) {
56 | tradeService.sendTradeOrder(trade);
57 | }
58 | });
59 | };
60 |
61 | $scope.logout = function() {
62 | tradeService.disconnect();
63 | };
64 |
65 | tradeService.connect("/spring-websocket-portfolio/portfolio")
66 | .then(function (username) {
67 | $scope.username = username;
68 | pushNotification("Trade results take a 2-3 second simulated delay. Notifications will appear.");
69 | return tradeService.loadPositions();
70 | },
71 | function (error) {
72 | pushNotification(error);
73 | })
74 | .then(function (positions) {
75 | positions.forEach(function(pos) {
76 | $scope.positions[pos.ticker] = pos;
77 | });
78 | tradeService.fetchQuoteStream().then(null, null,
79 | function(quote) {
80 | processQuote(quote);
81 | }
82 | );
83 | tradeService.fetchPositionUpdateStream().then(null, null,
84 | function(position) {
85 | udpatePosition(position);
86 | }
87 | );
88 | tradeService.fetchErrorStream().then(null, null,
89 | function (error) {
90 | pushNotification(error);
91 | }
92 | );
93 | });
94 |
95 | }])
96 | .controller('TradeModalController',
97 | ["$scope", "$uibModalInstance", "TradeService", "action", "position",
98 | function ($scope, $uibModalInstance, tradeService, action, position) {
99 |
100 | $scope.action = action;
101 | $scope.position = position;
102 | $scope.numberOfShares = 0;
103 | $scope.trade = function () {
104 | $uibModalInstance.close({action: action, position: position, numberOfShares: $scope.numberOfShares});
105 | };
106 | $scope.cancel = function () {
107 | $uibModalInstance.dismiss('cancel');
108 | };
109 | }])
110 | .filter('percent', ['$filter', function ($filter) {
111 | return function (input, total) {
112 | return $filter('number')(input / total * 100, 1) + '%';
113 | };
114 | }])
115 | .filter('totalPortfolioShares', [function () {
116 | return function (positions) {
117 | var total = 0;
118 | for(var ticker in positions) {
119 | total += positions[ticker].shares;
120 | }
121 | return total;
122 | };
123 | }])
124 | .filter('totalPortfolioValue', [function () {
125 | return function (positions) {
126 | var total = 0;
127 | for(var ticker in positions) {
128 | total += positions[ticker].price * positions[ticker].shares;
129 | }
130 | return total;
131 | };
132 | }]);
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/standalone/StandalonePortfolioControllerTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2013 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.standalone;
18 |
19 | import static org.junit.Assert.*;
20 |
21 | import java.nio.charset.Charset;
22 | import java.util.Arrays;
23 | import java.util.HashMap;
24 |
25 | import com.fasterxml.jackson.databind.ObjectMapper;
26 | import org.junit.Before;
27 | import org.junit.Test;
28 |
29 | import org.springframework.context.support.StaticApplicationContext;
30 | import org.springframework.messaging.Message;
31 | import org.springframework.messaging.MessageChannel;
32 | import org.springframework.messaging.SubscribableChannel;
33 | import org.springframework.messaging.converter.MappingJackson2MessageConverter;
34 | import org.springframework.messaging.simp.SimpMessageSendingOperations;
35 | import org.springframework.messaging.simp.SimpMessagingTemplate;
36 | import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
37 | import org.springframework.messaging.simp.stomp.StompCommand;
38 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
39 | import org.springframework.messaging.support.MessageBuilder;
40 | import org.springframework.samples.portfolio.service.PortfolioService;
41 | import org.springframework.samples.portfolio.service.PortfolioServiceImpl;
42 | import org.springframework.samples.portfolio.service.Trade;
43 | import org.springframework.samples.portfolio.web.PortfolioController;
44 | import org.springframework.samples.portfolio.web.support.TestPrincipal;
45 | import org.springframework.test.util.JsonPathExpectationsHelper;
46 |
47 | /**
48 | * Tests for PortfolioController that instantiate directly the minimum
49 | * infrastructure necessary to test annotated controller methods and do not load
50 | * Spring configuration.
51 | *
52 | * Tests can create a Spring {@link org.springframework.messaging.Message} that
53 | * represents a STOMP frame and send it directly to the
54 | * SimpAnnotationMethodMessageHandler responsible for invoking annotated controller
55 | * methods.
56 | *
57 | * Test message channels can be used to detect any messages the controller may send.
58 | * It's also easy to inject the controller with a test-specific TradeService to
59 | * verify what trades are getting executed.
60 | *
61 | * The test strategy here is to test the behavior of controllers taking into
62 | * account controller annotations and nothing more. The tests are simpler to write
63 | * and faster to executed. They provide the most amount of control and that is good
64 | * for writing as many controller tests as needed. Separate tests are still required
65 | * to verify the Spring configuration but those tests should be fewer overall.
66 | *
67 | * @author Rossen Stoyanchev
68 | */
69 | public class StandalonePortfolioControllerTests {
70 |
71 | private PortfolioService portfolioService;
72 |
73 | private TestTradeService tradeService;
74 |
75 | private TestMessageChannel clientOutboundChannel;
76 |
77 | private TestAnnotationMethodHandler annotationMethodHandler;
78 |
79 |
80 | @Before
81 | public void setup() {
82 |
83 | this.portfolioService = new PortfolioServiceImpl();
84 | this.tradeService = new TestTradeService();
85 | PortfolioController controller = new PortfolioController(this.portfolioService, this.tradeService);
86 |
87 | this.clientOutboundChannel = new TestMessageChannel();
88 |
89 | this.annotationMethodHandler = new TestAnnotationMethodHandler(
90 | new TestMessageChannel(), clientOutboundChannel, new SimpMessagingTemplate(new TestMessageChannel()));
91 |
92 | this.annotationMethodHandler.registerHandler(controller);
93 | this.annotationMethodHandler.setDestinationPrefixes(Arrays.asList("/app"));
94 | this.annotationMethodHandler.setMessageConverter(new MappingJackson2MessageConverter());
95 | this.annotationMethodHandler.setApplicationContext(new StaticApplicationContext());
96 | this.annotationMethodHandler.afterPropertiesSet();
97 | }
98 |
99 |
100 | @Test
101 | public void getPositions() throws Exception {
102 |
103 | StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
104 | headers.setSubscriptionId("0");
105 | headers.setDestination("/app/positions");
106 | headers.setSessionId("0");
107 | headers.setUser(new TestPrincipal("fabrice"));
108 | headers.setSessionAttributes(new HashMap<>());
109 | Message message = MessageBuilder.withPayload(new byte[0]).setHeaders(headers).build();
110 |
111 | this.annotationMethodHandler.handleMessage(message);
112 |
113 | assertEquals(1, this.clientOutboundChannel.getMessages().size());
114 | Message> reply = this.clientOutboundChannel.getMessages().get(0);
115 |
116 | StompHeaderAccessor replyHeaders = StompHeaderAccessor.wrap(reply);
117 | assertEquals("0", replyHeaders.getSessionId());
118 | assertEquals("0", replyHeaders.getSubscriptionId());
119 | assertEquals("/app/positions", replyHeaders.getDestination());
120 |
121 | String json = new String((byte[]) reply.getPayload(), Charset.forName("UTF-8"));
122 | new JsonPathExpectationsHelper("$[0].company").assertValue(json, "Citrix Systems, Inc.");
123 | new JsonPathExpectationsHelper("$[1].company").assertValue(json, "Dell Inc.");
124 | new JsonPathExpectationsHelper("$[2].company").assertValue(json, "Microsoft");
125 | new JsonPathExpectationsHelper("$[3].company").assertValue(json, "Oracle");
126 | }
127 |
128 | @Test
129 | public void executeTrade() throws Exception {
130 |
131 | Trade trade = new Trade();
132 | trade.setAction(Trade.TradeAction.Buy);
133 | trade.setTicker("DELL");
134 | trade.setShares(25);
135 |
136 | byte[] payload = new ObjectMapper().writeValueAsBytes(trade);
137 |
138 | StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
139 | headers.setDestination("/app/trade");
140 | headers.setSessionId("0");
141 | headers.setUser(new TestPrincipal("fabrice"));
142 | headers.setSessionAttributes(new HashMap<>());
143 | Message message = MessageBuilder.withPayload(payload).setHeaders(headers).build();
144 |
145 | this.annotationMethodHandler.handleMessage(message);
146 |
147 | assertEquals(1, this.tradeService.getTrades().size());
148 | Trade actual = this.tradeService.getTrades().get(0);
149 |
150 | assertEquals(Trade.TradeAction.Buy, actual.getAction());
151 | assertEquals("DELL", actual.getTicker());
152 | assertEquals(25, actual.getShares());
153 | assertEquals("fabrice", actual.getUsername());
154 | }
155 |
156 |
157 | /**
158 | * An extension of SimpAnnotationMethodMessageHandler that exposes a (public)
159 | * method for manually registering a controller, rather than having it
160 | * auto-discovered in the Spring ApplicationContext.
161 | */
162 | private static class TestAnnotationMethodHandler extends SimpAnnotationMethodMessageHandler {
163 |
164 | public TestAnnotationMethodHandler(SubscribableChannel inChannel, MessageChannel outChannel,
165 | SimpMessageSendingOperations brokerTemplate) {
166 |
167 | super(inChannel, outChannel, brokerTemplate);
168 | }
169 |
170 | public void registerHandler(Object handler) {
171 | super.detectHandlerMethods(handler);
172 | }
173 | }
174 |
175 | }
176 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/load/StompWebSocketLoadTestServer.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2014 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.load;
18 |
19 |
20 | import java.text.DateFormat;
21 | import java.text.SimpleDateFormat;
22 | import java.util.Date;
23 | import java.util.HashMap;
24 | import java.util.List;
25 | import java.util.Map;
26 | import java.util.concurrent.atomic.AtomicInteger;
27 |
28 | import org.springframework.context.annotation.Bean;
29 | import org.springframework.context.annotation.Configuration;
30 | import org.springframework.core.task.TaskExecutor;
31 | import org.springframework.messaging.Message;
32 | import org.springframework.messaging.converter.DefaultContentTypeResolver;
33 | import org.springframework.messaging.converter.MessageConverter;
34 | import org.springframework.messaging.converter.StringMessageConverter;
35 | import org.springframework.messaging.handler.annotation.MessageMapping;
36 | import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler;
37 | import org.springframework.messaging.simp.config.ChannelRegistration;
38 | import org.springframework.messaging.simp.config.MessageBrokerRegistry;
39 | import org.springframework.messaging.simp.stomp.StompCommand;
40 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
41 | import org.springframework.samples.portfolio.web.support.JettyWebSocketTestServer;
42 | import org.springframework.samples.portfolio.web.support.TomcatWebSocketTestServer;
43 | import org.springframework.samples.portfolio.web.support.WebSocketTestServer;
44 | import org.springframework.scheduling.TaskScheduler;
45 | import org.springframework.scheduling.annotation.EnableScheduling;
46 | import org.springframework.util.MimeTypeUtils;
47 | import org.springframework.util.SocketUtils;
48 | import org.springframework.web.bind.annotation.RequestMapping;
49 | import org.springframework.web.bind.annotation.RequestMethod;
50 | import org.springframework.web.bind.annotation.RestController;
51 | import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
52 | import org.springframework.web.servlet.config.annotation.EnableWebMvc;
53 | import org.springframework.web.socket.WebSocketHandler;
54 | import org.springframework.web.socket.config.WebSocketMessageBrokerStats;
55 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
56 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurationSupport;
57 | import org.springframework.web.socket.server.jetty.JettyRequestUpgradeStrategy;
58 | import org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy;
59 | import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
60 |
61 |
62 | public class StompWebSocketLoadTestServer {
63 |
64 | // When false, Tomcat is used
65 | public static final boolean USE_JETTY = false;
66 |
67 | private static final StringMessageConverter MESSAGE_CONVERTER;
68 |
69 | static {
70 | DefaultContentTypeResolver resolver = new DefaultContentTypeResolver();
71 | resolver.setDefaultMimeType(MimeTypeUtils.TEXT_PLAIN);
72 |
73 | MESSAGE_CONVERTER = new StringMessageConverter();
74 | MESSAGE_CONVERTER.setContentTypeResolver(resolver);
75 | }
76 |
77 |
78 | public static void main(String[] args) {
79 |
80 | WebSocketTestServer server = null;
81 |
82 | try {
83 | AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext();
84 | cxt.register(WebSocketConfig.class);
85 |
86 | int port = SocketUtils.findAvailableTcpPort();
87 | if (USE_JETTY) {
88 | server = new JettyWebSocketTestServer(port);
89 | }
90 | else {
91 | System.setProperty("spring.profiles.active", "test.tomcat");
92 | server = new TomcatWebSocketTestServer(port);
93 | }
94 | server.deployDispatcherServlet(cxt);
95 | server.start();
96 |
97 | System.out.println("Running on port " + port);
98 | System.out.println("Press any key to stop");
99 | System.in.read();
100 |
101 | if (server != null) {
102 | try {
103 | server.undeployConfig();
104 | }
105 | catch (Throwable t) {
106 | System.err.println("Failed to undeploy application");
107 | t.printStackTrace();
108 | }
109 |
110 | try {
111 | server.stop();
112 | }
113 | catch (Throwable t) {
114 | System.err.println("Failed to stop server");
115 | t.printStackTrace();
116 | }
117 | }
118 | }
119 | catch (Throwable t) {
120 | t.printStackTrace();
121 | }
122 |
123 | System.exit(0);
124 | }
125 |
126 |
127 | @Configuration
128 | @EnableWebMvc
129 | @EnableScheduling
130 | static class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
131 |
132 | @Override
133 | public void registerStompEndpoints(StompEndpointRegistry registry) {
134 |
135 | // The test classpath includes both Tomcat and Jetty, so let's be explicit
136 | DefaultHandshakeHandler handler = USE_JETTY ?
137 | new DefaultHandshakeHandler(new JettyRequestUpgradeStrategy()) :
138 | new DefaultHandshakeHandler(new TomcatRequestUpgradeStrategy());
139 |
140 | registry.addEndpoint("/stomp").setHandshakeHandler(handler).withSockJS()
141 | .setStreamBytesLimit(512 * 1024)
142 | .setHttpMessageCacheSize(1000)
143 | .setDisconnectDelay(30 * 1000);
144 | }
145 |
146 | @Override
147 | public void configureMessageBroker(MessageBrokerRegistry registry) {
148 | registry.enableStompBrokerRelay("/topic/");
149 | registry.setApplicationDestinationPrefixes("/app");
150 | }
151 |
152 | @Override
153 | public void configureClientOutboundChannel(ChannelRegistration registration) {
154 | registration.taskExecutor().corePoolSize(50);
155 | }
156 |
157 | @Override
158 | public boolean configureMessageConverters(List messageConverters) {
159 | messageConverters.add(MESSAGE_CONVERTER);
160 | return false;
161 | }
162 |
163 | @Bean
164 | public HomeController homeController() {
165 | return new HomeController();
166 | }
167 |
168 | @Override
169 | public WebSocketMessageBrokerStats webSocketMessageBrokerStats(
170 | AbstractBrokerMessageHandler relayHandler, WebSocketHandler webSocketHandler,
171 | TaskExecutor inboundExecutor, TaskExecutor outboundExecutor, TaskScheduler taskScheduler) {
172 |
173 | WebSocketMessageBrokerStats stats = super.webSocketMessageBrokerStats(
174 | relayHandler, webSocketHandler, inboundExecutor, outboundExecutor, taskScheduler);
175 |
176 | stats.setLoggingPeriod(5 * 1000);
177 | return stats;
178 | }
179 | }
180 |
181 | @RestController
182 | static class HomeController {
183 |
184 | public static final DateFormat DATE_FORMAT = SimpleDateFormat.getDateInstance();
185 |
186 |
187 | @RequestMapping(value="/home", method = RequestMethod.GET)
188 | public void home() {
189 | }
190 |
191 | @MessageMapping("/greeting")
192 | public String handleGreeting(String greeting) {
193 | return "[" + DATE_FORMAT.format(new Date()) + "] " + greeting;
194 | }
195 | }
196 |
197 | private static class StompMessageCounter {
198 |
199 | private static Map counters = new HashMap();
200 |
201 | public StompMessageCounter() {
202 | for (StompCommand command : StompCommand.values()) {
203 | this.counters.put(command, new AtomicInteger(0));
204 | }
205 | }
206 |
207 | public void handleMessage(Message> message) {
208 | StompHeaderAccessor headers =StompHeaderAccessor.wrap(message);
209 | AtomicInteger counter = this.counters.get(headers.getCommand());
210 | counter.incrementAndGet();
211 | }
212 |
213 | public String toString() {
214 | StringBuilder sb = new StringBuilder();
215 | for (StompCommand command : StompCommand.values()) {
216 | AtomicInteger counter = this.counters.get(command);
217 | if (counter.get() > 0) {
218 | sb.append("(").append(command.name()).append(": ").append(counter.get()).append(") ");
219 | }
220 | }
221 | return sb.toString();
222 | }
223 | }
224 |
225 | }
226 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/context/ContextPortfolioControllerTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2014 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.context;
18 |
19 | import static org.junit.Assert.*;
20 |
21 | import java.nio.charset.Charset;
22 | import java.util.HashMap;
23 | import java.util.List;
24 |
25 | import com.fasterxml.jackson.databind.ObjectMapper;
26 | import org.junit.Before;
27 | import org.junit.Test;
28 | import org.junit.runner.RunWith;
29 |
30 | import org.springframework.beans.factory.annotation.Autowired;
31 | import org.springframework.context.ApplicationListener;
32 | import org.springframework.context.annotation.ComponentScan;
33 | import org.springframework.context.annotation.Configuration;
34 | import org.springframework.context.annotation.FilterType;
35 | import org.springframework.context.event.ContextRefreshedEvent;
36 | import org.springframework.core.env.Environment;
37 | import org.springframework.messaging.Message;
38 | import org.springframework.messaging.MessageHandler;
39 | import org.springframework.messaging.SubscribableChannel;
40 | import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
41 | import org.springframework.messaging.simp.config.MessageBrokerRegistry;
42 | import org.springframework.messaging.simp.stomp.StompCommand;
43 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
44 | import org.springframework.messaging.support.AbstractSubscribableChannel;
45 | import org.springframework.messaging.support.MessageBuilder;
46 | import org.springframework.samples.portfolio.service.Trade;
47 | import org.springframework.samples.portfolio.web.support.TestPrincipal;
48 | import org.springframework.scheduling.annotation.EnableScheduling;
49 | import org.springframework.test.context.ContextConfiguration;
50 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
51 | import org.springframework.test.util.JsonPathExpectationsHelper;
52 | import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
53 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
54 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
55 |
56 |
57 | /**
58 | * Tests for PortfolioController that rely on the Spring TestContext framework to
59 | * load the actual Spring configuration. The test strategy here is to test the
60 | * behavior of controllers using the actual Spring configuration while using
61 | * the TestContext framework ensures that Spring configuration is loaded only
62 | * once per test class.
63 | *
64 | *
The test manually creates messages representing STOMP frames and sends them
65 | * to the "clientInboundChannel" simulating clients by setting the session id and
66 | * user headers of the message accordingly.
67 | *
68 | *
Test ChannelInterceptor implementations are installed on the "brokerChannel"
69 | * and the "clientOutboundChannel" in order to capture messages sent through
70 | * them. Although not the case here, often a controller method will
71 | * not send any messages at all. In such cases it might be necessary to inject
72 | * the controller with "mock" services in order to verify message handling.
73 | *
74 | *
Note the (optional) use of TestConfig, which removes MessageHandler
75 | * subscriptions to message channels except the handler that delegates to
76 | * annotated controller methods. This allows focusing on controllers.
77 | *
78 | * @author Rossen Stoyanchev
79 | */
80 |
81 | @RunWith(SpringJUnit4ClassRunner.class)
82 | @ContextConfiguration(classes = {
83 | ContextPortfolioControllerTests.TestWebSocketConfig.class,
84 | ContextPortfolioControllerTests.TestConfig.class
85 | })
86 | public class ContextPortfolioControllerTests {
87 |
88 | @Autowired private AbstractSubscribableChannel clientInboundChannel;
89 |
90 | @Autowired private AbstractSubscribableChannel clientOutboundChannel;
91 |
92 | @Autowired private AbstractSubscribableChannel brokerChannel;
93 |
94 | private TestChannelInterceptor clientOutboundChannelInterceptor;
95 |
96 | private TestChannelInterceptor brokerChannelInterceptor;
97 |
98 |
99 | @Before
100 | public void setUp() throws Exception {
101 |
102 | this.brokerChannelInterceptor = new TestChannelInterceptor();
103 | this.clientOutboundChannelInterceptor = new TestChannelInterceptor();
104 |
105 | this.brokerChannel.addInterceptor(this.brokerChannelInterceptor);
106 | this.clientOutboundChannel.addInterceptor(this.clientOutboundChannelInterceptor);
107 | }
108 |
109 |
110 | @Test
111 | public void getPositions() throws Exception {
112 |
113 | StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SUBSCRIBE);
114 | headers.setSubscriptionId("0");
115 | headers.setDestination("/app/positions");
116 | headers.setSessionId("0");
117 | headers.setUser(new TestPrincipal("fabrice"));
118 | headers.setSessionAttributes(new HashMap<>());
119 | Message message = MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
120 |
121 | this.clientOutboundChannelInterceptor.setIncludedDestinations("/app/positions");
122 | this.clientInboundChannel.send(message);
123 |
124 | Message> reply = this.clientOutboundChannelInterceptor.awaitMessage(5);
125 | assertNotNull(reply);
126 |
127 | StompHeaderAccessor replyHeaders = StompHeaderAccessor.wrap(reply);
128 | assertEquals("0", replyHeaders.getSessionId());
129 | assertEquals("0", replyHeaders.getSubscriptionId());
130 | assertEquals("/app/positions", replyHeaders.getDestination());
131 |
132 | String json = new String((byte[]) reply.getPayload(), Charset.forName("UTF-8"));
133 | new JsonPathExpectationsHelper("$[0].company").assertValue(json, "Citrix Systems, Inc.");
134 | new JsonPathExpectationsHelper("$[1].company").assertValue(json, "Dell Inc.");
135 | new JsonPathExpectationsHelper("$[2].company").assertValue(json, "Microsoft");
136 | new JsonPathExpectationsHelper("$[3].company").assertValue(json, "Oracle");
137 | }
138 |
139 | @Test
140 | public void executeTrade() throws Exception {
141 |
142 | Trade trade = new Trade();
143 | trade.setAction(Trade.TradeAction.Buy);
144 | trade.setTicker("DELL");
145 | trade.setShares(25);
146 |
147 | byte[] payload = new ObjectMapper().writeValueAsBytes(trade);
148 |
149 | StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.SEND);
150 | headers.setDestination("/app/trade");
151 | headers.setSessionId("0");
152 | headers.setUser(new TestPrincipal("fabrice"));
153 | headers.setSessionAttributes(new HashMap<>());
154 | Message message = MessageBuilder.createMessage(payload, headers.getMessageHeaders());
155 |
156 | this.brokerChannelInterceptor.setIncludedDestinations("/user/**");
157 | this.clientInboundChannel.send(message);
158 |
159 | Message> positionUpdate = this.brokerChannelInterceptor.awaitMessage(5);
160 | assertNotNull(positionUpdate);
161 |
162 | StompHeaderAccessor positionUpdateHeaders = StompHeaderAccessor.wrap(positionUpdate);
163 | assertEquals("/user/fabrice/queue/position-updates", positionUpdateHeaders.getDestination());
164 |
165 | String json = new String((byte[]) positionUpdate.getPayload(), Charset.forName("UTF-8"));
166 | new JsonPathExpectationsHelper("$.ticker").assertValue(json, "DELL");
167 | new JsonPathExpectationsHelper("$.shares").assertValue(json, 75);
168 | }
169 |
170 | @Configuration
171 | @EnableScheduling
172 | @ComponentScan(
173 | basePackages="org.springframework.samples",
174 | excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, value = Configuration.class)
175 | )
176 | @EnableWebSocketMessageBroker
177 | static class TestWebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
178 |
179 | @Autowired
180 | Environment env;
181 |
182 | @Override
183 | public void registerStompEndpoints(StompEndpointRegistry registry) {
184 | registry.addEndpoint("/portfolio").withSockJS();
185 | }
186 |
187 | @Override
188 | public void configureMessageBroker(MessageBrokerRegistry registry) {
189 | // registry.enableSimpleBroker("/queue/", "/topic/");
190 | registry.enableStompBrokerRelay("/queue/", "/topic/");
191 | registry.setApplicationDestinationPrefixes("/app");
192 | }
193 | }
194 |
195 | /**
196 | * Configuration class that un-registers MessageHandler's it finds in the
197 | * ApplicationContext from the message channels they are subscribed to...
198 | * except the message handler used to invoke annotated message handling methods.
199 | * The intent is to reduce additional processing and additional messages not
200 | * related to the test.
201 | */
202 | @Configuration
203 | @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
204 | static class TestConfig implements ApplicationListener {
205 |
206 | @Autowired
207 | private List channels;
208 |
209 | @Autowired
210 | private List handlers;
211 |
212 |
213 | @Override
214 | public void onApplicationEvent(ContextRefreshedEvent event) {
215 | for (MessageHandler handler : handlers) {
216 | if (handler instanceof SimpAnnotationMethodMessageHandler) {
217 | continue;
218 | }
219 | for (SubscribableChannel channel :channels) {
220 | channel.unsubscribe(handler);
221 | }
222 | }
223 | }
224 | }
225 | }
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 | org.springframework.samples
5 | spring-websocket-portfolio
6 | war
7 | 1.0.0-SNAPSHOT
8 |
9 |
10 |
11 |
12 | org.springframework
13 | spring-framework-bom
14 | pom
15 | import
16 | 5.3.22
17 |
18 |
19 | org.springframework.security
20 | spring-security-bom
21 | pom
22 | import
23 | 5.6.6
24 |
25 |
26 | io.projectreactor
27 | reactor-bom
28 | pom
29 | import
30 | 2020.0.21
31 |
32 |
33 | com.fasterxml.jackson
34 | jackson-bom
35 | pom
36 | import
37 | 2.12.7
38 |
39 |
40 | org.apache.logging.log4j
41 | log4j-bom
42 | pom
43 | import
44 | 2.18.0
45 |
46 |
47 |
48 |
49 |
50 |
51 | org.springframework
52 | spring-context
53 |
54 |
55 | org.springframework
56 | spring-messaging
57 |
58 |
59 | org.springframework
60 | spring-web
61 |
62 |
63 | org.springframework
64 | spring-webmvc
65 |
66 |
67 | org.springframework
68 | spring-websocket
69 |
70 |
71 |
72 | javax.servlet
73 | javax.servlet-api
74 | 4.0.1
75 | provided
76 |
77 |
78 |
79 |
80 | io.projectreactor.netty
81 | reactor-netty
82 |
83 |
84 |
85 | org.apache.logging.log4j
86 | log4j-api
87 |
88 |
89 | org.apache.logging.log4j
90 | log4j-core
91 |
92 |
93 | org.apache.logging.log4j
94 | log4j-slf4j-impl
95 |
96 |
97 | org.apache.logging.log4j
98 | log4j-jul
99 |
100 |
101 |
102 | org.springframework.security
103 | spring-security-core
104 |
105 |
106 | org.springframework.security
107 | spring-security-config
108 |
109 |
110 | org.springframework.security
111 | spring-security-web
112 |
113 |
114 | org.springframework
115 | spring-jdbc
116 |
117 |
118 |
119 |
120 | org.springframework.security
121 | spring-security-messaging
122 |
123 |
124 |
125 |
126 | org.webjars
127 | webjars-locator
128 | 0.45
129 |
130 |
137 |
138 | org.webjars
139 | bootstrap
140 | 3.4.1
141 |
142 |
143 | org.webjars
144 | flat-ui
145 | bcaf2de95e
146 |
147 |
148 | org.webjars
149 | sockjs-client
150 | 1.5.1
151 |
152 |
153 | org.webjars.npm
154 | webstomp-client
155 | 1.2.6
156 |
157 |
158 | org.webjars.bower
159 | angular
160 | 1.7.9
161 |
162 |
163 | org.webjars.bower
164 | angular-ui-bootstrap-bower
165 | 2.5.0
166 |
167 |
168 |
169 | junit
170 | junit
171 | 4.13.2
172 | test
173 |
174 |
175 | org.springframework
176 | spring-test
177 | test
178 |
179 |
180 | javax.websocket
181 | javax.websocket-api
182 | 1.1
183 | test
184 |
185 |
186 |
187 | org.apache.tomcat.embed
188 | tomcat-embed-core
189 | 9.0.65
190 | test
191 |
192 |
193 | org.apache.tomcat.embed
194 | tomcat-embed-websocket
195 | 9.0.65
196 | test
197 |
198 |
199 |
200 | org.eclipse.jetty.websocket
201 | websocket-client
202 | 9.4.48.v20220622
203 | test
204 |
205 |
206 | org.eclipse.jetty.websocket
207 | websocket-server
208 | 9.4.48.v20220622
209 | test
210 |
211 |
212 | org.eclipse.jetty
213 | jetty-webapp
214 | 9.4.48.v20220622
215 | test
216 |
217 |
218 | org.eclipse.jetty
219 | jetty-client
220 | 9.4.48.v20220622
221 | test
222 |
223 |
224 |
225 | org.apache.httpcomponents
226 | httpclient
227 | 4.5.13
228 | test
229 |
230 |
231 |
232 | com.jayway.jsonpath
233 | json-path
234 | 2.6.0
235 | test
236 |
237 |
238 |
239 |
240 |
241 |
242 | java-net
243 | https://maven.java.net/content/repositories/releases
244 |
245 |
246 |
247 |
248 | ${project.artifactId}
249 |
250 |
251 | org.apache.maven.plugins
252 | maven-compiler-plugin
253 |
254 | 1.8
255 | 1.8
256 |
257 |
258 |
259 | org.apache.maven.plugins
260 | maven-war-plugin
261 |
262 | false
263 |
264 |
265 |
266 | org.apache.maven.plugins
267 | maven-resources-plugin
268 | 3.2.0
269 |
270 | UTF-8
271 |
272 |
273 |
274 | org.apache.maven.plugins
275 | maven-surefire-plugin
276 |
277 |
278 | **/*Tests.java
279 |
280 |
281 | **/Abstract*.java
282 |
283 | junit:junit
284 | -Xmx512m
285 |
286 |
287 |
288 | org.apache.maven.plugins
289 | maven-eclipse-plugin
290 |
291 | true
292 | false
293 | 2.0
294 |
295 | 2.10
296 |
297 |
298 | org.eclipse.jetty
299 | jetty-maven-plugin
300 | 9.4.48.v20220622
301 |
302 |
303 | /${project.artifactId}
304 |
305 |
306 |
307 |
308 |
309 |
310 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | https://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "{}"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright {yyyy} {name of copyright owner}
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | https://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/load/StompWebSocketLoadTestClient.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2014 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.load;
18 |
19 |
20 | import static org.junit.Assert.*;
21 |
22 | import java.lang.reflect.Type;
23 | import java.util.ArrayList;
24 | import java.util.List;
25 | import java.util.concurrent.CountDownLatch;
26 | import java.util.concurrent.TimeUnit;
27 | import java.util.concurrent.atomic.AtomicInteger;
28 | import java.util.concurrent.atomic.AtomicReference;
29 |
30 | import org.apache.commons.logging.Log;
31 | import org.apache.commons.logging.LogFactory;
32 | import org.eclipse.jetty.client.HttpClient;
33 | import org.eclipse.jetty.util.thread.QueuedThreadPool;
34 |
35 | import org.springframework.http.HttpStatus;
36 | import org.springframework.messaging.converter.StringMessageConverter;
37 | import org.springframework.messaging.simp.stomp.ConnectionLostException;
38 | import org.springframework.messaging.simp.stomp.StompCommand;
39 | import org.springframework.messaging.simp.stomp.StompFrameHandler;
40 | import org.springframework.messaging.simp.stomp.StompHeaders;
41 | import org.springframework.messaging.simp.stomp.StompSession;
42 | import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
43 | import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
44 | import org.springframework.util.Assert;
45 | import org.springframework.util.StopWatch;
46 | import org.springframework.web.client.RestTemplate;
47 | import org.springframework.web.socket.client.standard.StandardWebSocketClient;
48 | import org.springframework.web.socket.messaging.WebSocketStompClient;
49 | import org.springframework.web.socket.sockjs.client.JettyXhrTransport;
50 | import org.springframework.web.socket.sockjs.client.SockJsClient;
51 | import org.springframework.web.socket.sockjs.client.Transport;
52 | import org.springframework.web.socket.sockjs.client.WebSocketTransport;
53 |
54 |
55 | public class StompWebSocketLoadTestClient {
56 |
57 | private static Log logger = LogFactory.getLog(StompWebSocketLoadTestClient.class);
58 |
59 | private static final int NUMBER_OF_USERS = 200;
60 |
61 | private static final int BROADCAST_MESSAGE_COUNT = 2000;
62 |
63 |
64 | public static void main(String[] args) throws Exception {
65 |
66 | // Modify host and port below to match wherever StompWebSocketServer.java is running!!
67 | // When StompWebSocketServer starts it prints the selected available
68 |
69 | String host = "localhost";
70 | if (args.length > 0) {
71 | host = args[0];
72 | }
73 |
74 | int port = 37232;
75 | if (args.length > 1) {
76 | port = Integer.valueOf(args[1]);
77 | }
78 |
79 | String homeUrl = "http://{host}:{port}/home";
80 | logger.debug("Sending warm-up HTTP request to " + homeUrl);
81 | HttpStatus status = new RestTemplate().getForEntity(homeUrl, Void.class, host, port).getStatusCode();
82 | Assert.state(status == HttpStatus.OK);
83 |
84 | final CountDownLatch connectLatch = new CountDownLatch(NUMBER_OF_USERS);
85 | final CountDownLatch subscribeLatch = new CountDownLatch(NUMBER_OF_USERS);
86 | final CountDownLatch messageLatch = new CountDownLatch(NUMBER_OF_USERS);
87 | final CountDownLatch disconnectLatch = new CountDownLatch(NUMBER_OF_USERS);
88 |
89 | final AtomicReference failure = new AtomicReference<>();
90 |
91 | StandardWebSocketClient webSocketClient = new StandardWebSocketClient();
92 |
93 | HttpClient jettyHttpClient = new HttpClient();
94 | jettyHttpClient.setMaxConnectionsPerDestination(1000);
95 | jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
96 | jettyHttpClient.start();
97 |
98 | List transports = new ArrayList<>();
99 | transports.add(new WebSocketTransport(webSocketClient));
100 | transports.add(new JettyXhrTransport(jettyHttpClient));
101 |
102 | SockJsClient sockJsClient = new SockJsClient(transports);
103 |
104 | try {
105 | ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
106 | taskScheduler.afterPropertiesSet();
107 |
108 | String stompUrl = "ws://{host}:{port}/stomp";
109 | WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient);
110 | stompClient.setMessageConverter(new StringMessageConverter());
111 | stompClient.setTaskScheduler(taskScheduler);
112 | stompClient.setDefaultHeartbeat(new long[] {0, 0});
113 |
114 | logger.debug("Connecting and subscribing " + NUMBER_OF_USERS + " users ");
115 | StopWatch stopWatch = new StopWatch("STOMP Broker Relay WebSocket Load Tests");
116 | stopWatch.start();
117 |
118 | List consumers = new ArrayList<>();
119 | for (int i=0; i < NUMBER_OF_USERS; i++) {
120 | consumers.add(new ConsumerStompSessionHandler(BROADCAST_MESSAGE_COUNT, connectLatch,
121 | subscribeLatch, messageLatch, disconnectLatch, failure));
122 | stompClient.connect(stompUrl, consumers.get(i), host, port);
123 | }
124 |
125 | if (failure.get() != null) {
126 | throw new AssertionError("Test failed", failure.get());
127 | }
128 | if (!connectLatch.await(5000, TimeUnit.MILLISECONDS)) {
129 | fail("Not all users connected, remaining: " + connectLatch.getCount());
130 | }
131 | if (!subscribeLatch.await(5000, TimeUnit.MILLISECONDS)) {
132 | fail("Not all users subscribed, remaining: " + subscribeLatch.getCount());
133 | }
134 |
135 | stopWatch.stop();
136 | logger.debug("Finished: " + stopWatch.getLastTaskTimeMillis() + " millis");
137 |
138 | logger.debug("Broadcasting " + BROADCAST_MESSAGE_COUNT + " messages to " + NUMBER_OF_USERS + " users ");
139 | stopWatch.start();
140 |
141 | ProducerStompSessionHandler producer = new ProducerStompSessionHandler(BROADCAST_MESSAGE_COUNT, failure);
142 | stompClient.connect(stompUrl, producer, host, port);
143 | stompClient.setTaskScheduler(taskScheduler);
144 |
145 | if (failure.get() != null) {
146 | throw new AssertionError("Test failed", failure.get());
147 | }
148 | if (!messageLatch.await(60 * 1000, TimeUnit.MILLISECONDS)) {
149 | for (ConsumerStompSessionHandler consumer : consumers) {
150 | if (consumer.messageCount.get() < consumer.expectedMessageCount) {
151 | logger.debug(consumer);
152 | }
153 | }
154 | }
155 | if (!messageLatch.await(60 * 1000, TimeUnit.MILLISECONDS)) {
156 | fail("Not all handlers received every message, remaining: " + messageLatch.getCount());
157 | }
158 |
159 | producer.session.disconnect();
160 | if (!disconnectLatch.await(5000, TimeUnit.MILLISECONDS)) {
161 | fail("Not all disconnects completed, remaining: " + disconnectLatch.getCount());
162 | }
163 |
164 | stopWatch.stop();
165 | logger.debug("Finished: " + stopWatch.getLastTaskTimeMillis() + " millis");
166 |
167 | System.out.println("\nPress any key to exit...");
168 | System.in.read();
169 | }
170 | catch (Throwable t) {
171 | t.printStackTrace();
172 | }
173 | finally {
174 | jettyHttpClient.stop();
175 | }
176 |
177 | logger.debug("Exiting");
178 | System.exit(0);
179 | }
180 |
181 |
182 | private static class ConsumerStompSessionHandler extends StompSessionHandlerAdapter {
183 |
184 | private final int expectedMessageCount;
185 |
186 | private final CountDownLatch connectLatch;
187 |
188 | private final CountDownLatch subscribeLatch;
189 |
190 | private final CountDownLatch messageLatch;
191 |
192 | private final CountDownLatch disconnectLatch;
193 |
194 | private final AtomicReference failure;
195 |
196 | private AtomicInteger messageCount = new AtomicInteger(0);
197 |
198 |
199 | public ConsumerStompSessionHandler(int expectedMessageCount, CountDownLatch connectLatch,
200 | CountDownLatch subscribeLatch, CountDownLatch messageLatch, CountDownLatch disconnectLatch,
201 | AtomicReference failure) {
202 |
203 | this.expectedMessageCount = expectedMessageCount;
204 | this.connectLatch = connectLatch;
205 | this.subscribeLatch = subscribeLatch;
206 | this.messageLatch = messageLatch;
207 | this.disconnectLatch = disconnectLatch;
208 | this.failure = failure;
209 | }
210 |
211 | @Override
212 | public void afterConnected(final StompSession session, StompHeaders connectedHeaders) {
213 | this.connectLatch.countDown();
214 | session.setAutoReceipt(true);
215 | session.subscribe("/topic/greeting", new StompFrameHandler() {
216 | @Override
217 | public Type getPayloadType(StompHeaders headers) {
218 | return String.class;
219 | }
220 |
221 | @Override
222 | public void handleFrame(StompHeaders headers, Object payload) {
223 | if (messageCount.incrementAndGet() == expectedMessageCount) {
224 | messageLatch.countDown();
225 | disconnectLatch.countDown();
226 | session.disconnect();
227 | }
228 | }
229 | }).addReceiptTask(new Runnable() {
230 | @Override
231 | public void run() {
232 | subscribeLatch.countDown();
233 | }
234 | });
235 | }
236 |
237 | @Override
238 | public void handleTransportError(StompSession session, Throwable exception) {
239 | logger.error("Transport error", exception);
240 | this.failure.set(exception);
241 | if (exception instanceof ConnectionLostException) {
242 | this.disconnectLatch.countDown();
243 | }
244 | }
245 |
246 | @Override
247 | public void handleException(StompSession s, StompCommand c, StompHeaders h, byte[] p, Throwable ex) {
248 | logger.error("Handling exception", ex);
249 | this.failure.set(ex);
250 | }
251 |
252 | @Override
253 | public void handleFrame(StompHeaders headers, Object payload) {
254 | Exception ex = new Exception(headers.toString());
255 | logger.error("STOMP ERROR frame", ex);
256 | this.failure.set(ex);
257 | }
258 |
259 | @Override
260 | public String toString() {
261 | return "ConsumerStompSessionHandler[messageCount=" + this.messageCount + "]";
262 | }
263 | }
264 |
265 | private static class ProducerStompSessionHandler extends StompSessionHandlerAdapter {
266 |
267 | private final int numberOfMessagesToBroadcast;
268 |
269 | private final AtomicReference failure;
270 |
271 | private StompSession session;
272 |
273 |
274 | public ProducerStompSessionHandler(int numberOfMessagesToBroadcast, AtomicReference failure) {
275 | this.numberOfMessagesToBroadcast = numberOfMessagesToBroadcast;
276 | this.failure = failure;
277 | }
278 |
279 | @Override
280 | public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
281 | this.session = session;
282 | int i =0;
283 | try {
284 | for ( ; i < this.numberOfMessagesToBroadcast; i++) {
285 | session.send("/app/greeting", "hello");
286 | }
287 | }
288 | catch (Throwable t) {
289 | logger.error("Message sending failed at " + i, t);
290 | failure.set(t);
291 | }
292 | }
293 |
294 | @Override
295 | public void handleTransportError(StompSession session, Throwable exception) {
296 | logger.error("Transport error", exception);
297 | this.failure.set(exception);
298 | }
299 |
300 | @Override
301 | public void handleException(StompSession s, StompCommand c, StompHeaders h, byte[] p, Throwable ex) {
302 | logger.error("Handling exception", ex);
303 | this.failure.set(ex);
304 | }
305 |
306 | @Override
307 | public void handleFrame(StompHeaders headers, Object payload) {
308 | Exception ex = new Exception(headers.toString());
309 | logger.error("STOMP ERROR frame", ex);
310 | this.failure.set(ex);
311 | }
312 | }
313 |
314 | }
315 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/tomcat/IntegrationPortfolioTests.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2015 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.tomcat;
18 |
19 | import java.lang.reflect.Type;
20 | import java.util.ArrayList;
21 | import java.util.List;
22 | import java.util.concurrent.CountDownLatch;
23 | import java.util.concurrent.TimeUnit;
24 | import java.util.concurrent.atomic.AtomicReference;
25 |
26 | import org.apache.commons.logging.Log;
27 | import org.apache.commons.logging.LogFactory;
28 | import org.junit.AfterClass;
29 | import org.junit.BeforeClass;
30 | import org.junit.Test;
31 |
32 | import org.springframework.beans.factory.annotation.Autowired;
33 | import org.springframework.context.annotation.ComponentScan;
34 | import org.springframework.context.annotation.Configuration;
35 | import org.springframework.context.annotation.FilterType;
36 | import org.springframework.core.env.Environment;
37 | import org.springframework.http.HttpHeaders;
38 | import org.springframework.http.HttpMethod;
39 | import org.springframework.http.MediaType;
40 | import org.springframework.http.converter.FormHttpMessageConverter;
41 | import org.springframework.messaging.converter.MappingJackson2MessageConverter;
42 | import org.springframework.messaging.simp.config.MessageBrokerRegistry;
43 | import org.springframework.messaging.simp.stomp.StompCommand;
44 | import org.springframework.messaging.simp.stomp.StompFrameHandler;
45 | import org.springframework.messaging.simp.stomp.StompHeaders;
46 | import org.springframework.messaging.simp.stomp.StompSession;
47 | import org.springframework.messaging.simp.stomp.StompSessionHandler;
48 | import org.springframework.messaging.simp.stomp.StompSessionHandlerAdapter;
49 | import org.springframework.samples.portfolio.PortfolioPosition;
50 | import org.springframework.samples.portfolio.config.DispatcherServletInitializer;
51 | import org.springframework.samples.portfolio.config.WebConfig;
52 | import org.springframework.samples.portfolio.config.WebSecurityInitializer;
53 | import org.springframework.samples.portfolio.service.Trade;
54 | import org.springframework.samples.portfolio.web.support.TomcatWebSocketTestServer;
55 | import org.springframework.scheduling.annotation.EnableScheduling;
56 | import org.springframework.test.util.JsonPathExpectationsHelper;
57 | import org.springframework.util.LinkedMultiValueMap;
58 | import org.springframework.util.MultiValueMap;
59 | import org.springframework.util.SocketUtils;
60 | import org.springframework.web.client.RestTemplate;
61 | import org.springframework.web.socket.WebSocketHttpHeaders;
62 | import org.springframework.web.socket.client.standard.StandardWebSocketClient;
63 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
64 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
65 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
66 | import org.springframework.web.socket.messaging.WebSocketStompClient;
67 | import org.springframework.web.socket.server.standard.TomcatRequestUpgradeStrategy;
68 | import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
69 | import org.springframework.web.socket.sockjs.client.RestTemplateXhrTransport;
70 | import org.springframework.web.socket.sockjs.client.SockJsClient;
71 | import org.springframework.web.socket.sockjs.client.Transport;
72 | import org.springframework.web.socket.sockjs.client.WebSocketTransport;
73 |
74 | import static org.junit.Assert.*;
75 |
76 | /**
77 | * End-to-end integration tests that run an embedded Tomcat server and establish
78 | * an actual WebSocket session using
79 | * {@link org.springframework.web.socket.client.standard.StandardWebSocketClient}.
80 | * as well as a simple STOMP/WebSocket client created to support these tests.
81 | *
82 | * The test strategy here is to test from the perspective of a client connecting
83 | * to a server and therefore it is a much more complete test. However, writing
84 | * and maintaining these tests is a bit more involved.
85 | *
86 | * An all-encapsulating strategy might be to write the majority of tests using
87 | * server-side testing (either standalone or with Spring configuration) with
88 | * end-to-end integration tests serving as a higher-level verification but
89 | * overall fewer in number.
90 | *
91 | * @author Rossen Stoyanchev
92 | */
93 | public class IntegrationPortfolioTests {
94 |
95 | private static Log logger = LogFactory.getLog(IntegrationPortfolioTests.class);
96 |
97 | private static int port;
98 |
99 | private static TomcatWebSocketTestServer server;
100 |
101 | private static SockJsClient sockJsClient;
102 |
103 | private final static WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
104 |
105 |
106 | @BeforeClass
107 | public static void setup() throws Exception {
108 |
109 | // Since test classpath includes both embedded Tomcat and Jetty we need to
110 | // set a Spring profile explicitly to bypass WebSocket engine detection.
111 | // See {@link org.springframework.samples.portfolio.config.WebSocketConfig}
112 |
113 | // This test is not supported with Jetty because it doesn't seem to support
114 | // deployment withspecific ServletContainerInitializer's at for testing
115 |
116 | System.setProperty("spring.profiles.active", "test.tomcat");
117 |
118 | port = SocketUtils.findAvailableTcpPort();
119 |
120 | server = new TomcatWebSocketTestServer(port);
121 | server.deployWithInitializer(TestDispatcherServletInitializer.class, WebSecurityInitializer.class);
122 | server.start();
123 |
124 | loginAndSaveJsessionIdCookie("fabrice", "fab123", headers);
125 |
126 | List transports = new ArrayList<>();
127 | transports.add(new WebSocketTransport(new StandardWebSocketClient()));
128 | RestTemplateXhrTransport xhrTransport = new RestTemplateXhrTransport(new RestTemplate());
129 | transports.add(xhrTransport);
130 |
131 | sockJsClient = new SockJsClient(transports);
132 | }
133 |
134 | private static void loginAndSaveJsessionIdCookie(final String user, final String password,
135 | final HttpHeaders headersToUpdate) {
136 |
137 | String url = "http://localhost:" + port + "/login.html";
138 |
139 | new RestTemplate().execute(url, HttpMethod.POST,
140 |
141 | request -> {
142 | MultiValueMap map = new LinkedMultiValueMap<>();
143 | map.add("username", user);
144 | map.add("password", password);
145 | new FormHttpMessageConverter().write(map, MediaType.APPLICATION_FORM_URLENCODED, request);
146 | },
147 |
148 | response -> {
149 | headersToUpdate.add("Cookie", response.getHeaders().getFirst("Set-Cookie"));
150 | return null;
151 | });
152 | }
153 |
154 | @AfterClass
155 | public static void teardown() {
156 | if (server != null) {
157 | try {
158 | server.undeployConfig();
159 | }
160 | catch (Throwable t) {
161 | logger.error("Failed to undeploy application", t);
162 | }
163 |
164 | try {
165 | server.stop();
166 | }
167 | catch (Throwable t) {
168 | logger.error("Failed to stop server", t);
169 | }
170 | }
171 | }
172 |
173 |
174 | @Test
175 | public void getPositions() throws Exception {
176 |
177 | final CountDownLatch latch = new CountDownLatch(1);
178 | final AtomicReference failure = new AtomicReference<>();
179 |
180 | StompSessionHandler handler = new AbstractTestSessionHandler(failure) {
181 |
182 | @Override
183 | public void afterConnected(final StompSession session, StompHeaders connectedHeaders) {
184 | session.subscribe("/app/positions", new StompFrameHandler() {
185 | @Override
186 | public Type getPayloadType(StompHeaders headers) {
187 | return byte[].class;
188 | }
189 |
190 | @Override
191 | public void handleFrame(StompHeaders headers, Object payload) {
192 | String json = new String((byte[]) payload);
193 | logger.debug("Got " + json);
194 | try {
195 | new JsonPathExpectationsHelper("$[0].company").assertValue(json, "Citrix Systems, Inc.");
196 | new JsonPathExpectationsHelper("$[1].company").assertValue(json, "Dell Inc.");
197 | new JsonPathExpectationsHelper("$[2].company").assertValue(json, "Microsoft");
198 | new JsonPathExpectationsHelper("$[3].company").assertValue(json, "Oracle");
199 | }
200 | catch (Throwable t) {
201 | failure.set(t);
202 | }
203 | finally {
204 | session.disconnect();
205 | latch.countDown();
206 | }
207 | }
208 | });
209 | }
210 | };
211 |
212 | WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient);
213 | stompClient.connect("ws://localhost:{port}/portfolio", this.headers, handler, port);
214 |
215 | if (failure.get() != null) {
216 | throw new AssertionError("", failure.get());
217 | }
218 |
219 | if (!latch.await(5, TimeUnit.SECONDS)) {
220 | fail("Portfolio positions not received");
221 | }
222 | }
223 |
224 | @Test
225 | public void executeTrade() throws Exception {
226 |
227 | final CountDownLatch latch = new CountDownLatch(1);
228 | final AtomicReference failure = new AtomicReference<>();
229 |
230 | StompSessionHandler handler = new AbstractTestSessionHandler(failure) {
231 |
232 | @Override
233 | public void afterConnected(final StompSession session, StompHeaders connectedHeaders) {
234 | session.subscribe("/user/queue/position-updates", new StompFrameHandler() {
235 | @Override
236 | public Type getPayloadType(StompHeaders headers) {
237 | return PortfolioPosition.class;
238 | }
239 |
240 | @Override
241 | public void handleFrame(StompHeaders headers, Object payload) {
242 | PortfolioPosition position = (PortfolioPosition) payload;
243 | logger.debug("Got " + position);
244 | try {
245 | assertEquals(75, position.getShares());
246 | assertEquals("Dell Inc.", position.getCompany());
247 | }
248 | catch (Throwable t) {
249 | failure.set(t);
250 | }
251 | finally {
252 | session.disconnect();
253 | latch.countDown();
254 | }
255 | }
256 | });
257 |
258 | try {
259 | Trade trade = new Trade();
260 | trade.setAction(Trade.TradeAction.Buy);
261 | trade.setTicker("DELL");
262 | trade.setShares(25);
263 | session.send("/app/trade", trade);
264 | }
265 | catch (Throwable t) {
266 | failure.set(t);
267 | latch.countDown();
268 | }
269 | }
270 | };
271 |
272 | WebSocketStompClient stompClient = new WebSocketStompClient(sockJsClient);
273 | stompClient.setMessageConverter(new MappingJackson2MessageConverter());
274 | stompClient.connect("ws://localhost:{port}/portfolio", headers, handler, port);
275 |
276 | if (!latch.await(10, TimeUnit.SECONDS)) {
277 | fail("Trade confirmation not received");
278 | }
279 | else if (failure.get() != null) {
280 | throw new AssertionError("", failure.get());
281 | }
282 | }
283 |
284 |
285 | public static class TestDispatcherServletInitializer extends DispatcherServletInitializer {
286 |
287 | @Override
288 | protected Class>[] getServletConfigClasses() {
289 | return new Class[] { WebConfig.class, TestWebSocketConfig.class };
290 | }
291 | }
292 |
293 | @Configuration
294 | @EnableScheduling
295 | @ComponentScan(
296 | basePackages="org.springframework.samples",
297 | excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION, value = Configuration.class)
298 | )
299 | @EnableWebSocketMessageBroker
300 | static class TestWebSocketConfig implements WebSocketMessageBrokerConfigurer {
301 |
302 | @Autowired
303 | Environment env;
304 |
305 | @Override
306 | public void registerStompEndpoints(StompEndpointRegistry registry) {
307 | // The test classpath includes both Tomcat and Jetty, so let's be explicit
308 | DefaultHandshakeHandler handler = new DefaultHandshakeHandler(new TomcatRequestUpgradeStrategy());
309 | registry.addEndpoint("/portfolio").setHandshakeHandler(handler).withSockJS();
310 | }
311 |
312 | @Override
313 | public void configureMessageBroker(MessageBrokerRegistry registry) {
314 | registry.enableSimpleBroker("/queue/", "/topic/");
315 | // registry.enableStompBrokerRelay("/queue/", "/topic/");
316 | registry.setApplicationDestinationPrefixes("/app");
317 | }
318 | }
319 |
320 | private static abstract class AbstractTestSessionHandler extends StompSessionHandlerAdapter {
321 |
322 | private final AtomicReference failure;
323 |
324 |
325 | public AbstractTestSessionHandler(AtomicReference failure) {
326 | this.failure = failure;
327 | }
328 |
329 | @Override
330 | public void handleFrame(StompHeaders headers, Object payload) {
331 | logger.error("STOMP ERROR frame: " + headers.toString());
332 | this.failure.set(new Exception(headers.toString()));
333 | }
334 |
335 | @Override
336 | public void handleException(StompSession s, StompCommand c, StompHeaders h, byte[] p, Throwable ex) {
337 | logger.error("Handler exception", ex);
338 | this.failure.set(ex);
339 | }
340 |
341 | @Override
342 | public void handleTransportError(StompSession session, Throwable ex) {
343 | logger.error("Transport failure", ex);
344 | this.failure.set(ex);
345 | }
346 | }
347 |
348 | }
349 |
--------------------------------------------------------------------------------
/src/test/java/org/springframework/samples/portfolio/web/load/StompBrokerRelayLoadApp.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2015 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.samples.portfolio.web.load;
18 |
19 | import java.util.ArrayList;
20 | import java.util.Collections;
21 | import java.util.List;
22 | import java.util.concurrent.BlockingQueue;
23 | import java.util.concurrent.CountDownLatch;
24 | import java.util.concurrent.LinkedBlockingQueue;
25 | import java.util.concurrent.TimeUnit;
26 |
27 | import org.springframework.context.ApplicationEvent;
28 | import org.springframework.context.ApplicationListener;
29 | import org.springframework.context.annotation.Bean;
30 | import org.springframework.context.annotation.Configuration;
31 | import org.springframework.context.event.ContextRefreshedEvent;
32 | import org.springframework.core.task.TaskExecutor;
33 | import org.springframework.messaging.Message;
34 | import org.springframework.messaging.MessageHandler;
35 | import org.springframework.messaging.MessagingException;
36 | import org.springframework.messaging.simp.SimpMessagingTemplate;
37 | import org.springframework.messaging.simp.broker.BrokerAvailabilityEvent;
38 | import org.springframework.messaging.simp.config.AbstractMessageBrokerConfiguration;
39 | import org.springframework.messaging.simp.config.ChannelRegistration;
40 | import org.springframework.messaging.simp.config.MessageBrokerRegistry;
41 | import org.springframework.messaging.simp.stomp.StompCommand;
42 | import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
43 | import org.springframework.messaging.simp.user.SimpUserRegistry;
44 | import org.springframework.messaging.support.AbstractSubscribableChannel;
45 | import org.springframework.messaging.support.MessageBuilder;
46 | import org.springframework.util.StopWatch;
47 | import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
48 | import org.springframework.web.socket.messaging.DefaultSimpUserRegistry;
49 |
50 | import static org.junit.Assert.*;
51 |
52 | /**
53 | * A load app that measures the throughput of messages sent through the
54 | * {@link org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler}
55 | * as well as the resulting messages broadcast back from the message broker.
56 | *
57 | *
This is not a full end-to-end app and does not involve WebSocket clients.
58 | * The test manually creates messages representing STOMP frames and sends them
59 | * to the "clientInboundChannel" (simulating clients) and to the "brokerChannel"
60 | * (for broadcasting messages). Messages received from the message broker are
61 | * captured through a {@link StompBrokerRelayLoadApp.TestMessageHandler}
62 | * subscribed to the "clientOutboundChannel".
63 | *
64 | *