Commit 7d77ed98 authored by Blaas Alexander's avatar Blaas Alexander
Browse files

first draft with websockets introduced

parent 11404799
/bin/
/target/
/.classpath
/.settings/
/.project
/src/main/webapp/WEB-INF/.faces-config.xml.jsfdia
\ No newline at end of file
......@@ -26,3 +26,4 @@ Christian Sillaber
Michael Brunner
Clemens Sauerwein
Andrea Mussmann
Alexander Blaas
......@@ -69,19 +69,6 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-api</artifactId>
<version>2.2.20</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.sun.faces</groupId>
<artifactId>jsf-impl</artifactId>
<version>2.2.20</version>
<scope>compile</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
......@@ -99,6 +86,18 @@
<scope>compile</scope>
</dependency>
<!-- special websockets dependencies -->
<dependency>
<groupId>org.joinfaces</groupId>
<artifactId>omnifaces3-spring-boot-starter</artifactId>
<version>4.4.2</version>
</dependency>
<dependency>
<groupId>org.joinfaces</groupId>
<artifactId>weld-spring-boot-starter</artifactId>
<version>4.4.2</version>
</dependency>
<!-- special test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
......@@ -162,4 +161,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
\ No newline at end of file
package at.qe.skeleton;
import at.qe.skeleton.configs.CustomServletContextInitializer;
import at.qe.skeleton.configs.WebSecurityConfig;
import at.qe.skeleton.utils.ViewScope;
import java.util.HashMap;
import javax.faces.webapp.FacesServlet;
import org.apache.catalina.startup.ContextConfig;
import org.springframework.beans.factory.config.CustomScopeConfigurer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
......@@ -12,8 +12,15 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.FilterType;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import at.qe.skeleton.configs.CustomServletContextInitializer;
import at.qe.skeleton.configs.WebSecurityConfig;
import at.qe.skeleton.utils.ViewScope;
/**
* Spring boot application. Execute maven with <code>mvn spring-boot:run</code>
* to start this web application.
......@@ -24,34 +31,45 @@ import org.springframework.security.config.annotation.method.configuration.Enabl
*/
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
/*
* Prevent spring from trying to autowire the websocket-infrastructure: Exclude
* the at.qe.skeleton.ui.websockets package from component scan.
*
* NOTE: Do not add any components to this package which should be managed by
* spring. It is reserved for the CDI-injection-mechanisms (Weld). Only add
* CDI-managed components.
*/
@ComponentScan(basePackages = "at.qe.skeleton", excludeFilters = @Filter(type = FilterType.REGEX, pattern = "at.qe.skeleton.ui.websockets.*"))
public class Main extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(new Class[]{Main.class, CustomServletContextInitializer.class, WebSecurityConfig.class});
}
@Bean
public ServletRegistrationBean servletRegistrationBean() {
FacesServlet servlet = new FacesServlet();
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(servlet, "*.xhtml");
servletRegistrationBean.setName("Faces Servlet");
servletRegistrationBean.setAsyncSupported(true);
servletRegistrationBean.setLoadOnStartup(1);
return servletRegistrationBean;
}
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();
HashMap<String, Object> customScopes = new HashMap<>();
customScopes.put("view", new ViewScope());
customScopeConfigurer.setScopes(customScopes);
return customScopeConfigurer;
}
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Main.class, CustomServletContextInitializer.class, WebSecurityConfig.class,
ContextConfig.class);
}
@Bean
public ServletRegistrationBean<FacesServlet> servletRegistrationBean() {
FacesServlet servlet = new FacesServlet();
ServletRegistrationBean<FacesServlet> servletRegistrationBean = new ServletRegistrationBean<>(servlet,
"*.xhtml");
servletRegistrationBean.setName("Faces Servlet");
servletRegistrationBean.setAsyncSupported(true);
servletRegistrationBean.setLoadOnStartup(1);
return servletRegistrationBean;
}
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();
HashMap<String, Object> customScopes = new HashMap<>();
customScopes.put("view", new ViewScope());
customScopeConfigurer.setScopes(customScopes);
return customScopeConfigurer;
}
}
......@@ -2,6 +2,7 @@ package at.qe.skeleton.configs;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.context.annotation.Configuration;
......@@ -15,10 +16,12 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class CustomServletContextInitializer implements ServletContextInitializer {
@Override
public void onStartup(ServletContext sc) throws ServletException {
sc.setInitParameter("javax.faces.DEFAULT_SUFFIX", ".xhtml");
sc.setInitParameter("javax.faces.PROJECT_STAGE", "Development");
}
@Override
public void onStartup(ServletContext sc) throws ServletException {
sc.setInitParameter("javax.faces.DEFAULT_SUFFIX", ".xhtml");
sc.setInitParameter("javax.faces.PROJECT_STAGE", "Development");
// websockets configuration
sc.setInitParameter("javax.faces.ENABLE_CDI_RESOLVER_CHAIN", "true");
sc.setInitParameter("org.omnifaces.SOCKET_ENDPOINT_ENABLED", "true");
}
}
......@@ -10,8 +10,11 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import at.qe.skeleton.spring.CustomizedLogoutSuccessHandler;
/**
* Spring configuration for web security.
*
......@@ -26,6 +29,11 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
DataSource dataSource;
@Bean
protected LogoutSuccessHandler logoutSuccessHandler() {
return new CustomizedLogoutSuccessHandler();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
......@@ -37,7 +45,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.logoutSuccessUrl("/login.xhtml");
.logoutSuccessUrl("/login.xhtml")
.logoutSuccessHandler(this.logoutSuccessHandler());
http.authorizeRequests()
//Permit access to the H2 console
......@@ -51,6 +60,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//Permit access only for some roles
.antMatchers("/secured/**")
.hasAnyAuthority("ADMIN", "MANAGER", "EMPLOYEE")
// Allow only certain roles to use websockets (only logged in users)
.antMatchers("/omnifaces.push/**")
.hasAnyAuthority("ADMIN", "MANAGER", "EMPLOYEE")
//If user doesn't have permission, forward him to login page
.and()
.formLogin()
......
......@@ -25,7 +25,7 @@ import org.springframework.data.domain.Persistable;
* University of Innsbruck.
*/
@Entity
public class User implements Persistable<String>, Serializable {
public class User implements Persistable<String>, Serializable, Comparable<User> {
private static final long serialVersionUID = 1L;
......@@ -194,4 +194,9 @@ public class User implements Persistable<String>, Serializable {
return (null == createDate);
}
@Override
public int compareTo(User o) {
return this.username.compareTo(o.getUsername());
}
}
package at.qe.skeleton.spring;
import java.lang.reflect.Field;
import javax.enterprise.inject.spi.CDI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
import at.qe.skeleton.ui.websockets.WebSocketManager;
import at.qe.skeleton.utils.CDIAutowired;
import at.qe.skeleton.utils.CDIContextRelated;
/**
* This beanPostProcessor is used to manually "autowire" CDI-managed beans (see
* {@link WebSocketManager}) within the spring-context. This happens after a
* beans' initialization is finished.
*
* This class is part of the skeleton project provided for students of the
* courses "Software Architecture" and "Software Engineering" offered by the
* University of Innsbruck.
*
*/
@Component
public class CDIAwareBeanPostProcessor implements BeanPostProcessor {
private static final Logger LOGGER = LoggerFactory.getLogger(CDIAwareBeanPostProcessor.class);
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// only if controller has a webSocketManager
Class<? extends Object> beanClass = bean.getClass();
/*
* proceed only if bean uses websockets (i.e. in our case cdi-managed
* webSocket-infrastructure)
*/
if (beanClass.isAnnotationPresent(CDIContextRelated.class)) {
// check for @CDIAutowired on fields to find the websocket-managing field
for (Field field : beanClass.getDeclaredFields()) {
field.setAccessible(true);
// when annotation is present, perform a manual "autowiring"
if (field.isAnnotationPresent(CDIAutowired.class)) {
Class<?> fieldType = field.getType();
Object cdiManagedBean = CDI.current().select(fieldType).get();
LOGGER.info("Field '{}' of '{}' successfully autowired", field.getName(), bean.getClass());
try {
field.set(bean, cdiManagedBean);
} catch (IllegalArgumentException | IllegalAccessException e) {
LOGGER.error("Manual CDI-injection failed", e);
}
}
}
}
// simply returns bean
return BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName);
}
}
package at.qe.skeleton.spring;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import at.qe.skeleton.ui.controllers.demo.ChatManagerController;
import at.qe.skeleton.ui.controllers.demo.UserStatusController;
/**
* This handler is triggered after a logout is performed.
*
* This class is part of the skeleton project provided for students of the
* courses "Software Architecture" and "Software Engineering" offered by the
* University of Innsbruck.
*
*/
@Component
public class CustomizedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private UserStatusController userStatusController;
@Autowired
private ChatManagerController chatManagerController;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
String username = authentication.getName();
// update chat-manager
this.chatManagerController.onLogout(username);
// update online-status
this.userStatusController.afterLogout(username);
// continue as expected
super.onLogoutSuccess(request, response, authentication);
}
}
package at.qe.skeleton.spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.stereotype.Component;
import at.qe.skeleton.ui.controllers.demo.ChatManagerController;
import at.qe.skeleton.ui.controllers.demo.UserStatusController;
/**
* This handler is triggered after a login is performed.
*
* This class is part of the skeleton project provided for students of the
* courses "Software Architecture" and "Software Engineering" offered by the
* University of Innsbruck.
*
*/
@Component
public class LoginSuccessHandler implements ApplicationListener<InteractiveAuthenticationSuccessEvent> {
@Autowired
private UserStatusController userStatusController;
@Autowired
private ChatManagerController chatManagerController;
@Override
public void onApplicationEvent(InteractiveAuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
// update chat-manager
this.chatManagerController.onLogin(username);
// update online-status
this.userStatusController.afterLogin(username);
}
}
package at.qe.skeleton.spring;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import at.qe.skeleton.ui.controllers.demo.UserStatusController;
/**
* This handler is triggered after the application-context is refreshed, i.e.
* configurations are setup.
*
* This class is part of the skeleton project provided for students of the
* courses "Software Architecture" and "Software Engineering" offered by the
* University of Innsbruck.
*
*/
@Component
public class UserStatusInitializationHandler implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private UserStatusController userStatusController;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// init
this.userStatusController.setupUserStatus();
}
}
......@@ -18,7 +18,12 @@ import org.springframework.stereotype.Component;
@Scope("view")
public class UserDetailController implements Serializable {
@Autowired
/**
*
*/
private static final long serialVersionUID = -8724249000495756469L;
@Autowired
private UserService userService;
/**
......
......@@ -19,7 +19,11 @@ import org.springframework.stereotype.Component;
@Scope("view")
public class UserListController implements Serializable {
@Autowired
/**
*
*/
private static final long serialVersionUID = -174521650843617720L;
@Autowired
private UserService userService;
/**
......
package at.qe.skeleton.ui.controllers.demo;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Controller;
import at.qe.skeleton.model.User;
import at.qe.skeleton.repositories.UserRepository;
import at.qe.skeleton.spring.CustomizedLogoutSuccessHandler;
import at.qe.skeleton.spring.LoginSuccessHandler;
import at.qe.skeleton.ui.websockets.WebSocketManager;
import at.qe.skeleton.utils.CDIAutowired;
import at.qe.skeleton.utils.CDIContextRelated;
/**
* The chatManagerController is used to manage all conversations between users
* (message-lists, i.e. chats).
*
* This class is part of the skeleton project provided for students of the
* courses "Software Architecture" and "Software Engineering" offered by the
* University of Innsbruck.
*/
@Controller
@Scope("application")
@CDIContextRelated
public class ChatManagerController {
@Autowired
private UserRepository userRepository;
@CDIAutowired
private WebSocketManager websocketManager;
private Set<User> possibleRecipients = new ConcurrentSkipListSet<>();
private Map<String, List<Message>> chats = new ConcurrentHashMap<>();
/**
* Called when a user logs in (see {@link LoginSuccessHandler}). Simply
* initializes the chat-infrastructure for the logged in user and adds it to the
* list of possible recipients
*
* @param username
*/
public void onLogin(String username) {
User user = this.userRepository.findFirstByUsername(username);
this.possibleRecipients.add(user);
this.chats.put(username, new LinkedList<>());
}
/**
* Called when a user logs out (see {@link CustomizedLogoutSuccessHandler}).
* Simply destroys, respectively, clears the previously initialized
* chat-infrastructure and removes the user from the list of possible recipients
*
* @param username
*/
public void onLogout(String username) {
User user = this.userRepository.findFirstByUsername(username);
this.possibleRecipients.remove(user);
this.chats.remove(user.getUsername());
}
/**
* Sends a message to specified recipients (to-property of message) using
* websockets. Only one delivery at a time is allowed.
*
* @param message
*/
public synchronized void deliver(Message message) {
User sender = message.getFrom();
List<User> recipients = message.getTo();
List<String> sendTo = recipients.stream().map(User::getUsername).collect(Collectors.toList());
// don't forget the sender
sendTo.add(sender.getUsername());
// add to chat-content
this.addToChatContent(message, recipients);
// also display at sender
this.addToChatContent(message, sender);
// notify sender and recpipient to update their chat-window
this.websocketManager.getMessageChannel().send("msgRecieved", sendTo);
}
/**
* Adds a message to the chat-content of the specified users.
*
* @param message The message to add
* @param to The recipient
*/
private void addToChatContent(Message message, User to) {
this.chats.get(to.getUsername()).add(message);
}
/**
* Convenience-method. See {@link #addToChatContent(Message, User)}
*
* @param message
* @param to
*/
private void addToChatContent(Message message, List<User> to) {
to.forEach(toUser -> this.addToChatContent(message, toUser));
}
public List<Message> getChatContentRef(User user1) {
return Collections.unmodifiableList(this.chats.get(user1.getUsername()));
}
public Set<User> getPossibleRecipients() {
return Collections.unmodifiableSet(possibleRecipients);
}
}
package at.qe.skeleton.ui.controllers.demo;
import java.util.Date;
import at.qe.skeleton.model.User;
/**
* A class which represents a logEntry
*
* This class is part of the skeleton project provided for students of the
* courses "Software Architecture" and "Software Engineering" offered by the
* University of Innsbruck.
*/
public class LogEntry {
private User user;
private Date timestamp = new Date();
private LogEntryType logType;