Skip to content

Commit 8ea3e3f

Browse files
committed
implement real time update of adding card
1 parent 64f285f commit 8ea3e3f

23 files changed

+876
-52
lines changed

front-end/src/real-time-client.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class RealTimeClient {
100100
console.log('[RealTimeClient] Received message', message)
101101

102102
if (message.channel) {
103-
this.$bus.$emit(this._channelEvent(message.channel), message.payload)
103+
this.$bus.$emit(this._channelEvent(message.channel), JSON.parse(message.payload))
104104
}
105105
}
106106
_send (message) {

front-end/src/views/BoardPage.vue

+24-7
Original file line numberDiff line numberDiff line change
@@ -200,17 +200,14 @@ export default {
200200
}
201201
202202
const card = {
203-
boardId: this.boardId,
203+
boardId: this.board.id,
204204
cardListId: cardList.id,
205205
title: cardList.cardForm.title,
206206
position: cardList.cards.length + 1
207207
}
208208
209209
cardService.add(card).then(savedCard => {
210-
cardList.cards.push({
211-
id: savedCard.id,
212-
title: savedCard.title
213-
})
210+
this.appendCardToList(cardList, savedCard)
214211
cardList.cardForm.title = ''
215212
this.focusCardForm(cardList)
216213
}).catch(error => {
@@ -290,8 +287,28 @@ export default {
290287
notify.error(error.message)
291288
})
292289
},
293-
onRealTimeUpdated (updates) {
294-
290+
onRealTimeUpdated (update) {
291+
console.log('[BoardPage] Real time update received', update)
292+
if (update.type === 'cardAdded') {
293+
this.onCardAdded(update.card)
294+
}
295+
},
296+
onCardAdded (card) {
297+
const cardList = this.cardLists.filter(cardList => { return cardList.id === card.cardListId })[0]
298+
if (!cardList) {
299+
console.warn('No card list found by id ' + card.cardListId)
300+
return
301+
}
302+
this.appendCardToList(cardList, card)
303+
},
304+
appendCardToList (cardList, card) {
305+
const existingIndex = cardList.cards.findIndex(existingCard => { return existingCard.id === card.id })
306+
if (existingIndex === -1) {
307+
cardList.cards.push({
308+
id: card.id,
309+
title: card.title
310+
})
311+
}
295312
}
296313
}
297314
}

src/main/java/com/taskagile/config/SecurityConfiguration.java

+9-9
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@ protected void configure(HttpSecurity http) throws Exception {
2929
http
3030
.exceptionHandling().accessDeniedHandler(accessDeniedHandler())
3131
.and()
32-
.authorizeRequests()
33-
.antMatchers(PUBLIC).permitAll()
34-
.anyRequest().authenticated()
32+
.authorizeRequests()
33+
.antMatchers(PUBLIC).permitAll()
34+
.anyRequest().authenticated()
3535
.and()
36-
.addFilterAt(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
37-
.formLogin()
38-
.loginPage("/login")
36+
.addFilterAt(authenticationFilter(), UsernamePasswordAuthenticationFilter.class)
37+
.formLogin()
38+
.loginPage("/login")
3939
.and()
40-
.logout()
41-
.logoutUrl("/api/me/logout")
42-
.logoutSuccessHandler(logoutSuccessHandler())
40+
.logout()
41+
.logoutUrl("/api/me/logout")
42+
.logoutSuccessHandler(logoutSuccessHandler())
4343
.and()
4444
.csrf().disable();
4545
}

src/main/java/com/taskagile/domain/model/user/UserId.java

+4
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@ public class UserId extends AbstractBaseId {
99
public UserId(long id) {
1010
super(id);
1111
}
12+
13+
public String toString() {
14+
return String.valueOf(value());
15+
}
1216
}

src/main/java/com/taskagile/web/apis/BoardApiController.java

-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ public class BoardApiController {
3232
private TeamService teamService;
3333
private CardListService cardListService;
3434
private CardService cardService;
35-
private UserService userService;
3635

3736
public BoardApiController(BoardService boardService,
3837
TeamService teamService,

src/main/java/com/taskagile/web/apis/CardApiController.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.taskagile.web.results.AddCardResult;
1010
import com.taskagile.web.results.ApiResult;
1111
import com.taskagile.web.results.Result;
12+
import com.taskagile.web.updater.CardUpdater;
1213
import org.springframework.http.ResponseEntity;
1314
import org.springframework.stereotype.Controller;
1415
import org.springframework.web.bind.annotation.PostMapping;
@@ -18,15 +19,18 @@
1819
public class CardApiController {
1920

2021
private CardService cardService;
22+
private CardUpdater cardUpdater;
2123

22-
public CardApiController(CardService cardService) {
24+
public CardApiController(CardService cardService, CardUpdater cardUpdater) {
2325
this.cardService = cardService;
26+
this.cardUpdater = cardUpdater;
2427
}
2528

2629
@PostMapping("/api/cards")
2730
public ResponseEntity<ApiResult> addCard(@RequestBody AddCardPayload payload,
2831
@CurrentUser SimpleUser currentUser) {
2932
Card card = cardService.addCard(payload.toCommand(currentUser.getUserId()));
33+
cardUpdater.onCardAdded(payload.getBoardId(), card);
3034
return AddCardResult.build(card);
3135
}
3236

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.taskagile.web.socket;
2+
3+
import java.lang.annotation.*;
4+
5+
@Target({ElementType.METHOD})
6+
@Retention(RetentionPolicy.RUNTIME)
7+
@Documented
8+
public @interface Action {
9+
10+
/**
11+
* The action pattern. It needs to be an exact match.
12+
* <p>For example, "subscribe"
13+
*/
14+
String value() default "";
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package com.taskagile.web.socket;
2+
3+
import org.springframework.stereotype.Component;
4+
5+
import java.lang.annotation.*;
6+
7+
@Target({ElementType.TYPE})
8+
@Retention(RetentionPolicy.RUNTIME)
9+
@Documented
10+
@Component
11+
public @interface ChannelHandler {
12+
13+
/**
14+
* Channel patter, alias of value()
15+
*/
16+
String pattern() default "";
17+
18+
/**
19+
* The channel pattern that the handler will be mapped to by {@link WebSocketRequestDispatcher}
20+
* using Spring's {@link org.springframework.util.AntPathMatcher}
21+
*/
22+
String value() default "";
23+
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package com.taskagile.web.socket;
2+
3+
import com.taskagile.utils.JsonUtils;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
6+
import org.springframework.util.AntPathMatcher;
7+
import org.springframework.util.Assert;
8+
9+
import java.lang.annotation.Annotation;
10+
import java.lang.reflect.Method;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
public class ChannelHandlerInvoker {
15+
16+
private static final Logger log = LoggerFactory.getLogger(ChannelHandlerInvoker.class);
17+
18+
private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
19+
20+
private String channelPattern;
21+
private Object handler;
22+
// Key is the action, value is the method to handle that action
23+
private final Map<String, Method> actionMethods = new HashMap<>();
24+
25+
public ChannelHandlerInvoker(Object handler) {
26+
Assert.notNull(handler, "Parameter `handler` must not be null");
27+
28+
Class<?> handlerClass = handler.getClass();
29+
ChannelHandler handlerAnnotation = handlerClass.getAnnotation(ChannelHandler.class);
30+
Assert.notNull(handlerAnnotation, "Parameter `handler` must have annotation @ChannelHandler");
31+
32+
Method[] methods = handlerClass.getMethods();
33+
for (Method method : methods) {
34+
Action actionAnnotation = method.getAnnotation(Action.class);
35+
if (actionAnnotation == null) {
36+
continue;
37+
}
38+
39+
String action = actionAnnotation.value();
40+
actionMethods.put(action, method);
41+
log.debug("Mapped action `{}` in channel handler `{}#{}`", action, handlerClass.getName(), method);
42+
}
43+
44+
this.channelPattern = ChannelHandlers.getPattern(handlerAnnotation);
45+
this.handler = handler;
46+
}
47+
48+
public boolean supports(String action) {
49+
return actionMethods.containsKey(action);
50+
}
51+
52+
public void handle(IncomingMessage incomingMessage, RealTimeSession session) {
53+
Assert.isTrue(antPathMatcher.match(channelPattern, incomingMessage.getChannel()), "Channel of the handler must match");
54+
Method actionMethod = actionMethods.get(incomingMessage.getAction());
55+
Assert.notNull(actionMethod, "Action method for `" + incomingMessage.getAction() + "` must exist");
56+
57+
// Find all required parameters
58+
Class<?>[] parameterTypes = actionMethod.getParameterTypes();
59+
// All the annotations for each parameter
60+
Annotation[][] allParameterAnnotations = actionMethod.getParameterAnnotations();
61+
// The arguments that will be passed to the action method
62+
Object[] args = new Object[parameterTypes.length];
63+
64+
try {
65+
// Populate arguments
66+
for (int i = 0; i < parameterTypes.length; i++) {
67+
Class<?> parameterType = parameterTypes[i];
68+
Annotation[] parameterAnnotations = allParameterAnnotations[i];
69+
70+
// No annotation applied on this parameter
71+
if (parameterAnnotations.length == 0) {
72+
if (parameterType.isInstance(session)) {
73+
args[i] = session;
74+
} else {
75+
args[i] = null;
76+
}
77+
continue;
78+
}
79+
80+
// Only use the first annotation applied on the parameter
81+
Annotation parameterAnnotation = parameterAnnotations[0];
82+
if (parameterAnnotation instanceof Payload) {
83+
Object arg = JsonUtils.toObject(incomingMessage.getPayload(), parameterType);
84+
if (arg == null) {
85+
throw new IllegalArgumentException("Unable to instantiate parameter of type `" +
86+
parameterType.getName() + "`.");
87+
}
88+
args[i] = arg;
89+
} else if (parameterAnnotation instanceof ChannelValue) {
90+
args[i] = incomingMessage.getChannel();
91+
}
92+
}
93+
94+
actionMethod.invoke(handler, args);
95+
} catch (Exception e) {
96+
String error = "Failed to invoker action method `" + incomingMessage.getAction() +
97+
"` at channel `" + incomingMessage.getChannel() + "` ";
98+
log.error(error, e);
99+
session.error(error);
100+
}
101+
}
102+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.taskagile.web.socket;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
import org.springframework.context.ApplicationContext;
6+
import org.springframework.stereotype.Component;
7+
import org.springframework.util.AntPathMatcher;
8+
9+
import java.util.HashMap;
10+
import java.util.Map;
11+
import java.util.Set;
12+
13+
@Component
14+
public class ChannelHandlerResolver {
15+
16+
private static final Logger log = LoggerFactory.getLogger(ChannelHandlerResolver.class);
17+
18+
private static final AntPathMatcher antPathMatcher = new AntPathMatcher();
19+
// The key is the channel ant-like path pattern, value is the corresponding invoker
20+
private final Map<String, ChannelHandlerInvoker> invokers = new HashMap<>();
21+
22+
private ApplicationContext applicationContext;
23+
24+
public ChannelHandlerResolver(ApplicationContext applicationContext) {
25+
this.applicationContext = applicationContext;
26+
this.bootstrap();
27+
}
28+
29+
public ChannelHandlerInvoker findInvoker(IncomingMessage incomingMessage) {
30+
ChannelHandlerInvoker invoker = null;
31+
Set<String> pathPatterns = invokers.keySet();
32+
for (String pathPattern : pathPatterns) {
33+
if (antPathMatcher.match(pathPattern, incomingMessage.getChannel())) {
34+
invoker = invokers.get(pathPattern);
35+
}
36+
}
37+
if (invoker == null) {
38+
return null;
39+
}
40+
return invoker.supports(incomingMessage.getAction()) ? invoker : null;
41+
}
42+
43+
private void bootstrap() {
44+
log.info("Bootstrapping channel handler resolver");
45+
46+
Map<String, Object> handlers = applicationContext.getBeansWithAnnotation(ChannelHandler.class);
47+
for (String handlerName : handlers.keySet()) {
48+
Object handler = handlers.get(handlerName);
49+
Class<?> handlerClass = handler.getClass();
50+
51+
ChannelHandler handlerAnnotation = handlerClass.getAnnotation(ChannelHandler.class);
52+
String channelPattern = ChannelHandlers.getPattern(handlerAnnotation);
53+
if (invokers.containsKey(channelPattern)) {
54+
throw new IllegalStateException("Duplicated handlers found for chanel pattern `" + channelPattern + "`.");
55+
}
56+
invokers.put(channelPattern, new ChannelHandlerInvoker(handler));
57+
log.debug("Mapped channel `{}` to channel handler `{}`", channelPattern, handlerClass.getName());
58+
}
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.taskagile.web.socket;
2+
3+
public final class ChannelHandlers {
4+
5+
public static String getPattern(ChannelHandler channelHandler) {
6+
if (!"".equals(channelHandler.pattern())) {
7+
return channelHandler.pattern();
8+
}
9+
return channelHandler.value();
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.taskagile.web.socket;
2+
3+
import java.lang.annotation.*;
4+
5+
/**
6+
* Mark a parameter as the channel's value
7+
*/
8+
@Target(ElementType.PARAMETER)
9+
@Retention(RetentionPolicy.RUNTIME)
10+
@Documented
11+
public @interface ChannelValue {
12+
}

0 commit comments

Comments
 (0)