diff --git a/README.md b/README.md index 86cbcb4eb4e28d8ed89fbe1379ee631a7ed182a0..3ef9d10812f5a94890f6fb6cb2bd8d234f1c5ee3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # ARSnova +--- +The next major version of ARSnova Backend is being developed on the `master` branch which is not yet production ready. +If you are looking for the current stable code base, switch to branch `2.x`. + +--- + ARSnova is a modern approach to Audience Response Systems (ARS). It is released under the GPLv3 license, and is offered as a Software as a Service free of charge. Head over to [arsnova.eu](https://arsnova.eu/) to see it in action. diff --git a/pom.xml b/pom.xml index 6f042b62468a7c3adb10efe59fcc4f55ab57749e..c101484e36729383a59a19bb03c4757d886f0024 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,5 @@ <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.thm.arsnova</groupId> <artifactId>arsnova-backend</artifactId> @@ -7,7 +7,7 @@ <packaging>war</packaging> <properties> - <io.spring.platform-version>Brussels-SR12</io.spring.platform-version> + <io.spring.platform-version>Cairo-SR3</io.spring.platform-version> <io.netty-version>4.1.28.Final</io.netty-version> <org.aspectj-version>1.8.13</org.aspectj-version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> @@ -111,6 +111,12 @@ <enabled>false</enabled> </snapshots> </repository> + <repository> + <id>spring-snapshots</id> + <name>Spring libs-snapshot</name> + <url>http://repo.spring.io/libs-snapshot</url> + <snapshots><enabled>true</enabled></snapshots> + </repository> </repositories> <dependencyManagement> @@ -137,23 +143,10 @@ <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> </dependency> - <!-- While commons-logging is not a required dependency, AJC fails without it. --> - <dependency> - <groupId>commons-logging</groupId> - <artifactId>commons-logging</artifactId> - <scope>provided</scope> - </dependency> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> - <exclusions> - <!-- Exclude Commons Logging in favor of SLF4j --> - <exclusion> - <groupId>commons-logging</groupId> - <artifactId>commons-logging</artifactId> - </exclusion> - </exclusions> </dependency> <dependency> <groupId>org.springframework</groupId> @@ -187,13 +180,6 @@ <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-cas</artifactId> - <exclusions> - <!-- Exclude log4j-over-slf4j to prevent delegation loop --> - <exclusion> - <groupId>org.slf4j</groupId> - <artifactId>log4j-over-slf4j</artifactId> - </exclusion> - </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> @@ -251,9 +237,9 @@ <scope>test</scope> </dependency> <dependency> - <groupId>org.pac4j</groupId> - <artifactId>spring-security-pac4j</artifactId> - <version>3.1.0</version> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>3.4.0</version> </dependency> <dependency> <groupId>org.pac4j</groupId> @@ -489,8 +475,8 @@ <apiSource> <springmvc>true</springmvc> <locations> - <location>de.thm.arsnova.controller</location> - <location>de.thm.arsnova.entities</location> + <location>de.thm.arsnova.controller.v2</location> + <location>de.thm.arsnova.model.v2</location> </locations> <schemes> <scheme>http</scheme> diff --git a/src/main/java/de/thm/arsnova/aop/UserSessionAspect.java b/src/main/java/de/thm/arsnova/aop/UserSessionAspect.java deleted file mode 100644 index f3f7ad86058bfffbfc74d0d0f02bbf6f6155d4cd..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/aop/UserSessionAspect.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.aop; - -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.services.UserSessionService; -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.AfterReturning; -import org.aspectj.lang.annotation.Aspect; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Configurable; - -/** - * Assigns a session to the {@link de.thm.arsnova.services.UserSessionService} whenever a user joins a - * session. - */ -@Aspect -@Configurable -public class UserSessionAspect { - - @Autowired - private UserSessionService userSessionService; - - /** Sets current user and ARSnova session in session scoped UserSessionService - */ - @AfterReturning( - pointcut = "execution(public * de.thm.arsnova.services.SessionService.join(..)) && args(keyword)", - returning = "session" - ) - public void joinSessionAdvice(final JoinPoint jp, final String keyword, final Session session) { - userSessionService.setSession(session); - } -} diff --git a/src/main/java/de/thm/arsnova/aop/package-info.java b/src/main/java/de/thm/arsnova/aop/package-info.java deleted file mode 100644 index b39e67531f983fae4917abe2d58a45aba9db2494..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/aop/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Classes and interfaces to support aspect-oriented programming - */ -package de.thm.arsnova.aop; diff --git a/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java b/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java index 4e94d3efa892c41c27ada9be5af2e1d02f44387a..719a3ed968597f83517aadce83e2b397e5c49886 100644 --- a/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java +++ b/src/main/java/de/thm/arsnova/cache/CacheBusterImpl.java @@ -17,7 +17,7 @@ */ package de.thm.arsnova.cache; -import de.thm.arsnova.events.*; +import de.thm.arsnova.event.*; import org.springframework.cache.annotation.CacheEvict; import org.springframework.stereotype.Component; @@ -77,10 +77,10 @@ public class CacheBusterImpl implements CacheBuster, ArsnovaEventVisitor { public void visit(NewFeedbackEvent event) { } @Override - public void visit(DeleteFeedbackForSessionsEvent event) { } + public void visit(DeleteFeedbackForRoomsEvent event) { } @Override - public void visit(StatusSessionEvent event) { } + public void visit(StatusRoomEvent event) { } @CacheEvict(value = "statistics", allEntries = true) @Override @@ -100,11 +100,11 @@ public class CacheBusterImpl implements CacheBuster, ArsnovaEventVisitor { @CacheEvict(value = "statistics", allEntries = true) @Override - public void visit(NewSessionEvent newSessionEvent) { } + public void visit(NewRoomEvent newSessionEvent) { } @CacheEvict(value = "statistics", allEntries = true) @Override - public void visit(DeleteSessionEvent deleteSessionEvent) { } + public void visit(DeleteRoomEvent deleteSessionEvent) { } @Override public void visit(LockVoteEvent lockVoteEvent) { } diff --git a/src/main/java/de/thm/arsnova/cache/CacheBusterListener.java b/src/main/java/de/thm/arsnova/cache/CacheBusterListener.java index eb481df4c4c0c8f7a2c826d81b152db887721b38..07baa649958ad208a6b5b4157a24810638113e5a 100644 --- a/src/main/java/de/thm/arsnova/cache/CacheBusterListener.java +++ b/src/main/java/de/thm/arsnova/cache/CacheBusterListener.java @@ -17,8 +17,8 @@ */ package de.thm.arsnova.cache; -import de.thm.arsnova.events.ArsnovaEvent; -import de.thm.arsnova.events.ArsnovaEventVisitor; +import de.thm.arsnova.event.ArsnovaEvent; +import de.thm.arsnova.event.ArsnovaEventVisitor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; diff --git a/src/main/java/de/thm/arsnova/cache/ScheduledCacheBuster.java b/src/main/java/de/thm/arsnova/cache/ScheduledCacheBuster.java index 9d3ee0eacdf97466fe8826b4b0e44a7bf9c15690..c3ea951f41fc27c8559a7e9dbbbbe6f326d9625e 100644 --- a/src/main/java/de/thm/arsnova/cache/ScheduledCacheBuster.java +++ b/src/main/java/de/thm/arsnova/cache/ScheduledCacheBuster.java @@ -34,7 +34,7 @@ import org.springframework.stereotype.Component; @Component public class ScheduledCacheBuster { - @CacheEvict(value = "sessions", allEntries = true) + @CacheEvict(value = "rooms", allEntries = true) @Scheduled(initialDelay = 1000 * 25, fixedRate = 1000 * 60 * 60 * 6) private void clearSessionCache() { } diff --git a/src/main/java/de/thm/arsnova/config/AppConfig.java b/src/main/java/de/thm/arsnova/config/AppConfig.java index 38b968c39f78ad279860b042007668d771b66bee..b7dcb3b72b6c2739e3ec429443ef822714cfd402 100644 --- a/src/main/java/de/thm/arsnova/config/AppConfig.java +++ b/src/main/java/de/thm/arsnova/config/AppConfig.java @@ -20,11 +20,14 @@ package de.thm.arsnova.config; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import de.thm.arsnova.model.migration.FromV2Migrator; +import de.thm.arsnova.model.migration.ToV2Migrator; import de.thm.arsnova.util.ImageUtils; import de.thm.arsnova.connector.client.ConnectorClient; import de.thm.arsnova.connector.client.ConnectorClientImpl; -import de.thm.arsnova.entities.serialization.CouchDbDocumentModule; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.CouchDbDocumentModule; +import de.thm.arsnova.model.serialization.View; +import de.thm.arsnova.web.PathApiVersionContentNegotiationStrategy; import de.thm.arsnova.websocket.ArsnovaSocketioServer; import de.thm.arsnova.websocket.ArsnovaSocketioServerImpl; import de.thm.arsnova.websocket.ArsnovaSocketioServerListener; @@ -59,9 +62,10 @@ import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.PathMatchConfigurer; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.view.InternalResourceViewResolver; import java.nio.charset.Charset; @@ -77,13 +81,11 @@ import java.util.List; * AspectJ's weaving). */ @ComponentScan({ - "de.thm.arsnova.aop", "de.thm.arsnova.cache", "de.thm.arsnova.controller", - "de.thm.arsnova.dao", - "de.thm.arsnova.events", + "de.thm.arsnova.event", "de.thm.arsnova.security", - "de.thm.arsnova.services", + "de.thm.arsnova.service", "de.thm.arsnova.web"}) @Configuration @EnableAsync @@ -93,11 +95,14 @@ import java.util.List; @EnableWebMvc @PropertySource( value = {"classpath:arsnova.properties.example", "file:/etc/arsnova/arsnova.properties"}, - ignoreResourceNotFound = true + ignoreResourceNotFound = true, + encoding = "UTF-8" ) -public class AppConfig extends WebMvcConfigurerAdapter { - public static final MediaType API_V2_MEDIA_TYPE = new MediaType("application", "vnd.de.thm.arsnova.v2+json"); - public static final MediaType API_V3_MEDIA_TYPE = new MediaType("application", "vnd.de.thm.arsnova.v3+json"); +public class AppConfig implements WebMvcConfigurer { + public static final String API_V2_MEDIA_TYPE_VALUE = "application/vnd.de.thm.arsnova.v2+json"; + public static final String API_V3_MEDIA_TYPE_VALUE = "application/vnd.de.thm.arsnova.v3+json"; + public static final MediaType API_V2_MEDIA_TYPE = MediaType.valueOf(API_V2_MEDIA_TYPE_VALUE); + public static final MediaType API_V3_MEDIA_TYPE = MediaType.valueOf(API_V3_MEDIA_TYPE_VALUE); @Autowired private Environment env; @@ -117,19 +122,27 @@ public class AppConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { - converters.add(stringMessageConverter()); - converters.add(apiV2JsonMessageConverter()); converters.add(defaultJsonMessageConverter()); + converters.add(apiV2JsonMessageConverter()); + converters.add(stringMessageConverter()); //converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); } @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + PathApiVersionContentNegotiationStrategy strategy = + new PathApiVersionContentNegotiationStrategy(API_V3_MEDIA_TYPE); configurer.mediaType("json", MediaType.APPLICATION_JSON_UTF8); configurer.mediaType("xml", MediaType.APPLICATION_XML); - configurer.defaultContentType(API_V3_MEDIA_TYPE); - configurer.favorParameter(true); + configurer.favorParameter(false); configurer.favorPathExtension(false); + //configurer.defaultContentType(API_V3_MEDIA_TYPE); + configurer.defaultContentTypeStrategy(strategy); + } + + @Override + public void configurePathMatch(final PathMatchConfigurer configurer) { + configurer.setUseSuffixPatternMatch(false); } @Override @@ -199,7 +212,7 @@ public class AppConfig extends WebMvcConfigurerAdapter { public MappingJackson2HttpMessageConverter apiV2JsonMessageConverter() { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); builder - .serializationInclusion(JsonInclude.Include.NON_EMPTY) + .serializationInclusion(JsonInclude.Include.NON_NULL) .defaultViewInclusion(false) .indentOutput(apiIndent) .featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) @@ -284,4 +297,14 @@ public class AppConfig extends WebMvcConfigurerAdapter { public ImageUtils imageUtils() { return new ImageUtils(); } + + @Bean + public FromV2Migrator fromV2Migrator() { + return new FromV2Migrator(); + } + + @Bean + public ToV2Migrator toV2Migrator() { + return new ToV2Migrator(); + } } diff --git a/src/main/java/de/thm/arsnova/config/PersistanceConfig.java b/src/main/java/de/thm/arsnova/config/PersistenceConfig.java similarity index 59% rename from src/main/java/de/thm/arsnova/config/PersistanceConfig.java rename to src/main/java/de/thm/arsnova/config/PersistenceConfig.java index 8955777066a7a0fb8bffdb00931c513edb952b0c..713e5fb6640e0bf91a14612036b32ebee963b9f9 100644 --- a/src/main/java/de/thm/arsnova/config/PersistanceConfig.java +++ b/src/main/java/de/thm/arsnova/config/PersistenceConfig.java @@ -1,28 +1,45 @@ package de.thm.arsnova.config; -import de.thm.arsnova.entities.serialization.CouchDbObjectMapperFactory; -import de.thm.arsnova.persistance.*; -import de.thm.arsnova.persistance.couchdb.*; -import org.ektorp.CouchDbConnector; +import de.thm.arsnova.model.serialization.CouchDbObjectMapperFactory; +import de.thm.arsnova.persistence.*; +import de.thm.arsnova.persistence.couchdb.*; +import de.thm.arsnova.persistence.couchdb.support.MangoCouchDbConnector; import org.ektorp.impl.StdCouchDbInstance; import org.ektorp.spring.HttpClientFactoryBean; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; +@ComponentScan({ + "de.thm.arsnova.persistence.couchdb" +}) @Configuration @Profile("!test") -public class PersistanceConfig { +public class PersistenceConfig { + private static final int MIGRATION_SOCKET_TIMEOUT = 30000; + @Value("${couchdb.name}") private String couchDbName; @Value("${couchdb.host}") private String couchDbHost; @Value("${couchdb.port}") private int couchDbPort; @Value("${couchdb.username:}") private String couchDbUsername; @Value("${couchdb.password:}") private String couchDbPassword; + @Value("${couchdb.migrate-from:}") private String couchDbMigrateFrom; + + @Bean + @Primary + public MangoCouchDbConnector couchDbConnector() throws Exception { + return new MangoCouchDbConnector(couchDbName, couchDbInstance(), couchDbObjectMapperFactory()); + } @Bean - public CouchDbConnector couchDbConnector() throws Exception { - return new InitializingCouchDbConnector(couchDbName, couchDbInstance(), new CouchDbObjectMapperFactory()); + public MangoCouchDbConnector couchDbMigrationConnector() throws Exception { + if (couchDbMigrateFrom.isEmpty()) { + return null; + } + return new MangoCouchDbConnector(couchDbMigrateFrom, couchDbInstance(), couchDbObjectMapperFactory()); } @Bean @@ -30,6 +47,11 @@ public class PersistanceConfig { return new StdCouchDbInstance(couchDbHttpClientFactory().getObject()); } + @Bean + public StdCouchDbInstance couchDbMigrationInstance() throws Exception { + return new StdCouchDbInstance(couchDbMigrationHttpClientFactory().getObject()); + } + @Bean public HttpClientFactoryBean couchDbHttpClientFactory() throws Exception { final HttpClientFactoryBean factory = new HttpClientFactoryBean(); @@ -43,6 +65,19 @@ public class PersistanceConfig { return factory; } + @Bean + public HttpClientFactoryBean couchDbMigrationHttpClientFactory() throws Exception { + final HttpClientFactoryBean factory = couchDbHttpClientFactory(); + factory.setSocketTimeout(MIGRATION_SOCKET_TIMEOUT); + + return factory; + } + + @Bean + public CouchDbObjectMapperFactory couchDbObjectMapperFactory() { + return new CouchDbObjectMapperFactory(); + } + @Bean public LogEntryRepository logEntryRepository() throws Exception { return new CouchDbLogEntryRepository(couchDbConnector(), false); @@ -54,8 +89,8 @@ public class PersistanceConfig { } @Bean - public SessionRepository sessionRepository() throws Exception { - return new CouchDbSessionRepository(couchDbConnector(), false); + public RoomRepository sessionRepository() throws Exception { + return new CouchDbRoomRepository(couchDbConnector(), false); } @Bean @@ -78,16 +113,6 @@ public class PersistanceConfig { return new CouchDbMotdRepository(couchDbConnector(), false); } - @Bean - public MotdListRepository motdListRepository() throws Exception { - return new CouchDbMotdListRepository(couchDbConnector(), false); - } - - @Bean - public VisitedSessionRepository visitedSessionRepository() throws Exception { - return new CouchDbVisitedSessionRepository(couchDbConnector(), false); - } - @Bean public StatisticsRepository statisticsRepository() throws Exception { return new CouchDbStatisticsRepository(couchDbConnector(), false); diff --git a/src/main/java/de/thm/arsnova/config/SecurityConfig.java b/src/main/java/de/thm/arsnova/config/SecurityConfig.java index 6874343f37dd61ed453bb11ad7cf0b8de368536c..5f7104582583ada8c308da05eff4a62e1007c9ae 100644 --- a/src/main/java/de/thm/arsnova/config/SecurityConfig.java +++ b/src/main/java/de/thm/arsnova/config/SecurityConfig.java @@ -21,16 +21,18 @@ import de.thm.arsnova.security.CasLogoutSuccessHandler; import de.thm.arsnova.security.CasUserDetailsService; import de.thm.arsnova.security.LoginAuthenticationFailureHandler; import de.thm.arsnova.security.LoginAuthenticationSucessHandler; -import de.thm.arsnova.security.ApplicationPermissionEvaluator; import de.thm.arsnova.security.CustomLdapUserDetailsMapper; -import de.thm.arsnova.security.DbUserDetailsService; +import de.thm.arsnova.security.RegisteredUserDetailsService; +import de.thm.arsnova.security.jwt.JwtAuthenticationProvider; +import de.thm.arsnova.security.jwt.JwtTokenFilter; +import de.thm.arsnova.security.pac4j.OauthCallbackFilter; +import de.thm.arsnova.security.pac4j.OauthAuthenticationProvider; import org.jasig.cas.client.validation.Cas20ProxyTicketValidator; import org.pac4j.core.client.Client; import org.pac4j.core.config.Config; import org.pac4j.oauth.client.FacebookClient; import org.pac4j.oauth.client.Google2Client; import org.pac4j.oauth.client.TwitterClient; -import org.pac4j.springframework.security.web.CallbackFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -40,11 +42,10 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; +import org.springframework.core.annotation.Order; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.ldap.core.support.LdapContextSource; -import org.springframework.security.access.PermissionEvaluator; -import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.cas.ServiceProperties; import org.springframework.security.cas.authentication.CasAuthenticationProvider; @@ -55,6 +56,7 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionRegistryImpl; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -69,6 +71,7 @@ import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; @@ -88,7 +91,9 @@ import java.util.List; @EnableWebSecurity @Profile("!test") public class SecurityConfig extends WebSecurityConfigurerAdapter { - private static final String OAUTH_CALLBACK_PATH_SUFFIX = "/auth/oauth_callback"; + public static final String OAUTH_CALLBACK_PATH_SUFFIX = "/auth/oauth_callback"; + public static final String CAS_LOGIN_PATH_SUFFIX = "/auth/login/cas"; + public static final String CAS_LOGOUT_PATH_SUFFIX = "/auth/logout/cas"; private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); @Autowired @@ -123,40 +128,60 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Value("${security.google.key}") private String googleKey; @Value("${security.google.secret}") private String googleSecret; - @PostConstruct - private void init() { - if ("".equals(apiPath)) { - apiPath = servletContext.getContextPath(); + public class HttpSecurityConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint()); + http.csrf().disable(); + http.headers().addHeaderWriter(new HstsHeaderWriter(false)); + + http.addFilterBefore(jwtTokenFilter(), UsernamePasswordAuthenticationFilter.class); + if (casEnabled) { + http.addFilter(casAuthenticationFilter()); + http.addFilter(casLogoutFilter()); + } + + if (facebookEnabled || googleEnabled || twitterEnabled) { + http.addFilterAfter(oauthCallbackFilter(), CasAuthenticationFilter.class); + } } } - @Override - protected void configure(HttpSecurity http) throws Exception { - http.exceptionHandling().authenticationEntryPoint(restAuthenticationEntryPoint()); - http.csrf().disable(); - http.headers() - .addHeaderWriter(new HstsHeaderWriter(false)); + @Configuration + @Order(2) + @Profile("!test") + public class StatelessHttpSecurityConfig extends HttpSecurityConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http.antMatcher("/**"); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + } + } - if (casEnabled) { - http.addFilter(casAuthenticationFilter()); - http.addFilter(casLogoutFilter()); + @Configuration + @Order(1) + @Profile("!test") + public class StatefulHttpSecurityConfig extends HttpSecurityConfig { + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http.antMatcher("/v2/**"); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED); } + } - if (facebookEnabled || googleEnabled || twitterEnabled) { - CallbackFilter callbackFilter = new CallbackFilter(oauthConfig()); - callbackFilter.setSuffix(OAUTH_CALLBACK_PATH_SUFFIX); - callbackFilter.setDefaultUrl(rootUrl + apiPath + "/"); - http.addFilterAfter(callbackFilter, CasAuthenticationFilter.class); + @PostConstruct + private void init() { + if ("".equals(apiPath)) { + apiPath = servletContext.getContextPath(); } } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { List<String> providers = new ArrayList<>(); - if (dbAuthEnabled) { - providers.add("user-db"); - auth.authenticationProvider(daoAuthenticationProvider()); - } + auth.authenticationProvider(jwtAuthenticationProvider()); if (ldapEnabled) { providers.add("ldap"); auth.authenticationProvider(ldapAuthenticationProvider()); @@ -165,24 +190,25 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { providers.add("cas"); auth.authenticationProvider(casAuthenticationProvider()); } - if (googleEnabled) { - providers.add("google"); - } - if (facebookEnabled) { - providers.add("facebook"); + if (dbAuthEnabled) { + providers.add("user-db"); + auth.authenticationProvider(daoAuthenticationProvider()); } - if (twitterEnabled) { - providers.add("twitter"); + if (googleEnabled || facebookEnabled || twitterEnabled) { + if (googleEnabled) { + providers.add("google"); + } + if (facebookEnabled) { + providers.add("facebook"); + } + if (twitterEnabled) { + providers.add("twitter"); + } + auth.authenticationProvider(oauthAuthenticationProvider()); } logger.info("Enabled authentication providers: {}", providers); } - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManager(); - } - @Bean public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { final PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); @@ -202,13 +228,19 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } @Bean - public PermissionEvaluator permissionEvaluator() { - return new ApplicationPermissionEvaluator(); + public static AuthenticationEntryPoint restAuthenticationEntryPoint() { + return new Http403ForbiddenEntryPoint(); } @Bean - public static AuthenticationEntryPoint restAuthenticationEntryPoint() { - return new Http403ForbiddenEntryPoint(); + public JwtAuthenticationProvider jwtAuthenticationProvider() { + return new JwtAuthenticationProvider(); + } + + @Bean + public JwtTokenFilter jwtTokenFilter() throws Exception { + JwtTokenFilter jwtTokenFilter = new JwtTokenFilter(); + return jwtTokenFilter; } @Bean @@ -232,7 +264,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public DaoAuthenticationProvider daoAuthenticationProvider() { final DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); - authProvider.setUserDetailsService(dbUserDetailsService()); + authProvider.setUserDetailsService(registeredUserDetailsService()); authProvider.setPasswordEncoder(passwordEncoder()); return authProvider; @@ -244,8 +276,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { } @Bean - public DbUserDetailsService dbUserDetailsService() { - return new DbUserDetailsService(); + public RegisteredUserDetailsService registeredUserDetailsService() { + return new RegisteredUserDetailsService(); } @Bean @@ -324,7 +356,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public ServiceProperties casServiceProperties() { ServiceProperties properties = new ServiceProperties(); - properties.setService(rootUrl + apiPath + "/login/cas"); + properties.setService(rootUrl + apiPath + CAS_LOGIN_PATH_SUFFIX); properties.setSendRenew(false); return properties; @@ -348,6 +380,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { public CasAuthenticationFilter casAuthenticationFilter() throws Exception { CasAuthenticationFilter filter = new CasAuthenticationFilter(); filter.setAuthenticationManager(authenticationManager()); + filter.setServiceProperties(casServiceProperties()); + filter.setFilterProcessesUrl("/**" + CAS_LOGIN_PATH_SUFFIX); filter.setAuthenticationSuccessHandler(successHandler()); filter.setAuthenticationFailureHandler(failureHandler()); @@ -357,7 +391,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public LogoutFilter casLogoutFilter() { LogoutFilter filter = new LogoutFilter(casLogoutSuccessHandler(), logoutHandler()); - filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/j_spring_cas_security_logout")); + filter.setLogoutRequestMatcher(new AntPathRequestMatcher("/**" + CAS_LOGOUT_PATH_SUFFIX)); return filter; } @@ -389,6 +423,20 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter { return new Config(rootUrl + apiPath + OAUTH_CALLBACK_PATH_SUFFIX, clients); } + @Bean + public OauthCallbackFilter oauthCallbackFilter() throws Exception { + OauthCallbackFilter callbackFilter = new OauthCallbackFilter(oauthConfig()); + callbackFilter.setAuthenticationManager(authenticationManager()); + callbackFilter.setFilterProcessesUrl("/**" + OAUTH_CALLBACK_PATH_SUFFIX); + + return callbackFilter; + } + + @Bean + public OauthAuthenticationProvider oauthAuthenticationProvider() { + return new OauthAuthenticationProvider(); + } + @Bean public FacebookClient facebookClient() { final FacebookClient client = new FacebookClient(facebookKey, facebookSecret); diff --git a/src/main/java/de/thm/arsnova/controller/AbstractEntityController.java b/src/main/java/de/thm/arsnova/controller/AbstractEntityController.java new file mode 100644 index 0000000000000000000000000000000000000000..0b6c96c8ffe8c3bd5c033aabbff12009b0f2dc4c --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/AbstractEntityController.java @@ -0,0 +1,173 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller; + +import de.thm.arsnova.model.Entity; +import de.thm.arsnova.model.FindQuery; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.service.EntityService; +import de.thm.arsnova.service.FindQueryService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.naming.OperationNotSupportedException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * Base type for Entity controllers which provides basic CRUD operations and supports Entity patching. + * + * @param <E> Entity type + * @author Daniel Gerhardt + */ +public abstract class AbstractEntityController<E extends Entity> { + public static final String MEDIATYPE_EMPTY = "application/x-empty"; + private static final Logger logger = LoggerFactory.getLogger(AbstractEntityController.class); + protected static final String ENTITY_ID_HEADER = "Arsnova-Entity-Id"; + protected static final String ENTITY_REVISION_HEADER = "Arsnova-Entity-Revision"; + protected static final String DEFAULT_ROOT_MAPPING = "/"; + protected static final String DEFAULT_ID_MAPPING = "/{id:[^~].+}"; + protected static final String DEFAULT_ALIAS_MAPPING = "/~{alias}"; + protected static final String DEFAULT_FIND_MAPPING = "/find"; + protected static final String ALIAS_SUBPATH = "/{subPath:.+}"; + protected static final String GET_MAPPING = DEFAULT_ID_MAPPING; + protected static final String GET_MULTIPLE_MAPPING = DEFAULT_ROOT_MAPPING; + protected static final String PUT_MAPPING = DEFAULT_ID_MAPPING; + protected static final String POST_MAPPING = DEFAULT_ROOT_MAPPING; + protected static final String PATCH_MAPPING = DEFAULT_ID_MAPPING; + protected static final String DELETE_MAPPING = DEFAULT_ID_MAPPING; + protected static final String FIND_MAPPING = DEFAULT_FIND_MAPPING; + protected final EntityService<E> entityService; + protected FindQueryService<E> findQueryService; + + protected AbstractEntityController(final EntityService<E> entityService) { + this.entityService = entityService; + } + + protected abstract String getMapping(); + + @GetMapping(GET_MAPPING) + public E get(@PathVariable final String id) { + return entityService.get(id); + } + + @GetMapping(GET_MULTIPLE_MAPPING) + public Iterable<E> getMultiple(@RequestParam final Collection<String> ids) { + return entityService.get(ids); + } + + @PutMapping(value = PUT_MAPPING, produces = MEDIATYPE_EMPTY) + public void putWithoutResponse(@RequestBody final E entity, final HttpServletResponse httpServletResponse) { + put(entity, httpServletResponse); + } + + @PutMapping(PUT_MAPPING) + public E put(@RequestBody final E entity, final HttpServletResponse httpServletResponse) { + E oldEntity = entityService.get(entity.getId()); + entityService.update(oldEntity, entity); + httpServletResponse.setHeader(ENTITY_ID_HEADER, entity.getId()); + httpServletResponse.setHeader(ENTITY_REVISION_HEADER, entity.getRevision()); + + return entity; + } + + @PostMapping(value = POST_MAPPING, produces = MEDIATYPE_EMPTY) + @ResponseStatus(HttpStatus.CREATED) + public void postWithoutResponse(@RequestBody final E entity, final HttpServletResponse httpServletResponse) { + post(entity, httpServletResponse); + } + + @PostMapping(POST_MAPPING) + @ResponseStatus(HttpStatus.CREATED) + public E post(@RequestBody final E entity, final HttpServletResponse httpServletResponse) { + entityService.create(entity); + final String uri = UriComponentsBuilder.fromPath(getMapping()).path(GET_MAPPING) + .buildAndExpand(entity.getId()).toUriString(); + httpServletResponse.setHeader(HttpHeaders.LOCATION, uri); + httpServletResponse.setHeader(ENTITY_ID_HEADER, entity.getId()); + httpServletResponse.setHeader(ENTITY_REVISION_HEADER, entity.getRevision()); + + return entity; + } + + @PatchMapping(value = PATCH_MAPPING, produces = MEDIATYPE_EMPTY) + public void patchWithoutResponse(@PathVariable final String id, @RequestBody final Map<String, Object> changes, + final HttpServletResponse httpServletResponse) throws IOException { + patch(id, changes, httpServletResponse); + } + + @PatchMapping(PATCH_MAPPING) + public E patch(@PathVariable final String id, @RequestBody final Map<String, Object> changes, + final HttpServletResponse httpServletResponse) throws IOException { + E entity = entityService.get(id); + entityService.patch(entity, changes); + httpServletResponse.setHeader(ENTITY_ID_HEADER, entity.getId()); + httpServletResponse.setHeader(ENTITY_REVISION_HEADER, entity.getRevision()); + + return entity; + } + + @DeleteMapping(DELETE_MAPPING) + public void delete(@PathVariable final String id) { + E entity = entityService.get(id); + entityService.delete(entity); + } + + @PostMapping(FIND_MAPPING) + public Iterable<E> find(@RequestBody final FindQuery<E> findQuery) throws OperationNotSupportedException { + if (findQueryService != null) { + logger.debug("Resolving find query: {}", findQuery); + Set<String> ids = findQueryService.resolveQuery(findQuery); + logger.debug("Resolved find query to IDs: {}", ids); + + return entityService.get(ids); + } else { + throw new OperationNotSupportedException("Find is not supported for this entity type."); + } + } + + @RequestMapping(value = {DEFAULT_ALIAS_MAPPING, DEFAULT_ALIAS_MAPPING + ALIAS_SUBPATH}) + public void forwardAlias(@PathVariable final String alias, @PathVariable(required = false) String subPath, + HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) + throws ServletException, IOException { + final String targetPath = String.format( + "%s/%s%s", getMapping(), resolveAlias(alias), subPath != null ? "/" + subPath : ""); + logger.debug("Forwarding alias request to {}", targetPath); + httpServletRequest.getRequestDispatcher( targetPath) + .forward(httpServletRequest, httpServletResponse); + } + + protected String resolveAlias(final String alias) { + throw new NotFoundException("Aliases not supported for " + getMapping() + "."); + } + + @Autowired(required = false) + public void setFindQueryService(final FindQueryService<E> findQueryService) { + this.findQueryService = findQueryService; + } +} diff --git a/src/main/java/de/thm/arsnova/controller/AnswerController.java b/src/main/java/de/thm/arsnova/controller/AnswerController.java new file mode 100644 index 0000000000000000000000000000000000000000..2f0a8b2f0c1d2fecd77206434d9d524ae9dacc64 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/AnswerController.java @@ -0,0 +1,41 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller; + +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.service.AnswerService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(AnswerController.REQUEST_MAPPING) +public class AnswerController extends AbstractEntityController<Answer> { + protected static final String REQUEST_MAPPING = "/answer"; + + private AnswerService answerService; + + public AnswerController(final AnswerService answerService) { + super(answerService); + this.answerService = answerService; + } + + @Override + protected String getMapping() { + return REQUEST_MAPPING; + } +} diff --git a/src/main/java/de/thm/arsnova/controller/AuthenticationController.java b/src/main/java/de/thm/arsnova/controller/AuthenticationController.java new file mode 100644 index 0000000000000000000000000000000000000000..91b3a35f5cee978aa8b1ab0f80f2aaf2a8e85051 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/AuthenticationController.java @@ -0,0 +1,47 @@ +package de.thm.arsnova.controller; + +import de.thm.arsnova.model.ClientAuthentication; +import de.thm.arsnova.model.LoginCredentials; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.service.UserService; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +public class AuthenticationController { + private UserService userService; + + public AuthenticationController(final UserService userService) { + this.userService = userService; + } + + @PostMapping("/login") + public ClientAuthentication login() { + return userService.getCurrentClientAuthentication(); + } + + @PostMapping("/login/registered") + public ClientAuthentication loginRegistered(@RequestBody LoginCredentials loginCredentials) { + final String loginId = loginCredentials.getLoginId().toLowerCase(); + userService.authenticate(new UsernamePasswordAuthenticationToken(loginId, loginCredentials.getPassword()), + UserProfile.AuthProvider.ARSNOVA); + return userService.getCurrentClientAuthentication(); + } + + @PostMapping("/login/guest") + public ClientAuthentication loginGuest() { + final ClientAuthentication currentAuthentication = userService.getCurrentClientAuthentication(); + if (currentAuthentication != null + && currentAuthentication.getAuthProvider() == UserProfile.AuthProvider.ARSNOVA_GUEST) { + return currentAuthentication; + } + userService.authenticate(new UsernamePasswordAuthenticationToken(null, null), + UserProfile.AuthProvider.ARSNOVA_GUEST); + + return userService.getCurrentClientAuthentication(); + } +} diff --git a/src/main/java/de/thm/arsnova/controller/CommentController.java b/src/main/java/de/thm/arsnova/controller/CommentController.java index b074df1c41c6d857858138165720f6345eb6596e..5e1ada7ff54a890bc17650c7307dcd451fcab91d 100644 --- a/src/main/java/de/thm/arsnova/controller/CommentController.java +++ b/src/main/java/de/thm/arsnova/controller/CommentController.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2018 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,95 +17,25 @@ */ package de.thm.arsnova.controller; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.CommentReadingCount; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.services.CommentService; -import de.thm.arsnova.web.DeprecatedApi; -import de.thm.arsnova.web.Pagination; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.service.CommentService; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import java.util.List; - -/** - * Handles requests related to comments. - */ @RestController -@RequestMapping("/audiencequestion") -@Api(value = "/audiencequestion", description = "the Audience Content API") -public class CommentController extends PaginationController { +@RequestMapping(CommentController.REQUEST_MAPPING) +public class CommentController extends AbstractEntityController<Comment> { + protected static final String REQUEST_MAPPING = "/comment"; - @Autowired private CommentService commentService; - @ApiOperation(value = "Count all the comments in current session", - nickname = "getAudienceQuestionCount") - @RequestMapping(value = "/count", method = RequestMethod.GET) - @DeprecatedApi - @Deprecated - public int getInterposedCount(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey) { - return commentService.count(sessionkey); - } - - @ApiOperation(value = "count all unread comments", - nickname = "getUnreadInterposedCount") - @RequestMapping(value = "/readcount", method = RequestMethod.GET) - @DeprecatedApi - @Deprecated - public CommentReadingCount getUnreadInterposedCount(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam("sessionkey") final String sessionkey, String user) { - return commentService.countRead(sessionkey, user); - } - - @ApiOperation(value = "Retrieves all Comments for a Session", - nickname = "getInterposedQuestions") - @RequestMapping(value = "/", method = RequestMethod.GET) - @Pagination - public List<Comment> getInterposedQuestions(@ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey) { - return commentService.getBySessionKey(sessionkey, offset, limit); - } - - @ApiOperation(value = "Retrieves an Comment", - nickname = "getInterposedQuestion") - @RequestMapping(value = "/{questionId}", method = RequestMethod.GET) - public Comment getInterposedQuestion(@ApiParam(value = "ID of the Comment that needs to be deleted", required = true) @PathVariable final String questionId) { - return commentService.getAndMarkRead(questionId); - } - - @ApiOperation(value = "Creates a new Comment for a Session and returns the Comment's data", - nickname = "postInterposedQuestion") - @ApiResponses(value = { - @ApiResponse(code = 400, message = HTML_STATUS_400) - }) - @RequestMapping(value = "/", method = RequestMethod.POST) - @ResponseStatus(HttpStatus.CREATED) - public void postInterposedQuestion( - @ApiParam(value = "Session-Key from current session", required = true) @RequestParam final String sessionkey, - @ApiParam(value = "the body from the new comment", required = true) @RequestBody final de.thm.arsnova.entities.Comment comment - ) { - if (commentService.save(comment)) { - return; - } - - throw new BadRequestException(); + public CommentController(final CommentService commentService) { + super(commentService); + this.commentService = commentService; } - @ApiOperation(value = "Deletes a Comment", - nickname = "deleteInterposedQuestion") - @RequestMapping(value = "/{questionId}", method = RequestMethod.DELETE) - public void deleteInterposedQuestion(@ApiParam(value = "ID of the comment that needs to be deleted", required = true) @PathVariable final String questionId) { - commentService.delete(questionId); + @Override + protected String getMapping() { + return REQUEST_MAPPING; } } diff --git a/src/main/java/de/thm/arsnova/controller/ConfigurationController.java b/src/main/java/de/thm/arsnova/controller/ConfigurationController.java index bd310bd4b9310be95c2d3f0154c056a1d6715bdb..36909b5da4d7ee42f4de762b03c976ef5bbdb95f 100644 --- a/src/main/java/de/thm/arsnova/controller/ConfigurationController.java +++ b/src/main/java/de/thm/arsnova/controller/ConfigurationController.java @@ -92,7 +92,7 @@ public class ConfigurationController extends AbstractController { private String gridSquareEnabled; @Value("${features.session-import-export.enabled:false}") - private String sessionImportExportEnabled; + private String roomImportExportEnabled; @Value("${features.public-pool.enabled:false}") private String publicPoolEnabled; @@ -131,7 +131,7 @@ public class ConfigurationController extends AbstractController { private String trackingSiteId; @Value("${session.demo-id:}") - private String demoSessionKey; + private String demoRoomShortId; @Value("${ui.slogan:}") private String arsnovaSlogan; @@ -209,8 +209,8 @@ public class ConfigurationController extends AbstractController { if (!"".equals(privacyPolicyUrl)) { config.put("privacyPolicyUrl", privacyPolicyUrl); } - if (!"".equals(demoSessionKey)) { - config.put("demoSessionKey", demoSessionKey); + if (!"".equals(demoRoomShortId)) { + config.put("demoRoomShortId", demoRoomShortId); } if (!"".equals(arsnovaSlogan)) { config.put("arsnovaSlogan", arsnovaSlogan); @@ -233,7 +233,7 @@ public class ConfigurationController extends AbstractController { features.put("markdown", true); features.put("imageAnswer", "true".equals(imageAnswerEnabled)); features.put("gridSquare", "true".equals(gridSquareEnabled)); - features.put("sessionImportExport", "true".equals(sessionImportExportEnabled)); + features.put("sessionImportExport", "true".equals(roomImportExportEnabled)); features.put("publicPool", "true".equals(publicPoolEnabled)); features.put("exportToClick", "true".equals(exportToClickEnabled)); diff --git a/src/main/java/de/thm/arsnova/controller/ContentController.java b/src/main/java/de/thm/arsnova/controller/ContentController.java index e2858bf4040c8d5c84783cd6a06bb55e5cfc939d..de17e6d37009184dcaf8b142ca4a254afcde593c 100644 --- a/src/main/java/de/thm/arsnova/controller/ContentController.java +++ b/src/main/java/de/thm/arsnova/controller/ContentController.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2018 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,596 +17,37 @@ */ package de.thm.arsnova.controller; -import de.thm.arsnova.util.PaginationListDecorator; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.exceptions.ForbiddenException; -import de.thm.arsnova.exceptions.NoContentException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.services.ContentService; -import de.thm.arsnova.web.DeprecatedApi; -import de.thm.arsnova.web.Pagination; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; +import de.thm.arsnova.model.AnswerStatistics; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.service.AnswerService; +import de.thm.arsnova.service.ContentService; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * Handles requests related to questions teachers are asking their students. - */ @RestController -@RequestMapping("/lecturerquestion") -@Api(value = "/lecturerquestion", description = "Operations for Lecture Questions") -public class ContentController extends PaginationController { - @Autowired - private ContentService contentService; - - @ApiOperation(value = "Get question with provided question Id", - nickname = "getQuestion") - @ApiResponses(value = { - @ApiResponse(code = 404, message = HTML_STATUS_404) - }) - @RequestMapping(value = "/{questionId}", method = RequestMethod.GET) - public Content getQuestion(@PathVariable final String questionId) { - final Content content = contentService.get(questionId); - if (content != null) { - return content; - } - - throw new NotFoundException(); - } - - @ApiOperation(value = "Post provided content", - nickname = "postQuestion") - @ApiResponses(value = { - @ApiResponse(code = 400, message = HTML_STATUS_400) - }) - @RequestMapping(value = "/", method = RequestMethod.POST) - @ResponseStatus(HttpStatus.CREATED) - public Content postQuestion(@RequestBody final Content content) { - if (contentService.save(content) != null) { - return content; - } - throw new BadRequestException(); - } - - @ApiOperation(value = "Post provided contents", nickname = "bulkPostQuestions") - @ApiResponses(value = { - @ApiResponse(code = 400, message = HTML_STATUS_400) - }) - @RequestMapping(value = "/bulk", method = RequestMethod.POST) - @ResponseStatus(HttpStatus.CREATED) - public List<Content> bulkPostQuestions(@RequestBody final List<Content> contents) { - for (final Content content : contents) { - if (contentService.save(content) == null) { - throw new BadRequestException(); - } - } - return contents; - } - - @ApiOperation(value = "Update the content, identified by provided id, with the provided content in the Request Body", - nickname = "updateQuestion") - @ApiResponses(value = { - @ApiResponse(code = 400, message = HTML_STATUS_400) - }) - @RequestMapping(value = "/{questionId}", method = RequestMethod.PUT) - public Content updateQuestion( - @PathVariable final String questionId, - @RequestBody final Content content - ) { - try { - return contentService.update(content); - } catch (final Exception e) { - throw new BadRequestException(); - } - } - - @ApiOperation(value = "Start new Pi Round on question, identified by provided id, with an optional time", - nickname = "startPiRound") - @RequestMapping(value = "/{questionId}/questionimage", method = RequestMethod.GET) - public String getQuestionImage( - @PathVariable final String questionId, - @RequestParam(value = "fcImage", defaultValue = "false", required = false) final boolean fcImage - ) { - - if (fcImage) { - return contentService.getQuestionFcImage(questionId); - } else { - return contentService.getQuestionImage(questionId); - } - } - - @RequestMapping(value = "/{questionId}/startnewpiround", method = RequestMethod.POST) - public void startPiRound( - @PathVariable final String questionId, - @RequestParam(value = "time", defaultValue = "0", required = false) final int time - ) { - - if (time == 0) { - contentService.startNewPiRound(questionId, null); - } else { - contentService.startNewPiRoundDelayed(questionId, time); - } - } - - @RequestMapping(value = "/{questionId}/canceldelayedpiround", method = RequestMethod.POST) - @ApiOperation(value = "Cancel Pi Round on question, identified by provided id", - nickname = "cancelPiRound") - public void cancelPiRound( - @PathVariable final String questionId - ) { - contentService.cancelPiRoundChange(questionId); - } - - @RequestMapping(value = "/{questionId}/resetpiroundstate", method = RequestMethod.POST) - @ApiOperation(value = "Reset Pi Round on question, identified by provided id", - nickname = "resetPiQuestion") - public void resetPiQuestion( - @PathVariable final String questionId - ) { - contentService.resetPiRoundState(questionId); - } - - @ApiOperation(value = "Set voting admission on question, identified by provided id", - nickname = "setVotingAdmission") - @RequestMapping(value = "/{questionId}/disablevote", method = RequestMethod.POST) - public void setVotingAdmission( - @PathVariable final String questionId, - @RequestParam(value = "disable", defaultValue = "false", required = false) final Boolean disableVote - ) { - boolean disable = false; - - if (disableVote != null) { - disable = disableVote; - } - - contentService.setVotingAdmission(questionId, disable); - } - - @ApiOperation(value = "Set voting admission for all questions", - nickname = "setVotingAdmissionForAllQuestions") - @RequestMapping(value = "/disablevote", method = RequestMethod.POST) - public void setVotingAdmissionForAllQuestions( - @RequestParam final String sessionkey, - @RequestParam(value = "disable", defaultValue = "false", required = false) final Boolean disableVote, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false", required = false) final boolean lectureQuestionsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false", required = false) final boolean preparationQuestionsOnly - ) { - boolean disable = false; - List<Content> contents; - - if (disableVote != null) { - disable = disableVote; - } - - if (lectureQuestionsOnly) { - contents = contentService.getLectureQuestions(sessionkey); - contentService.setVotingAdmissions(sessionkey, disable, contents); - } else if (preparationQuestionsOnly) { - contents = contentService.getPreparationQuestions(sessionkey); - contentService.setVotingAdmissions(sessionkey, disable, contents); - } else { - contentService.setVotingAdmissionForAllQuestions(sessionkey, disable); - } - } - - @ApiOperation(value = "Publish a content, identified by provided id and content in Request Body.", - nickname = "publishQuestion") - @RequestMapping(value = "/{questionId}/publish", method = RequestMethod.POST) - public void publishQuestion( - @PathVariable final String questionId, - @RequestParam(required = false) final Boolean publish, - @RequestBody final Content content - ) { - if (publish != null) { - content.setActive(publish); - } - contentService.update(content); - } - - @ApiOperation(value = "Publish all questions", - nickname = "publishAllQuestions") - @RequestMapping(value = "/publish", method = RequestMethod.POST) - public void publishAllQuestions( - @RequestParam final String sessionkey, - @RequestParam(required = false) final Boolean publish, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false", required = false) final boolean lectureQuestionsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false", required = false) final boolean preparationQuestionsOnly - ) { - boolean p = publish == null || publish; - List<Content> contents; - - if (lectureQuestionsOnly) { - contents = contentService.getLectureQuestions(sessionkey); - contentService.publishQuestions(sessionkey, p, contents); - } else if (preparationQuestionsOnly) { - contents = contentService.getPreparationQuestions(sessionkey); - contentService.publishQuestions(sessionkey, p, contents); - } else { - contentService.publishAll(sessionkey, p); - } - } - - @ApiOperation(value = "Publish statistics from content with provided id", - nickname = "publishStatistics") - @RequestMapping(value = "/{questionId}/publishstatistics", method = RequestMethod.POST) - public void publishStatistics( - @PathVariable final String questionId, - @RequestParam(required = false) final Boolean showStatistics, - @RequestBody final Content content - ) { - if (showStatistics != null) { - content.setShowStatistic(showStatistics); - } - contentService.update(content); - } - - @ApiOperation(value = "Publish correct answer from content with provided id", - nickname = "publishCorrectAnswer") - @RequestMapping(value = "/{questionId}/publishcorrectanswer", method = RequestMethod.POST) - public void publishCorrectAnswer( - @PathVariable final String questionId, - @RequestParam(required = false) final Boolean showCorrectAnswer, - @RequestBody final Content content - ) { - if (showCorrectAnswer != null) { - content.setShowAnswer(showCorrectAnswer); - } - contentService.update(content); - } +@RequestMapping(ContentController.REQUEST_MAPPING) +public class ContentController extends AbstractEntityController<Content> { + protected static final String REQUEST_MAPPING = "/content"; + private static final String GET_ANSWER_STATISTICS_MAPPING = DEFAULT_ID_MAPPING + "/stats"; - @ApiOperation(value = "Get skill questions", - nickname = "getSkillQuestions") - @RequestMapping(value = "/", method = RequestMethod.GET) - @Pagination - public List<Content> getSkillQuestions( - @RequestParam final String sessionkey, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") final boolean lectureQuestionsOnly, - @RequestParam(value = "flashcardsonly", defaultValue = "false") final boolean flashcardsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") final boolean preparationQuestionsOnly, - @RequestParam(value = "requestImageData", defaultValue = "false") final boolean requestImageData, - final HttpServletResponse response - ) { - List<Content> contents; - if (lectureQuestionsOnly) { - contents = contentService.getLectureQuestions(sessionkey); - } else if (flashcardsOnly) { - contents = contentService.getFlashcards(sessionkey); - } else if (preparationQuestionsOnly) { - contents = contentService.getPreparationQuestions(sessionkey); - } else { - contents = contentService.getBySessionKey(sessionkey); - } - if (contents == null || contents.isEmpty()) { - response.setStatus(HttpStatus.NO_CONTENT.value()); - return null; - } else if (!requestImageData) { - contents = contentService.replaceImageData(contents); - } - - return new PaginationListDecorator<>(contents, offset, limit); - } - - @ApiOperation(value = "Delete skill questions", - nickname = "deleteSkillQuestions") - @RequestMapping(value = { "/" }, method = RequestMethod.DELETE) - public void deleteSkillQuestions( - @RequestParam final String sessionkey, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") final boolean lectureQuestionsOnly, - @RequestParam(value = "flashcardsonly", defaultValue = "false") final boolean flashcardsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") final boolean preparationQuestionsOnly, - final HttpServletResponse response - ) { - if (lectureQuestionsOnly) { - contentService.deleteLectureQuestions(sessionkey); - } else if (preparationQuestionsOnly) { - contentService.deletePreparationQuestions(sessionkey); - } else if (flashcardsOnly) { - contentService.deleteFlashcards(sessionkey); - } else { - contentService.deleteAllContent(sessionkey); - } - } - - @ApiOperation(value = "Get the amount of skill questions by the sessionkey", - nickname = "getSkillQuestionCount") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/count", method = RequestMethod.GET) - public int getSkillQuestionCount( - @RequestParam final String sessionkey, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") final boolean lectureQuestionsOnly, - @RequestParam(value = "flashcardsonly", defaultValue = "false") final boolean flashcardsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") final boolean preparationQuestionsOnly - ) { - if (lectureQuestionsOnly) { - return contentService.countLectureQuestions(sessionkey); - } else if (preparationQuestionsOnly) { - return contentService.countPreparationQuestions(sessionkey); - } else if (flashcardsOnly) { - return contentService.countFlashcards(sessionkey); - } else { - return contentService.countBySessionKey(sessionkey); - } - } - - @ApiOperation(value = "Delete answers and questions", - nickname = "deleteAnswersAndQuestion") - @RequestMapping(value = "/{questionId}", method = RequestMethod.DELETE) - public void deleteAnswersAndQuestion( - @PathVariable final String questionId - ) { - contentService.delete(questionId); - } - - @ApiOperation(value = "Get unanswered skill question ID by provided session ID", - nickname = "getUnAnsweredSkillQuestionIds") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/unanswered", method = RequestMethod.GET) - public List<String> getUnAnsweredSkillQuestionIds( - @RequestParam final String sessionkey, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") final boolean lectureQuestionsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") final boolean preparationQuestionsOnly - ) { - List<String> answers; - if (lectureQuestionsOnly) { - answers = contentService.getUnAnsweredLectureQuestionIds(sessionkey); - } else if (preparationQuestionsOnly) { - answers = contentService.getUnAnsweredPreparationQuestionIds(sessionkey); - } else { - answers = contentService.getUnAnsweredQuestionIds(sessionkey); - } - if (answers == null || answers.isEmpty()) { - throw new NoContentException(); - } - - return answers; - } - - /** - * returns a JSON document which represents the given answer of a question. - * - * @param questionId - * CouchDB Content ID for which the given answer should be - * retrieved - * @return JSON Document of {@link Answer} or {@link NotFoundException} - * @throws NotFoundException - * if wrong session, wrong question or no answer was given by - * the current user - * @throws ForbiddenException - * if not logged in - */ - @ApiOperation(value = "Get my answer for a question, identified by provided question ID", - nickname = "getMyAnswer") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/{questionId}/myanswer", method = RequestMethod.GET) - public Answer getMyAnswer( - @PathVariable final String questionId, - final HttpServletResponse response - ) { - final Answer answer = contentService.getMyAnswer(questionId); - if (answer == null) { - response.setStatus(HttpStatus.NO_CONTENT.value()); - return null; - } - - return answer; - } - - /** - * returns a list of {@link Answer}s encoded as a JSON document for a given - * question id. In this case only {@link Answer} <tt>questionId</tt>, - * <tt>answerText</tt>, <tt>answerSubject</tt> and <tt>answerCount</tt> - * properties are set - * - * @param questionId - * CouchDB Content ID for which the given answers should be - * retrieved - * @throws NotFoundException - * if wrong session, wrong question or no answers was given - * @throws ForbiddenException - * if not logged in - */ - @ApiOperation(value = "Get answers for a question, identified by provided question ID", - nickname = "getAnswers") - @RequestMapping(value = "/{questionId}/answer/", method = RequestMethod.GET) - public List<Answer> getAnswers( - @PathVariable final String questionId, - @RequestParam(value = "piround", required = false) final Integer piRound, - @RequestParam(value = "all", required = false, defaultValue = "false") final Boolean allAnswers, - final HttpServletResponse response - ) { - List<Answer> answers; - if (allAnswers) { - answers = contentService.getAllAnswers(questionId, -1, -1); - } else if (null == piRound) { - answers = contentService.getAnswers(questionId, offset, limit); - } else { - if (piRound < 1 || piRound > 2) { - response.setStatus(HttpStatus.BAD_REQUEST.value()); - - return null; - } - answers = contentService.getAnswers(questionId, piRound, offset, limit); - } - if (answers == null) { - return new ArrayList<>(); - } - return answers; - } - - @ApiOperation(value = "Save answer, provided in the Request Body, for a question, identified by provided question ID", - nickname = "saveAnswer") - @RequestMapping(value = "/{questionId}/answer/", method = RequestMethod.POST) - public Answer saveAnswer( - @PathVariable final String questionId, - @RequestBody final Answer answer, - final HttpServletResponse response - ) { - return contentService.saveAnswer(questionId, answer); - } - - @ApiOperation(value = "Update answer, provided in Request Body, identified by question ID and answer ID", - nickname = "updateAnswer") - @RequestMapping(value = "/{questionId}/answer/{answerId}", method = RequestMethod.PUT) - public Answer updateAnswer( - @PathVariable final String questionId, - @PathVariable final String answerId, - @RequestBody final Answer answer, - final HttpServletResponse response - ) { - return contentService.updateAnswer(answer); - } - - @ApiOperation(value = "Get Image, identified by question ID and answer ID", - nickname = "getImage") - @RequestMapping(value = "/{questionId}/answer/{answerId}/image", method = RequestMethod.GET) - public String getImage( - @PathVariable final String questionId, - @PathVariable final String answerId, - final HttpServletResponse response - ) { - - return contentService.getImage(questionId, answerId); - } - - @ApiOperation(value = "Delete answer, identified by question ID and answer ID", - nickname = "deleteAnswer") - @RequestMapping(value = "/{questionId}/answer/{answerId}", method = RequestMethod.DELETE) - public void deleteAnswer( - @PathVariable final String questionId, - @PathVariable final String answerId, - final HttpServletResponse response - ) { - contentService.deleteAnswer(questionId, answerId); - } - - @ApiOperation(value = "Delete answers from a question, identified by question ID", - nickname = "deleteAnswers") - @RequestMapping(value = "/{questionId}/answer/", method = RequestMethod.DELETE) - public void deleteAnswers( - @PathVariable final String questionId, - final HttpServletResponse response - ) { - contentService.deleteAnswers(questionId); - } - - @ApiOperation(value = "Delete all answers and questions from a session, identified by sessionkey", - nickname = "deleteAllQuestionsAnswers") - @RequestMapping(value = "/answers", method = RequestMethod.DELETE) - public void deleteAllQuestionsAnswers( - @RequestParam final String sessionkey, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") final boolean lectureQuestionsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") final boolean preparationQuestionsOnly, - final HttpServletResponse response - ) { - if (lectureQuestionsOnly) { - contentService.deleteAllLectureAnswers(sessionkey); - } else if (preparationQuestionsOnly) { - contentService.deleteAllPreparationAnswers(sessionkey); - } else { - contentService.deleteAllQuestionsAnswers(sessionkey); - } - } - - /** - * - * @param questionId - * CouchDB Content ID for which the given answers should be - * retrieved - * @return count of answers for given question id - * @throws NotFoundException - * if wrong session or wrong question - * @throws ForbiddenException - * if not logged in - */ - @ApiOperation(value = "Get the amount of answers for a question, identified by question ID", - nickname = "getAnswerCount") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/{questionId}/answercount", method = RequestMethod.GET) - public int getAnswerCount(@PathVariable final String questionId) { - return contentService.countAnswersByQuestionIdAndRound(questionId); - } - - @ApiOperation(value = "Get the amount of answers for a question, identified by the question ID", - nickname = "getAllAnswerCount") - @RequestMapping(value = "/{questionId}/allroundanswercount", method = RequestMethod.GET) - public List<Integer> getAllAnswerCount(@PathVariable final String questionId) { - return Arrays.asList( - contentService.countAnswersByQuestionIdAndRound(questionId, 1), - contentService.countAnswersByQuestionIdAndRound(questionId, 2) - ); - } - - @ApiOperation(value = "Get the total amount of answers by a question, identified by the question ID", - nickname = "getTotalAnswerCountByQuestion") - @RequestMapping(value = "/{questionId}/totalanswercount", method = RequestMethod.GET) - public int getTotalAnswerCountByQuestion(@PathVariable final String questionId) { - return contentService.countTotalAnswersByQuestionId(questionId); - } - - @ApiOperation(value = "Get the amount of answers and abstention answers by a question, identified by the question ID", - nickname = "getAnswerAndAbstentionCount") - @RequestMapping(value = "/{questionId}/answerandabstentioncount", method = RequestMethod.GET) - public List<Integer> getAnswerAndAbstentionCount(@PathVariable final String questionId) { - return Arrays.asList( - contentService.countAnswersByQuestionIdAndRound(questionId), - contentService.countTotalAbstentionsByQuestionId(questionId) - ); - } + private ContentService contentService; + private AnswerService answerService; - @ApiOperation(value = "Get all Freetext answers by a question, identified by the question ID", - nickname = "getFreetextAnswers") - @RequestMapping(value = "/{questionId}/freetextanswer/", method = RequestMethod.GET) - @Pagination - public List<Answer> getFreetextAnswers(@PathVariable final String questionId) { - return contentService.getFreetextAnswersByQuestionId(questionId, offset, limit); + public ContentController(final ContentService contentService, final AnswerService answerService) { + super(contentService); + this.contentService = contentService; + this.answerService = answerService; } - @ApiOperation(value = "Get my answers of an session, identified by the sessionkey", - nickname = "getMyAnswers") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/myanswers", method = RequestMethod.GET) - public List<Answer> getMyAnswers(@RequestParam final String sessionkey) { - return contentService.getMyAnswersBySessionKey(sessionkey); + @Override + protected String getMapping() { + return REQUEST_MAPPING; } - @ApiOperation(value = "Get the total amount of answers of an session, identified by the sessionkey", - nickname = "getTotalAnswerCount") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/answercount", method = RequestMethod.GET) - public int getTotalAnswerCount( - @RequestParam final String sessionkey, - @RequestParam(value = "lecturequestionsonly", defaultValue = "false") final boolean lectureQuestionsOnly, - @RequestParam(value = "preparationquestionsonly", defaultValue = "false") final boolean preparationQuestionsOnly - ) { - if (lectureQuestionsOnly) { - return contentService.countLectureQuestionAnswers(sessionkey); - } else if (preparationQuestionsOnly) { - return contentService.countPreparationQuestionAnswers(sessionkey); - } else { - return contentService.countTotalAnswersBySessionKey(sessionkey); - } + @GetMapping(GET_ANSWER_STATISTICS_MAPPING) + public AnswerStatistics getAnswerStatistics(@PathVariable final String id) { + return answerService.getAllStatistics(id); } } diff --git a/src/main/java/de/thm/arsnova/controller/ControllerExceptionHandler.java b/src/main/java/de/thm/arsnova/controller/ControllerExceptionHandler.java index 31d43c4cbf6824510d38fa56dcca1a841c72d5a8..2016f8defbb7abb9c8ed6eb219905823d625351e 100644 --- a/src/main/java/de/thm/arsnova/controller/ControllerExceptionHandler.java +++ b/src/main/java/de/thm/arsnova/controller/ControllerExceptionHandler.java @@ -17,14 +17,15 @@ */ package de.thm.arsnova.controller; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.exceptions.ForbiddenException; -import de.thm.arsnova.exceptions.NoContentException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.exceptions.NotImplementedException; -import de.thm.arsnova.exceptions.PayloadTooLargeException; -import de.thm.arsnova.exceptions.PreconditionFailedException; -import de.thm.arsnova.exceptions.UnauthorizedException; +import de.thm.arsnova.web.exceptions.BadRequestException; +import de.thm.arsnova.web.exceptions.ForbiddenException; +import de.thm.arsnova.web.exceptions.NoContentException; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.web.exceptions.NotImplementedException; +import de.thm.arsnova.web.exceptions.PayloadTooLargeException; +import de.thm.arsnova.web.exceptions.PreconditionFailedException; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import org.ektorp.DocumentNotFoundException; import org.slf4j.event.Level; import org.springframework.http.HttpStatus; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -33,12 +34,14 @@ import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.NoHandlerFoundException; +import javax.naming.OperationNotSupportedException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Map; @@ -109,7 +112,7 @@ public class ControllerExceptionHandler extends AbstractControllerExceptionHandl return handleException(e, Level.DEBUG); } - @ExceptionHandler(BadRequestException.class) + @ExceptionHandler({BadRequestException.class, HttpRequestMethodNotSupportedException.class}) @ResponseBody @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, Object> handleBadRequestException(final Exception e, final HttpServletRequest request) { @@ -123,7 +126,7 @@ public class ControllerExceptionHandler extends AbstractControllerExceptionHandl return handleException(e, Level.DEBUG); } - @ExceptionHandler(NotImplementedException.class) + @ExceptionHandler({NotImplementedException.class, OperationNotSupportedException.class}) @ResponseBody @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) public Map<String, Object> handleNotImplementedException(final Exception e, final HttpServletRequest request) { @@ -143,4 +146,12 @@ public class ControllerExceptionHandler extends AbstractControllerExceptionHandl public Map<String, Object> handleHttpMessageNotReadableException(final Exception e, final HttpServletRequest request) { return handleException(e, Level.DEBUG); } + + /* FIXME: Wrap persistance Exceptions - do not handle persistance Exceptions at the controller layer */ + @ExceptionHandler(DocumentNotFoundException.class) + @ResponseBody + @ResponseStatus(HttpStatus.NOT_FOUND) + public Map<String, Object> handleDocumentNotFoundException(final Exception e, final HttpServletRequest request) { + return handleException(e, Level.TRACE); + } } diff --git a/src/main/java/de/thm/arsnova/controller/FeedbackController.java b/src/main/java/de/thm/arsnova/controller/FeedbackController.java deleted file mode 100644 index 5838b6f1cfb0ec270299c701783974890062c3fc..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/controller/FeedbackController.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.controller; - -import de.thm.arsnova.entities.Feedback; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.services.FeedbackService; -import de.thm.arsnova.services.UserService; -import de.thm.arsnova.websocket.ArsnovaSocketioServerImpl; -import de.thm.arsnova.web.DeprecatedApi; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -/** - * Handles requests concerning the user's feedback, i.e., "too fast" or "faster, please". This HTTP API is - * deprecated in favor of the socket implementation. - * - * @see ArsnovaSocketioServerImpl - */ -@RestController -public class FeedbackController extends AbstractController { - @Autowired - private FeedbackService feedbackService; - - @Autowired - private UserService userService; - - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/session/{sessionkey}/feedback", method = RequestMethod.GET) - public Feedback getFeedback(@PathVariable final String sessionkey) { - return feedbackService.getBySessionKey(sessionkey); - } - - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/session/{sessionkey}/myfeedback", method = RequestMethod.GET) - public Integer getMyFeedback(@PathVariable final String sessionkey) { - Integer value = feedbackService.getBySessionKeyAndUser(sessionkey, userService.getCurrentUser()); - if (value != null && value >= Feedback.MIN_FEEDBACK_TYPE && value <= Feedback.MAX_FEEDBACK_TYPE) { - return value; - } - throw new NotFoundException(); - } - - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/session/{sessionkey}/feedbackcount", method = RequestMethod.GET) - public int getFeedbackCount(@PathVariable final String sessionkey) { - return feedbackService.countFeedbackBySessionKey(sessionkey); - } - - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/session/{sessionkey}/roundedaveragefeedback", method = RequestMethod.GET) - public long getAverageFeedbackRounded(@PathVariable final String sessionkey) { - return feedbackService.calculateRoundedAverageFeedback(sessionkey); - } - - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/session/{sessionkey}/averagefeedback", method = RequestMethod.GET) - public double getAverageFeedback(@PathVariable final String sessionkey) { - return feedbackService.calculateAverageFeedback(sessionkey); - } - - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/session/{sessionkey}/feedback", method = RequestMethod.POST) - @ResponseStatus(HttpStatus.CREATED) - public Feedback postFeedback( - @PathVariable final String sessionkey, - @RequestBody final int value - ) { - User user = userService.getCurrentUser(); - feedbackService.save(sessionkey, value, user); - Feedback feedback = feedbackService.getBySessionKey(sessionkey); - - return feedback; - } -} diff --git a/src/main/java/de/thm/arsnova/controller/LegacyController.java b/src/main/java/de/thm/arsnova/controller/LegacyController.java deleted file mode 100644 index afe6bb051d02d09bb52e301cb3a8f0ff3bb8f16c..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/controller/LegacyController.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.controller; - -import de.thm.arsnova.services.CommentService; -import de.thm.arsnova.services.ContentService; -import de.thm.arsnova.web.DeprecatedApi; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.ResponseBody; - -/** - * This controller forwards requests from deprecated URLs to their new controller, where the requests are handled. - */ -@Controller -public class LegacyController extends AbstractController { - - @Autowired - private ContentService contentService; - - @Autowired - private CommentService commentService; - - /* specific routes */ - - @DeprecatedApi - @RequestMapping(value = "/session/mysessions", method = RequestMethod.GET) - public String redirectSessionMy() { - return "forward:/session/?ownedonly=true"; - } - - @DeprecatedApi - @RequestMapping(value = "/session/visitedsessions", method = RequestMethod.GET) - public String redirectSessionVisited() { - return "forward:/session/?visitedonly=true"; - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/question") - public String redirectQuestionByLecturer(@PathVariable final String sessionKey) { - return String.format("forward:/lecturerquestion/?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/skillquestions", method = RequestMethod.GET) - public String redirectQuestionByLecturerList(@PathVariable final String sessionKey) { - return String.format("forward:/lecturerquestion/?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/skillquestioncount", method = RequestMethod.GET) - public String redirectQuestionByLecturerCount(@PathVariable final String sessionKey) { - return String.format("forward:/lecturerquestion/count?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/answercount", method = RequestMethod.GET) - public String redirectQuestionByLecturerAnswerCount(@PathVariable final String sessionKey) { - return String.format("forward:/lecturerquestion/answercount?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/unanswered", method = RequestMethod.GET) - public String redirectQuestionByLecturerUnnsweredCount(@PathVariable final String sessionKey) { - return String.format("forward:/lecturerquestion/answercount?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/myanswers", method = RequestMethod.GET) - public String redirectQuestionByLecturerMyAnswers(@PathVariable final String sessionKey) { - return String.format("forward:/lecturerquestion/myanswers?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/interposed") - public String redirectQuestionByAudience(@PathVariable final String sessionKey) { - return String.format("forward:/audiencequestion/?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/interposed", method = RequestMethod.DELETE) - @ResponseBody - public void deleteAllInterposedQuestions(@PathVariable final String sessionKey) { - commentService.deleteBySessionKey(sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/interposedcount", method = RequestMethod.GET) - public String redirectQuestionByAudienceCount(@PathVariable final String sessionKey) { - return String.format("forward:/audiencequestion/count?sessionkey=%s", sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/interposedreadingcount", method = RequestMethod.GET) - public String redirectQuestionByAudienceReadCount(@PathVariable final String sessionKey) { - return String.format("forward:/audiencequestion/readcount?sessionkey=%s", sessionKey); - } - - /* generalized routes */ - - @DeprecatedApi - @RequestMapping(value = { "/session/{sessionKey}/question/{arg1}", "/session/{sessionKey}/questions/{arg1}" }) - public String redirectQuestionByLecturerWithOneArgument( - @PathVariable final String sessionKey, - @PathVariable final String arg1 - ) { - return String.format("forward:/lecturerquestion/%s/?sessionkey=%s", arg1, sessionKey); - } - - @DeprecatedApi - @RequestMapping( - value = { "/session/{sessionKey}/question/{arg1}/{arg2}", "/session/{sessionKey}/questions/{arg1}/{arg2}" } - ) - public String redirectQuestionByLecturerWithTwoArguments( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - @PathVariable final String arg2 - ) { - return String.format("forward:/lecturerquestion/%s/%s/?sessionkey=%s", arg1, arg2, sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/interposed/{arg1}") - public String redirectQuestionByAudienceWithOneArgument( - @PathVariable final String sessionKey, - @PathVariable final String arg1 - ) { - return String.format("forward:/audiencequestion/%s/?sessionkey=%s", arg1, sessionKey); - } - - @DeprecatedApi - @RequestMapping(value = "/session/{sessionKey}/interposed/{arg1}/{arg2}") - public String redirectQuestionByAudienceWithTwoArguments( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - @PathVariable final String arg2 - ) { - return String.format("forward:/audiencequestion/%s/%s/?sessionkey=%s", arg1, arg2, sessionKey); - } -} diff --git a/src/main/java/de/thm/arsnova/controller/MotdController.java b/src/main/java/de/thm/arsnova/controller/MotdController.java index 3cf9569f781e5df352319a69ecc75698a41d2263..6b842ee8157f8c1384dd9013be02034548ea6bbc 100644 --- a/src/main/java/de/thm/arsnova/controller/MotdController.java +++ b/src/main/java/de/thm/arsnova/controller/MotdController.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2018 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,141 +17,25 @@ */ package de.thm.arsnova.controller; -import de.thm.arsnova.entities.Motd; -import de.thm.arsnova.entities.MotdList; -import de.thm.arsnova.services.MotdService; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; +import de.thm.arsnova.model.Motd; +import de.thm.arsnova.service.MotdService; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletResponse; -import java.util.Date; -import java.util.List; - -/** - * - */ @RestController -@RequestMapping("/motd") -@Api(value = "/motd", description = "the Motd Controller API") -public class MotdController extends AbstractController { +@RequestMapping(MotdController.REQUEST_MAPPING) +public class MotdController extends AbstractEntityController<Motd> { + protected static final String REQUEST_MAPPING = "/motd"; - @Autowired private MotdService motdService; - @ApiOperation(value = "get messages. if adminview=false, only messages with startdate<clientdate<enddate are returned") - @RequestMapping(value = "/", method = RequestMethod.GET) - @ApiResponses(value = { - @ApiResponse(code = 204, message = HTML_STATUS_204), - @ApiResponse(code = 501, message = HTML_STATUS_501) - }) - public List<Motd> getMotd( - @ApiParam(value = "clientdate", required = false) @RequestParam(value = "clientdate", defaultValue = "") final String clientdate, - @ApiParam(value = "adminview", required = false) @RequestParam(value = "adminview", defaultValue = "false") final Boolean adminview, - @ApiParam(value = "audience", required = false) @RequestParam(value = "audience", defaultValue = "all") final String audience, - @ApiParam(value = "sessionkey", required = false) @RequestParam(value = "sessionkey", defaultValue = "null") final String sessionkey - ) { - List<Motd> motds; - Date date = new Date(System.currentTimeMillis()); - if (!clientdate.isEmpty()) { - date.setTime(Long.parseLong(clientdate)); - } - if (adminview) { - motds = "session".equals(audience) ? - motdService.getAllSessionMotds(sessionkey) : - motdService.getAdminMotds(); - } else { - motds = "session".equals(audience) ? - motdService.getCurrentSessionMotds(date, sessionkey) : - motdService.getCurrentMotds(date, audience); - } - return motds; - } - - @ApiOperation(value = "create a new message of the day", nickname = "createMotd") - @ApiResponses(value = { - @ApiResponse(code = 201, message = HTML_STATUS_201), - @ApiResponse(code = 503, message = HTML_STATUS_503) - }) - @RequestMapping(value = "/", method = RequestMethod.POST) - @ResponseStatus(HttpStatus.CREATED) - public Motd postNewMotd( - @ApiParam(value = "current motd", required = true) @RequestBody final Motd motd, - final HttpServletResponse response - ) { - if (motd != null) { - Motd newMotd; - if ("session".equals(motd.getAudience()) && motd.getSessionkey() != null) { - newMotd = motdService.save(motd.getSessionkey(), motd); - } else { - newMotd = motdService.save(motd); - } - if (newMotd == null) { - response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value()); - return null; - } - return newMotd; - } else { - response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value()); - return null; - } - } - - @ApiOperation(value = "update a message of the day", nickname = "updateMotd") - @RequestMapping(value = "/{motdkey}", method = RequestMethod.PUT) - public Motd updateMotd( - @ApiParam(value = "motdkey from current motd", required = true) @PathVariable final String motdkey, - @ApiParam(value = "current motd", required = true) @RequestBody final Motd motd - ) { - if ("session".equals(motd.getAudience()) && motd.getSessionkey() != null) { - return motdService.update(motd.getSessionkey(), motd); - } else { - return motdService.update(motd); - } - } - - @ApiOperation(value = "deletes a message of the day", nickname = "deleteMotd") - @RequestMapping(value = "/{motdkey}", method = RequestMethod.DELETE) - public void deleteMotd(@ApiParam(value = "Motd-key from the message that shall be deleted", required = true) @PathVariable final String motdkey) { - Motd motd = motdService.getByKey(motdkey); - if ("session".equals(motd.getAudience())) { - motdService.deleteBySessionKey(motd.getSessionkey(), motd); - } else { - motdService.delete(motd); - } - } - - @ApiOperation(value = "get a list of the motdkeys the current user has confirmed to be read") - @RequestMapping(value = "/userlist", method = RequestMethod.GET) - public MotdList getUserMotdList( - @ApiParam(value = "users name", required = true) @RequestParam(value = "username", defaultValue = "null", required = true) final String username) { - return motdService.getMotdListByUsername(username); - } - - @ApiOperation(value = "create a list of the motdkeys the current user has confirmed to be read") - @RequestMapping(value = "/userlist", method = RequestMethod.POST) - public MotdList postUserMotdList( - @ApiParam(value = "current motdlist", required = true) @RequestBody final MotdList userMotdList - ) { - return motdService.saveMotdList(userMotdList); + public MotdController(final MotdService motdService) { + super(motdService); + this.motdService = motdService; } - @ApiOperation(value = "update a list of the motdkeys the current user has confirmed to be read") - @RequestMapping(value = "/userlist", method = RequestMethod.PUT) - public MotdList updateUserMotdList( - @ApiParam(value = "current motdlist", required = true) @RequestBody final MotdList userMotdList - ) { - return motdService.updateMotdList(userMotdList); + @Override + protected String getMapping() { + return REQUEST_MAPPING; } } diff --git a/src/main/java/de/thm/arsnova/controller/RoomController.java b/src/main/java/de/thm/arsnova/controller/RoomController.java new file mode 100644 index 0000000000000000000000000000000000000000..db38d8a3d3aabc7fa294d2055a40188c9b078319 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/RoomController.java @@ -0,0 +1,46 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller; + +import de.thm.arsnova.model.Room; +import de.thm.arsnova.service.RoomService; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(RoomController.REQUEST_MAPPING) +public class RoomController extends AbstractEntityController<Room> { + protected static final String REQUEST_MAPPING = "/room"; + + private RoomService roomService; + + public RoomController(final RoomService roomService) { + super(roomService); + this.roomService = roomService; + } + + @Override + protected String getMapping() { + return REQUEST_MAPPING; + } + + @Override + protected String resolveAlias(final String shortId) { + return roomService.getIdByShortId(shortId); + } +} diff --git a/src/main/java/de/thm/arsnova/controller/SessionController.java b/src/main/java/de/thm/arsnova/controller/SessionController.java deleted file mode 100644 index ac95db32f9837ed4e8b1de4d1de9f62f2534337b..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/controller/SessionController.java +++ /dev/null @@ -1,509 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.controller; - -import de.thm.arsnova.connector.model.Course; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.SessionFeature; -import de.thm.arsnova.entities.SessionInfo; -import de.thm.arsnova.entities.transport.ImportExportSession; -import de.thm.arsnova.entities.transport.ScoreStatistics; -import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.services.SessionService; -import de.thm.arsnova.services.UserService; -import de.thm.arsnova.services.SessionServiceImpl.SessionInfoNameComparator; -import de.thm.arsnova.services.SessionServiceImpl.SessionInfoShortNameComparator; -import de.thm.arsnova.services.SessionServiceImpl.SessionNameComparator; -import de.thm.arsnova.services.SessionServiceImpl.SessionShortNameComparator; -import de.thm.arsnova.web.DeprecatedApi; -import de.thm.arsnova.web.Pagination; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; -import io.swagger.annotations.ApiResponse; -import io.swagger.annotations.ApiResponses; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.security.access.AccessDeniedException; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; - -import javax.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Handles requests related to ARSnova sessions. - */ -@RestController -@RequestMapping("/session") -@Api(value = "/session", description = "the Session Controller API") -public class SessionController extends PaginationController { - @Autowired - private SessionService sessionService; - - @Autowired - private UserService userService; - - @ApiOperation(value = "join a session", - nickname = "joinSession") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/{sessionkey}", method = RequestMethod.GET) - public Session joinSession( - @ApiParam(value = "Session-Key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "Adminflag", required = false) @RequestParam(value = "admin", defaultValue = "false") final boolean admin - ) { - if (admin) { - return sessionService.getForAdmin(sessionkey); - } else { - return sessionService.getByKey(sessionkey); - } - } - - @ApiOperation(value = "deletes a session", - nickname = "deleteSession") - @RequestMapping(value = "/{sessionkey}", method = RequestMethod.DELETE) - public void deleteSession(@ApiParam(value = "Session-Key from current session", required = true) @PathVariable final String sessionkey) { - Session session = sessionService.getByKey(sessionkey); - sessionService.deleteCascading(session); - } - - @ApiOperation(value = "count active users", - nickname = "countActiveUsers") - @DeprecatedApi - @Deprecated - @RequestMapping(value = "/{sessionkey}/activeusercount", method = RequestMethod.GET) - public int countActiveUsers(@ApiParam(value = "Session-Key from current session", required = true) @PathVariable final String sessionkey) { - return sessionService.activeUsers(sessionkey); - } - - @ApiOperation(value = "Creates a new Session and returns the Session's data", - nickname = "postNewSession") - @ApiResponses(value = { - @ApiResponse(code = 201, message = HTML_STATUS_201), - @ApiResponse(code = 503, message = HTML_STATUS_503) - }) - @RequestMapping(value = "/", method = RequestMethod.POST) - @ResponseStatus(HttpStatus.CREATED) - public Session postNewSession(@ApiParam(value = "current session", required = true) @RequestBody final Session session, final HttpServletResponse response) { - if (session != null && session.isCourseSession()) { - final List<Course> courses = new ArrayList<>(); - final Course course = new Course(); - course.setId(session.getCourseId()); - courses.add(course); - final int sessionCount = sessionService.countSessionsByCourses(courses); - if (sessionCount > 0) { - final String appendix = " (" + (sessionCount + 1) + ")"; - session.setName(session.getName() + appendix); - session.setShortName(session.getShortName() + appendix); - } - } - - final Session newSession = sessionService.save(session); - - if (newSession == null) { - response.setStatus(HttpStatus.SERVICE_UNAVAILABLE.value()); - return null; - } - - return newSession; - } - - @ApiOperation(value = "updates a session", - nickname = "postNewSession") - @RequestMapping(value = "/{sessionkey}", method = RequestMethod.PUT) - public Session updateSession( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "current session", required = true) @RequestBody final Session session - ) { - return sessionService.update(sessionkey, session); - } - - @ApiOperation(value = "change the session creator (owner)", nickname = "changeSessionCreator") - @RequestMapping(value = "/{sessionkey}/changecreator", method = RequestMethod.PUT) - public Session changeSessionCreator( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "new session creator", required = true) @RequestBody final String newCreator - ) { - return sessionService.updateCreator(sessionkey, newCreator); - } - - @ApiOperation(value = "Retrieves a list of Sessions", - nickname = "getSessions") - @ApiResponses(value = { - @ApiResponse(code = 204, message = HTML_STATUS_204), - @ApiResponse(code = 501, message = HTML_STATUS_501) - }) - @RequestMapping(value = "/", method = RequestMethod.GET) - @Pagination - public List<Session> getSessions( - @ApiParam(value = "ownedOnly", required = true) @RequestParam(value = "ownedonly", defaultValue = "false") final boolean ownedOnly, - @ApiParam(value = "visitedOnly", required = true) @RequestParam(value = "visitedonly", defaultValue = "false") final boolean visitedOnly, - @ApiParam(value = "sortby", required = true) @RequestParam(value = "sortby", defaultValue = "name") final String sortby, - @ApiParam(value = "for a given username. admin rights needed", required = false) @RequestParam(value = - "username", defaultValue = "") final String username, - final HttpServletResponse response - ) { - List<Session> sessions; - - if (!"".equals(username)) { - try { - if (ownedOnly && !visitedOnly) { - sessions = sessionService.getUserSessions(username); - } else if (visitedOnly && !ownedOnly) { - sessions = sessionService.getUserVisitedSessions(username); - } else { - response.setStatus(HttpStatus.NOT_IMPLEMENTED.value()); - return null; - } - } catch (final AccessDeniedException e) { - throw new UnauthorizedException(); - } - } else { - /* TODO implement all parameter combinations, implement use of user parameter */ - try { - if (ownedOnly && !visitedOnly) { - sessions = sessionService.getMySessions(offset, limit); - } else if (visitedOnly && !ownedOnly) { - sessions = sessionService.getMyVisitedSessions(offset, limit); - } else { - response.setStatus(HttpStatus.NOT_IMPLEMENTED.value()); - return null; - } - } catch (final AccessDeniedException e) { - throw new UnauthorizedException(); - } - } - - if (sessions == null || sessions.isEmpty()) { - response.setStatus(HttpServletResponse.SC_NO_CONTENT); - return null; - } - - if ("shortname".equals(sortby)) { - Collections.sort(sessions, new SessionShortNameComparator()); - } else { - Collections.sort(sessions, new SessionNameComparator()); - } - - return sessions; - } - - /** - * Returns a list of my own sessions with only the necessary information like name, keyword, or counters. - */ - @ApiOperation(value = "Retrieves a Session", - nickname = "getMySessions") - @ApiResponses(value = { - @ApiResponse(code = 204, message = HTML_STATUS_204) - }) - @RequestMapping(value = "/", method = RequestMethod.GET, params = "statusonly=true") - @Pagination - public List<SessionInfo> getMySessions( - @ApiParam(value = "visitedOnly", required = true) @RequestParam(value = "visitedonly", defaultValue = "false") final boolean visitedOnly, - @ApiParam(value = "sort by", required = false) @RequestParam(value = "sortby", defaultValue = "name") final String sortby, - final HttpServletResponse response - ) { - List<SessionInfo> sessions; - if (!visitedOnly) { - sessions = sessionService.getMySessionsInfo(offset, limit); - } else { - sessions = sessionService.getMyVisitedSessionsInfo(offset, limit); - } - - if (sessions == null || sessions.isEmpty()) { - response.setStatus(HttpServletResponse.SC_NO_CONTENT); - return null; - } - - if ("shortname".equals(sortby)) { - Collections.sort(sessions, new SessionInfoShortNameComparator()); - } else { - Collections.sort(sessions, new SessionInfoNameComparator()); - } - return sessions; - } - - @ApiOperation(value = "Retrieves all public pool sessions for the current user", - nickname = "getMyPublicPoolSessions") - @ApiResponses(value = { - @ApiResponse(code = 204, message = HTML_STATUS_204) - }) - @RequestMapping(value = "/publicpool", method = RequestMethod.GET, params = "statusonly=true") - public List<SessionInfo> getMyPublicPoolSessions( - final HttpServletResponse response - ) { - List<SessionInfo> sessions = sessionService.getMyPublicPoolSessionsInfo(); - - if (sessions == null || sessions.isEmpty()) { - response.setStatus(HttpServletResponse.SC_NO_CONTENT); - return null; - } - - return sessions; - } - - @ApiOperation(value = "Retrieves all public pool sessions", - nickname = "getMyPublicPoolSessions") - @ApiResponses(value = { - @ApiResponse(code = 204, message = HTML_STATUS_204) - }) - @RequestMapping(value = "/publicpool", method = RequestMethod.GET) - public List<SessionInfo> getPublicPoolSessions( - final HttpServletResponse response - ) { - List<SessionInfo> sessions = sessionService.getPublicPoolSessionsInfo(); - - if (sessions == null || sessions.isEmpty()) { - response.setStatus(HttpServletResponse.SC_NO_CONTENT); - return null; - } - - return sessions; - } - - @ApiOperation(value = "imports a session", - nickname = "importSession") - @RequestMapping(value = "/import", method = RequestMethod.POST) - public SessionInfo importSession( - @ApiParam(value = "current session", required = true) @RequestBody final ImportExportSession session, - final HttpServletResponse response - ) { - return sessionService.importSession(session); - } - - @ApiOperation(value = "export sessions", nickname = "exportSession") - @RequestMapping(value = "/export", method = RequestMethod.GET) - public List<ImportExportSession> getExport( - @ApiParam(value = "sessionkey", required = true) @RequestParam(value = "sessionkey", defaultValue = "") final List<String> sessionkey, - @ApiParam(value = "wether statistics shall be exported", required = true) @RequestParam(value = "withAnswerStatistics", defaultValue = "false") final Boolean withAnswerStatistics, - @ApiParam(value = "wether comments shall be exported", required = true) @RequestParam(value = "withFeedbackQuestions", defaultValue = "false") final Boolean withFeedbackQuestions, - final HttpServletResponse response - ) { - List<ImportExportSession> sessions = new ArrayList<>(); - ImportExportSession temp; - for (String key : sessionkey) { - sessionService.setActive(key, false); - temp = sessionService.exportSession(key, withAnswerStatistics, withFeedbackQuestions); - if (temp != null) { - sessions.add(temp); - } - sessionService.setActive(key, true); - } - return sessions; - } - - @ApiOperation(value = "copy a session to the public pool if enabled") - @RequestMapping(value = "/{sessionkey}/copytopublicpool", method = RequestMethod.POST) - public SessionInfo copyToPublicPool( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "public pool attributes for session", required = true) @RequestBody final de.thm.arsnova.entities.transport.ImportExportSession.PublicPool publicPool - ) { - sessionService.setActive(sessionkey, false); - SessionInfo sessionInfo = sessionService.copySessionToPublicPool(sessionkey, publicPool); - sessionService.setActive(sessionkey, true); - return sessionInfo; - } - - - @ApiOperation(value = "Locks or unlocks a Session", - nickname = "lockSession") - @ApiResponses(value = { - @ApiResponse(code = 404, message = HTML_STATUS_404) - }) - @RequestMapping(value = "/{sessionkey}/lock", method = RequestMethod.POST) - public Session lockSession( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "lock", required = true) @RequestParam(required = false) final Boolean lock, - final HttpServletResponse response - ) { - if (lock != null) { - return sessionService.setActive(sessionkey, lock); - } - response.setStatus(HttpStatus.NOT_FOUND.value()); - return null; - } - - @ApiOperation(value = "retrieves a value for the score", - nickname = "getLearningProgress") - @RequestMapping(value = "/{sessionkey}/learningprogress", method = RequestMethod.GET) - public ScoreStatistics getLearningProgress( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "type", required = false) @RequestParam(value = "type", defaultValue = "questions") final String type, - @ApiParam(value = "question variant", required = false) @RequestParam(value = "questionVariant", required = false) final String questionVariant, - final HttpServletResponse response - ) { - return sessionService.getLearningProgress(sessionkey, type, questionVariant); - } - - @ApiOperation(value = "retrieves a value for the learning progress for the current user", - nickname = "getMyLearningProgress") - @RequestMapping(value = "/{sessionkey}/mylearningprogress", method = RequestMethod.GET) - public ScoreStatistics getMyLearningProgress( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @RequestParam(value = "type", defaultValue = "questions") final String type, - @RequestParam(value = "questionVariant", required = false) final String questionVariant, - final HttpServletResponse response - ) { - return sessionService.getMyLearningProgress(sessionkey, type, questionVariant); - } - - @ApiOperation(value = "retrieves all session features", - nickname = "getSessionFeatures") - @RequestMapping(value = "/{sessionkey}/features", method = RequestMethod.GET) - public SessionFeature getSessionFeatures( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - final HttpServletResponse response - ) { - return sessionService.getFeatures(sessionkey); - } - - @RequestMapping(value = "/{sessionkey}/features", method = RequestMethod.PUT) - @ApiOperation(value = "change all session features", - nickname = "changeSessionFeatures") - public SessionFeature changeSessionFeatures( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "session feature", required = true) @RequestBody final SessionFeature features, - final HttpServletResponse response - ) { - return sessionService.updateFeatures(sessionkey, features); - } - - @RequestMapping(value = "/{sessionkey}/lockfeedbackinput", method = RequestMethod.POST) - @ApiOperation(value = "locks input of user live feedback", - nickname = "lockFeedbackInput") - public boolean lockFeedbackInput( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "lock", required = true) @RequestParam(required = true) final Boolean lock, - final HttpServletResponse response - ) { - return sessionService.lockFeedbackInput(sessionkey, lock); - } - - @RequestMapping(value = "/{sessionkey}/flipflashcards", method = RequestMethod.POST) - @ApiOperation(value = "flip all flashcards in session", - nickname = "lockFeedbackInput") - public boolean flipFlashcards( - @ApiParam(value = "session-key from current session", required = true) @PathVariable final String sessionkey, - @ApiParam(value = "flip", required = true) @RequestParam(required = true) final Boolean flip, - final HttpServletResponse response - ) { - return sessionService.flipFlashcards(sessionkey, flip); - } - - /* internal redirections */ - - @RequestMapping(value = "/{sessionKey}/lecturerquestion") - public String redirectLecturerQuestion( - @PathVariable final String sessionKey, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/lecturerquestion/?sessionkey=%s", sessionKey); - } - - @RequestMapping(value = "/{sessionKey}/lecturerquestion/{arg1}") - public String redirectLecturerQuestionWithOneArgument( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/lecturerquestion/%s/?sessionkey=%s", arg1, sessionKey); - } - - @RequestMapping(value = "/{sessionKey}/lecturerquestion/{arg1}/{arg2}") - public String redirectLecturerQuestionWithTwoArguments( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - @PathVariable final String arg2, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/lecturerquestion/%s/%s/?sessionkey=%s", arg1, arg2, sessionKey); - } - - @RequestMapping(value = "/{sessionKey}/lecturerquestion/{arg1}/{arg2}/{arg3}") - public String redirectLecturerQuestionWithThreeArguments( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - @PathVariable final String arg2, - @PathVariable final String arg3, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/lecturerquestion/%s/%s/%s/?sessionkey=%s", arg1, arg2, arg3, sessionKey); - } - - @RequestMapping(value = "/{sessionKey}/audiencequestion") - public String redirectAudienceQuestion( - @PathVariable final String sessionKey, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/audiencequestion/?sessionkey=%s", sessionKey); - } - - @RequestMapping(value = "/{sessionKey}/audiencequestion/{arg1}") - public String redirectAudienceQuestionWithOneArgument( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/audiencequestion/%s/?sessionkey=%s", arg1, sessionKey); - } - - @RequestMapping(value = "/{sessionKey}/audiencequestion/{arg1}/{arg2}") - public String redirectAudienceQuestionWithTwoArguments( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - @PathVariable final String arg2, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/audiencequestion/%s/%s/?sessionkey=%s", arg1, arg2, sessionKey); - } - - @RequestMapping(value = "/{sessionKey}/audiencequestion/{arg1}/{arg2}/{arg3}") - public String redirectAudienceQuestionWithThreeArguments( - @PathVariable final String sessionKey, - @PathVariable final String arg1, - @PathVariable final String arg2, - @PathVariable final String arg3, - final HttpServletResponse response - ) { - response.addHeader(X_FORWARDED, "1"); - - return String.format("forward:/audiencequestion/%s/%s/%s/?sessionkey=%s", arg1, arg2, arg3, sessionKey); - } -} diff --git a/src/main/java/de/thm/arsnova/controller/UserController.java b/src/main/java/de/thm/arsnova/controller/UserController.java index aa5817ffb5310b507b7eb2004b8e592e122af993..e6549fbfad400713e66510abca6bc8fae14a049a 100644 --- a/src/main/java/de/thm/arsnova/controller/UserController.java +++ b/src/main/java/de/thm/arsnova/controller/UserController.java @@ -1,110 +1,123 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ package de.thm.arsnova.controller; -import de.thm.arsnova.entities.DbUser; -import de.thm.arsnova.services.UserService; -import de.thm.arsnova.services.UserSessionService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.stereotype.Controller; +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.LoginCredentials; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.model.serialization.View; +import de.thm.arsnova.web.exceptions.BadRequestException; +import de.thm.arsnova.web.exceptions.ForbiddenException; +import de.thm.arsnova.service.RoomService; +import de.thm.arsnova.service.UserService; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; +@RestController +@RequestMapping(UserController.REQUEST_MAPPING) +public class UserController extends AbstractEntityController<UserProfile> { + protected static final String REQUEST_MAPPING = "/user"; + private static final String REGISTER_MAPPING = "/register"; + private static final String ACTIVATE_MAPPING = DEFAULT_ID_MAPPING + "/activate"; + private static final String RESET_PASSWORD_MAPPING = DEFAULT_ID_MAPPING + "/resetpassword"; + private static final String ROOM_HISTORY_MAPPING = DEFAULT_ID_MAPPING + "/roomHistory"; -/** - * Handles requests related to ARSnova's own user registration and login process. - */ -@Controller -@RequestMapping("/user") -public class UserController extends AbstractController { - @Autowired - private DaoAuthenticationProvider daoProvider; - - @Autowired private UserService userService; + private RoomService roomService; + + public UserController(final UserService userService, final RoomService roomService) { + super(userService); + this.userService = userService; + this.roomService = roomService; + } - @Autowired - private UserSessionService userSessionService; + class Activation { + private String key; - @RequestMapping(value = "/register", method = RequestMethod.POST) - public void register(@RequestParam final String username, - @RequestParam final String password, - final HttpServletRequest request, final HttpServletResponse response) { - if (null != userService.create(username, password)) { - return; + public String getKey() { + return key; } - /* TODO: Improve error handling: send reason to client */ - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + @JsonView(View.Public.class) + public void setKey(final String key) { + this.key = key; + } } - @RequestMapping(value = "/{username}/activate", method = { RequestMethod.POST, - RequestMethod.GET }) - public void activate( - @PathVariable final String username, - @RequestParam final String key, final HttpServletRequest request, - final HttpServletResponse response) { - DbUser dbUser = userService.getByUsername(username); - if (null != dbUser && key.equals(dbUser.getActivationKey())) { - dbUser.setActivationKey(null); - userService.update(dbUser); - - return; + class PasswordReset { + private String key; + private String password; + + public String getKey() { + return key; + } + + @JsonView(View.Public.class) + public void setKey(final String key) { + this.key = key; + } + + public String getPassword() { + return password; } - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + @JsonView(View.Public.class) + public void setPassword(final String password) { + this.password = password; + } + } + + @Override + protected String getMapping() { + return REQUEST_MAPPING; + } + + @PostMapping(REGISTER_MAPPING) + public void register(@RequestBody LoginCredentials loginCredentials) { + userService.create(loginCredentials.getLoginId(), loginCredentials.getPassword()); } - @RequestMapping(value = "/{username}/", method = RequestMethod.DELETE) + @RequestMapping(value = ACTIVATE_MAPPING, method = RequestMethod.POST) public void activate( - @PathVariable final String username, - final HttpServletRequest request, - final HttpServletResponse response) { - if (null == userService.deleteByUsername(username)) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); + @PathVariable final String id, + @RequestParam final String key) { + UserProfile userProfile = userService.get(id, true); + if (userProfile == null || !key.equals(userProfile.getAccount().getActivationKey())) { + throw new BadRequestException(); } + userProfile.getAccount().setActivationKey(null); + userService.update(userProfile); } - @RequestMapping(value = "/{username}/resetpassword", method = RequestMethod.POST) + @RequestMapping(value = RESET_PASSWORD_MAPPING, method = RequestMethod.POST) public void resetPassword( - @PathVariable final String username, - @RequestParam(required = false) final String key, - @RequestParam(required = false) final String password, - final HttpServletRequest request, - final HttpServletResponse response) { - DbUser dbUser = userService.getByUsername(username); - if (null == dbUser) { - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - - return; + @PathVariable final String id, + @RequestBody final PasswordReset passwordReset) { + UserProfile userProfile = userService.get(id, true); + if (userProfile == null) { + throw new BadRequestException(); } - if (null != key) { - if (!userService.resetPassword(dbUser, key, password)) { - response.setStatus(HttpServletResponse.SC_FORBIDDEN); + if (passwordReset.getKey() != null) { + if (!userService.resetPassword(userProfile, passwordReset.getKey(), passwordReset.getPassword())) { + throw new ForbiddenException(); } } else { - userService.initiatePasswordReset(username); + userService.initiatePasswordReset(id); } } + + @PostMapping(ROOM_HISTORY_MAPPING) + public void postRoomHistoryEntry(@PathVariable final String id, + @RequestBody final UserProfile.RoomHistoryEntry roomHistoryEntry) { + userService.addRoomToHistory(userService.get(id), roomService.get(roomHistoryEntry.getRoomId())); + } + + @Override + protected String resolveAlias(final String alias) { + return userService.getByUsername(alias).getId(); + } } diff --git a/src/main/java/de/thm/arsnova/controller/WelcomeController.java b/src/main/java/de/thm/arsnova/controller/WelcomeController.java index 40070b7c7b19800bc0700b03d49d8e57393cdef6..29fea9bae5bdad4e0bcaab1f6ee9e13e739702e9 100644 --- a/src/main/java/de/thm/arsnova/controller/WelcomeController.java +++ b/src/main/java/de/thm/arsnova/controller/WelcomeController.java @@ -17,8 +17,8 @@ */ package de.thm.arsnova.controller; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.exceptions.NoContentException; +import de.thm.arsnova.web.exceptions.BadRequestException; +import de.thm.arsnova.web.exceptions.NoContentException; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; diff --git a/src/main/java/de/thm/arsnova/controller/LoginController.java b/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java similarity index 73% rename from src/main/java/de/thm/arsnova/controller/LoginController.java rename to src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java index 10e1bd4b0062961b845274bb13eef2a24115c7f2..a7f293d4a6fd2b8727248173ba54e17f84bd3f58 100644 --- a/src/main/java/de/thm/arsnova/controller/LoginController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/AuthenticationController.java @@ -15,14 +15,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.controller; - -import de.thm.arsnova.entities.ServiceDescription; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.services.UserService; -import de.thm.arsnova.services.UserSessionService; +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.config.SecurityConfig; +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.model.ServiceDescription; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.security.User; +import de.thm.arsnova.service.UserService; import org.pac4j.core.context.J2EContext; import org.pac4j.core.exception.HttpAction; import org.pac4j.oauth.client.FacebookClient; @@ -34,17 +36,14 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.cas.authentication.CasAuthenticationToken; import org.springframework.security.cas.web.CasAuthenticationEntryPoint; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.token.Sha512DigestUtils; -import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; -import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.util.UrlUtils; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -68,12 +67,9 @@ import java.util.List; /** * Handles authentication specific requests. */ -@Controller -public class LoginController extends AbstractController { - - private static final int MAX_USERNAME_LENGTH = 15; - private static final int MAX_GUESTHASH_LENGTH = 10; - +@Controller("v2AuthenticationController") +@RequestMapping("/v2/auth") +public class AuthenticationController extends AbstractController { @Value("${api.path:}") private String apiPath; @Value("${customization.path}") private String customizationPath; @@ -123,9 +119,6 @@ public class LoginController extends AbstractController { @Autowired private ServletContext servletContext; - @Autowired(required = false) - private DaoAuthenticationProvider daoProvider; - @Autowired(required = false) private TwitterClient twitterClient; @@ -135,19 +128,13 @@ public class LoginController extends AbstractController { @Autowired(required = false) private FacebookClient facebookClient; - @Autowired(required = false) - private LdapAuthenticationProvider ldapAuthenticationProvider; - @Autowired(required = false) private CasAuthenticationEntryPoint casEntryPoint; @Autowired private UserService userService; - @Autowired - private UserSessionService userSessionService; - - private static final Logger logger = LoggerFactory.getLogger(LoginController.class); + private static final Logger logger = LoggerFactory.getLogger(AuthenticationController.class); @PostConstruct private void init() { @@ -156,12 +143,11 @@ public class LoginController extends AbstractController { } } - @RequestMapping(value = { "/auth/login", "/doLogin" }, method = { RequestMethod.POST, RequestMethod.GET }) + @RequestMapping(value = { "/login", "/doLogin" }, method = { RequestMethod.POST, RequestMethod.GET }) public void doLogin( @RequestParam("type") final String type, @RequestParam(value = "user", required = false) String username, @RequestParam(required = false) final String password, - @RequestParam(value = "role", required = false) final UserSessionService.Role role, final HttpServletRequest request, final HttpServletResponse response ) throws IOException { @@ -171,74 +157,39 @@ public class LoginController extends AbstractController { return; } - - userSessionService.setRole(role); + final UsernamePasswordAuthenticationToken authRequest = + new UsernamePasswordAuthenticationToken(username, password); if (dbAuthEnabled && "arsnova".equals(type)) { - Authentication authRequest = new UsernamePasswordAuthenticationToken(username, password); try { - Authentication auth = daoProvider.authenticate(authRequest); - if (auth.isAuthenticated()) { - SecurityContextHolder.getContext().setAuthentication(auth); - request.getSession(true).setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); - - return; - } + userService.authenticate(authRequest, UserProfile.AuthProvider.ARSNOVA); } catch (AuthenticationException e) { logger.info("Database authentication failed.", e); + userService.increaseFailedLoginCount(addr); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); } - - userService.increaseFailedLoginCount(addr); - response.setStatus(HttpStatus.UNAUTHORIZED.value()); } else if (ldapEnabled && "ldap".equals(type)) { - if (!"".equals(username) && !"".equals(password)) { - org.springframework.security.core.userdetails.User user = - new org.springframework.security.core.userdetails.User( - username, password, true, true, true, true, this.getAuthorities(userService.isAdmin(username)) - ); - - Authentication token = new UsernamePasswordAuthenticationToken(user, password, getAuthorities(userService.isAdmin(username))); - try { - Authentication auth = ldapAuthenticationProvider.authenticate(token); - if (auth.isAuthenticated()) { - SecurityContextHolder.getContext().setAuthentication(auth); - request.getSession(true).setAttribute( - HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); - - return; - } - logger.info("LDAP authentication failed."); - } catch (AuthenticationException e) { - logger.info("LDAP authentication failed.", e); - } - + try { + userService.authenticate(authRequest, UserProfile.AuthProvider.LDAP); + } catch (AuthenticationException e) { + logger.info("LDAP authentication failed.", e); userService.increaseFailedLoginCount(addr); response.setStatus(HttpStatus.UNAUTHORIZED.value()); } } else if (guestEnabled && "guest".equals(type)) { - List<GrantedAuthority> authorities = new ArrayList<>(); - authorities.add(new SimpleGrantedAuthority("ROLE_GUEST")); - if (username == null || !username.startsWith("Guest") || username.length() != MAX_USERNAME_LENGTH) { - username = "Guest" + Sha512DigestUtils.shaHex(request.getSession().getId()).substring(0, MAX_GUESTHASH_LENGTH); + try { + userService.authenticate(authRequest, UserProfile.AuthProvider.ARSNOVA_GUEST); + } catch (final AuthenticationException e) { + logger.debug("Guest authentication failed.", e); + userService.increaseFailedLoginCount(addr); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); } - org.springframework.security.core.userdetails.User user = - new org.springframework.security.core.userdetails.User( - username, "", true, true, true, true, authorities - ); - Authentication token = new UsernamePasswordAuthenticationToken(user, null, authorities); - - SecurityContextHolder.getContext().setAuthentication(token); - request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, - SecurityContextHolder.getContext()); } else { response.setStatus(HttpStatus.BAD_REQUEST.value()); } } - @RequestMapping(value = { "/auth/dialog" }, method = RequestMethod.GET) + @RequestMapping(value = { "/dialog" }, method = RequestMethod.GET) @ResponseBody public View dialog( @RequestParam("type") final String type, @@ -297,26 +248,28 @@ public class LoginController extends AbstractController { return result; } - @RequestMapping(value = { "/auth/", "/whoami" }, method = RequestMethod.GET) + @RequestMapping(value = { "/", "/whoami" }, method = RequestMethod.GET) @ResponseBody - public User whoami() { - userSessionService.setUser(userService.getCurrentUser()); - return userService.getCurrentUser(); + public ClientAuthentication whoami(@AuthenticationPrincipal User user) { + if (user == null) { + throw new UnauthorizedException(); + } + return new ClientAuthentication(user); } - @RequestMapping(value = { "/auth/logout", "/logout" }, method = { RequestMethod.POST, RequestMethod.GET }) - public View doLogout(final HttpServletRequest request) { + @RequestMapping(value = { "/logout" }, method = { RequestMethod.POST, RequestMethod.GET }) + public String doLogout(final HttpServletRequest request) { final Authentication auth = SecurityContextHolder.getContext().getAuthentication(); userService.removeUserFromMaps(userService.getCurrentUser()); request.getSession().invalidate(); SecurityContextHolder.clearContext(); if (auth instanceof CasAuthenticationToken) { - return new RedirectView(apiPath + "/j_spring_cas_security_logout"); + return "redirect:" + apiPath + SecurityConfig.CAS_LOGOUT_PATH_SUFFIX; } - return new RedirectView(request.getHeader("referer") != null ? request.getHeader("referer") : "/"); + return "redirect:" + request.getHeader("referer") != null ? request.getHeader("referer") : "/"; } - @RequestMapping(value = { "/auth/services" }, method = RequestMethod.GET) + @RequestMapping(value = { "/services" }, method = RequestMethod.GET) @ResponseBody public List<ServiceDescription> getServices(final HttpServletRequest request) { List<ServiceDescription> services = new ArrayList<>(); diff --git a/src/main/java/de/thm/arsnova/controller/v2/CommentController.java b/src/main/java/de/thm/arsnova/controller/v2/CommentController.java new file mode 100644 index 0000000000000000000000000000000000000000..dab81e01c66ac7c6fe96c0cd3319224892597779 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/CommentController.java @@ -0,0 +1,131 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.controller.PaginationController; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.FromV2Migrator; +import de.thm.arsnova.model.migration.ToV2Migrator; +import de.thm.arsnova.model.migration.v2.Comment; +import de.thm.arsnova.model.migration.v2.CommentReadingCount; +import de.thm.arsnova.service.CommentService; +import de.thm.arsnova.service.RoomService; +import de.thm.arsnova.service.UserService; +import de.thm.arsnova.web.DeprecatedApi; +import de.thm.arsnova.web.Pagination; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.io.IOException; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Handles requests related to comments. + */ +@RestController("v2CommentController") +@RequestMapping("/v2/audiencequestion") +@Api(value = "/audiencequestion", description = "Comment (Interposed/Audience Question) API") +public class CommentController extends PaginationController { + + @Autowired + private CommentService commentService; + + @Autowired + private RoomService roomService; + + @Autowired + private UserService userService; + + @Autowired + private ToV2Migrator toV2Migrator; + + @Autowired + private FromV2Migrator fromV2Migrator; + + @ApiOperation(value = "Count all the comments in current room", + nickname = "getCommentCount") + @RequestMapping(value = "/count", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + @DeprecatedApi + @Deprecated + public String getCommentCount(@ApiParam(value = "Room-Key from current room", required = true) @RequestParam("sessionkey") final String roomShortId) { + return String.valueOf(commentService.count(roomService.getIdByShortId(roomShortId))); + } + + @ApiOperation(value = "count all unread comments", + nickname = "getUnreadCommentCount") + @RequestMapping(value = "/readcount", method = RequestMethod.GET) + @DeprecatedApi + @Deprecated + public CommentReadingCount getUnreadCommentCount(@ApiParam(value = "Room-Key from current room", required = true) @RequestParam("sessionkey") final String roomShortId, String user) { + return commentService.countRead(roomService.getIdByShortId(roomShortId), user); + } + + @ApiOperation(value = "Retrieves all Comments for a Room", + nickname = "getComments") + @RequestMapping(value = "/", method = RequestMethod.GET) + @Pagination + public List<Comment> getComments(@ApiParam(value = "Room-Key from current room", required = true) @RequestParam("sessionkey") final String roomShortId) { + return commentService.getByRoomId(roomService.getIdByShortId(roomShortId), offset, limit).stream() + .map(toV2Migrator::migrate).collect(Collectors.toList()); + } + + @ApiOperation(value = "Retrieves an Comment", + nickname = "getComment") + @RequestMapping(value = "/{commentId}", method = RequestMethod.GET) + public Comment getComment(@ApiParam(value = "ID of the Comment that needs to be deleted", required = true) @PathVariable final String commentId) throws IOException { + return toV2Migrator.migrate(commentService.getAndMarkRead(commentId)); + } + + @ApiOperation(value = "Creates a new Comment for a Room and returns the Comment's data", + nickname = "postComment") + @ApiResponses(value = { + @ApiResponse(code = 400, message = HTML_STATUS_400) + }) + @RequestMapping(value = "/", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public void postComment( + @ApiParam(value = "Room-Key from current room", required = true) @RequestParam("sessionkey") final String roomShortId, + @ApiParam(value = "the body from the new comment", required = true) @RequestBody final Comment comment + ) { + de.thm.arsnova.model.Comment commentV3 = fromV2Migrator.migrate(comment); + Room roomV3 = roomService.getByShortId(roomShortId); + commentV3.setRoomId(roomV3.getId()); + commentService.create(commentV3); + } + + @ApiOperation(value = "Deletes a Comment", + nickname = "deleteComment") + @RequestMapping(value = "/{commentId}", method = RequestMethod.DELETE) + public void deleteComment(@ApiParam(value = "ID of the comment that needs to be deleted", required = true) @PathVariable final String commentId) { + commentService.delete(commentId); + } +} diff --git a/src/main/java/de/thm/arsnova/controller/v2/ContentController.java b/src/main/java/de/thm/arsnova/controller/v2/ContentController.java new file mode 100644 index 0000000000000000000000000000000000000000..b6bbe1fdbcdf28b72c43ec004c04914376a78640 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/ContentController.java @@ -0,0 +1,689 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.controller.PaginationController; +import de.thm.arsnova.model.ChoiceAnswer; +import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.TextAnswer; +import de.thm.arsnova.model.migration.FromV2Migrator; +import de.thm.arsnova.model.migration.ToV2Migrator; +import de.thm.arsnova.model.migration.v2.Answer; +import de.thm.arsnova.model.migration.v2.Content; +import de.thm.arsnova.web.exceptions.ForbiddenException; +import de.thm.arsnova.web.exceptions.NoContentException; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.web.exceptions.NotImplementedException; +import de.thm.arsnova.service.AnswerService; +import de.thm.arsnova.service.ContentService; +import de.thm.arsnova.service.RoomService; +import de.thm.arsnova.service.TimerService; +import de.thm.arsnova.util.PaginationListDecorator; +import de.thm.arsnova.web.DeprecatedApi; +import de.thm.arsnova.web.Pagination; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.naming.OperationNotSupportedException; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Handles requests related to contents. + */ +@RestController("v2ContentController") +@RequestMapping("/v2/lecturerquestion") +@Api(value = "/lecturerquestion", description = "Content (Skill/Lecturer Question) API") +public class ContentController extends PaginationController { + @Autowired + private ContentService contentService; + + @Autowired + private AnswerService answerService; + + @Autowired + private RoomService roomService; + + @Autowired + private TimerService timerService; + + @Autowired + private ToV2Migrator toV2Migrator; + + @Autowired + private FromV2Migrator fromV2Migrator; + + @ApiOperation(value = "Get content with provided content Id", + nickname = "getContent") + @ApiResponses(value = { + @ApiResponse(code = 404, message = HTML_STATUS_404) + }) + @RequestMapping(value = "/{contentId}", method = RequestMethod.GET) + public Content getContent(@PathVariable final String contentId) { + final de.thm.arsnova.model.Content content = contentService.get(contentId); + if (content != null) { + return toV2Migrator.migrate(content); + } + + throw new NotFoundException(); + } + + @ApiOperation(value = "Post provided content", + nickname = "postContent") + @ApiResponses(value = { + @ApiResponse(code = 400, message = HTML_STATUS_400) + }) + @RequestMapping(value = "/", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public Content postContent(@RequestBody final Content content) { + de.thm.arsnova.model.Content contentV3 = fromV2Migrator.migrate(content); + final String roomId = roomService.getIdByShortId(content.getSessionKeyword()); + contentV3.setRoomId(roomId); + contentService.create(contentV3); + + return toV2Migrator.migrate(contentV3); + } + + @ApiOperation(value = "Post provided contents", nickname = "bulkPostContents") + @ApiResponses(value = { + @ApiResponse(code = 400, message = HTML_STATUS_400) + }) + @RequestMapping(value = "/bulk", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public List<Content> bulkPostContents(@RequestBody final List<Content> contents) { + List<de.thm.arsnova.model.Content> contentsV3 = + contents.stream().map(c -> contentService.create(fromV2Migrator.migrate(c))).collect(Collectors.toList()); + return contentsV3.stream().map(toV2Migrator::migrate).collect(Collectors.toList()); + } + + @ApiOperation(value = "Update the content, identified by provided id, with the provided content in the Request Body", + nickname = "updateContent") + @ApiResponses(value = { + @ApiResponse(code = 400, message = HTML_STATUS_400) + }) + @RequestMapping(value = "/{contentId}", method = RequestMethod.PUT) + public Content updateContent( + @PathVariable final String contentId, + @RequestBody final Content content + ) { + return toV2Migrator.migrate(contentService.update(fromV2Migrator.migrate(content))); + } + + @ApiOperation(value = "Start new Pi Round on content, identified by provided id, with an optional time", + nickname = "startPiRound") + @RequestMapping(value = "/{contentId}/questionimage", method = RequestMethod.GET) + public String getContentImage( + @PathVariable final String contentId, + @RequestParam(value = "fcImage", defaultValue = "false", required = false) final boolean fcImage + ) { + + throw new NotImplementedException(); + } + + @RequestMapping(value = "/{contentId}/startnewpiround", method = RequestMethod.POST) + public void startPiRound( + @PathVariable final String contentId, + @RequestParam(value = "time", defaultValue = "0", required = false) final int time + ) { + + if (time == 0) { + timerService.startNewRound(contentId, null); + } else { + timerService.startNewRoundDelayed(contentId, time); + } + } + + @RequestMapping(value = "/{contentId}/canceldelayedpiround", method = RequestMethod.POST) + @ApiOperation(value = "Cancel Pi Round on content, identified by provided id", + nickname = "cancelPiRound") + public void cancelPiRound( + @PathVariable final String contentId + ) { + timerService.cancelRoundChange(contentId); + } + + @RequestMapping(value = "/{contentId}/resetpiroundstate", method = RequestMethod.POST) + @ApiOperation(value = "Reset Pi Round on content, identified by provided id", + nickname = "resetPiContent") + public void resetPiContent( + @PathVariable final String contentId + ) { + timerService.resetRoundState(contentId); + } + + @ApiOperation(value = "Set voting admission on content, identified by provided id", + nickname = "setVotingAdmission") + @RequestMapping(value = "/{contentId}/disablevote", method = RequestMethod.POST) + public void setVotingAdmission( + @PathVariable final String contentId, + @RequestParam(value = "disable", defaultValue = "false", required = false) final Boolean disableVote + ) { + boolean disable = false; + + if (disableVote != null) { + disable = disableVote; + } + + contentService.setVotingAdmission(contentId, disable); + } + + @ApiOperation(value = "Set voting admission for all contents", + nickname = "setVotingAdmissionForAllContents") + @RequestMapping(value = "/disablevote", method = RequestMethod.POST) + public void setVotingAdmissionForAllContents( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(value = "disable", defaultValue = "false", required = false) final Boolean disableVote, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false", required = false) boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false", required = false) boolean preparationContentsOnly + ) { + String roomId = roomService.getIdByShortId(roomShortId); + boolean disable = false; + Iterable<de.thm.arsnova.model.Content> contents; + + if (disableVote != null) { + disable = disableVote; + } + + if (lectureContentsOnly) { + contents = contentService.getByRoomIdAndGroup(roomId, "lecture"); + contentService.setVotingAdmissions(roomId, disable, contents); + } else if (preparationContentsOnly) { + contents = contentService.getByRoomIdAndGroup(roomId, "preparation"); + contentService.setVotingAdmissions(roomId, disable, contents); + } else { + contents = contentService.getByRoomId(roomId); + contentService.setVotingAdmissions(roomId, disable, contents); + } + } + + @ApiOperation(value = "Publish a content, identified by provided id and content in Request Body.", + nickname = "publishContent") + @RequestMapping(value = "/{contentId}/publish", method = RequestMethod.POST) + public void publishContent( + @PathVariable final String contentId, + @RequestParam(required = false) final Boolean publish, + @RequestBody final Content content + ) { + de.thm.arsnova.model.Content contentV3 = fromV2Migrator.migrate(content); + if (publish != null) { + contentV3.getState().setVisible(publish); + } + contentService.update(contentV3); + } + + @ApiOperation(value = "Publish all contents", + nickname = "publishAllContents") + @RequestMapping(value = "/publish", method = RequestMethod.POST) + public void publishAllContents( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(required = false) final Boolean publish, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false", required = false) boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false", required = false) boolean preparationContentsOnly + ) { + String roomId = roomService.getIdByShortId(roomShortId); + boolean p = publish == null || publish; + Iterable<de.thm.arsnova.model.Content> contents; + + if (lectureContentsOnly) { + contents = contentService.getByRoomIdAndGroup(roomId, "lecture"); + contentService.publishContents(roomId, p, contents); + } else if (preparationContentsOnly) { + contents = contentService.getByRoomIdAndGroup(roomId, "preparation"); + contentService.publishContents(roomId, p, contents); + } else { + contentService.publishAll(roomId, p); + } + } + + @ApiOperation(value = "Publish statistics from content with provided id", + nickname = "publishStatistics") + @RequestMapping(value = "/{contentId}/publishstatistics", method = RequestMethod.POST) + public void publishStatistics( + @PathVariable final String contentId, + @RequestParam(required = false) final Boolean showStatistics, + @RequestBody final Content content + ) { + de.thm.arsnova.model.Content contentV3 = fromV2Migrator.migrate(content); + if (showStatistics != null) { + contentV3.getState().setResponsesVisible(showStatistics); + } + contentService.update(contentV3); + } + + @ApiOperation(value = "Publish correct answer from content with provided id", + nickname = "publishCorrectAnswer") + @RequestMapping(value = "/{contentId}/publishcorrectanswer", method = RequestMethod.POST) + public void publishCorrectAnswer( + @PathVariable final String contentId, + @RequestParam(required = false) final Boolean showCorrectAnswer, + @RequestBody final Content content + ) { + de.thm.arsnova.model.Content contentV3 = fromV2Migrator.migrate(content); + if (showCorrectAnswer != null) { + contentV3.getState().setSolutionVisible(showCorrectAnswer); + } + contentService.update(contentV3); + } + + @ApiOperation(value = "Get contents", + nickname = "getContents") + @RequestMapping(value = "/", method = RequestMethod.GET) + @Pagination + public List<Content> getContents( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, + @RequestParam(value = "flashcardsonly", defaultValue = "false") boolean flashcardsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly, + @RequestParam(value = "requestImageData", defaultValue = "false") boolean requestImageData, + final HttpServletResponse response + ) { + String roomId = roomService.getIdByShortId(roomShortId); + Iterable<de.thm.arsnova.model.Content> contents; + if (lectureContentsOnly) { + contents = contentService.getByRoomIdAndGroup(roomId, "lecture"); + } else if (flashcardsOnly) { + contents = contentService.getByRoomIdAndGroup(roomId, "flashcard"); + } else if (preparationContentsOnly) { + contents = contentService.getByRoomIdAndGroup(roomId, "preparation"); + } else { + contents = contentService.getByRoomId(roomId); + } + if (contents == null || !contents.iterator().hasNext()) { + response.setStatus(HttpStatus.NO_CONTENT.value()); + return null; + } + + return new PaginationListDecorator<>(StreamSupport.stream(contents.spliterator(), false) + .map(toV2Migrator::migrate).collect(Collectors.toList()), offset, limit); + } + + @ApiOperation(value = "Delete contents", + nickname = "deleteContents") + @RequestMapping(value = { "/" }, method = RequestMethod.DELETE) + public void deleteContents( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, + @RequestParam(value = "flashcardsonly", defaultValue = "false") boolean flashcardsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly, + final HttpServletResponse response + ) { + String roomId = roomService.getIdByShortId(roomShortId); + /* FIXME: Content variant is ignored for now */ + lectureContentsOnly = preparationContentsOnly = flashcardsOnly = false; + if (lectureContentsOnly) { + contentService.deleteLectureContents(roomId); + } else if (preparationContentsOnly) { + contentService.deletePreparationContents(roomId); + } else if (flashcardsOnly) { + contentService.deleteFlashcards(roomId); + } else { + contentService.deleteAllContents(roomId); + } + } + + @ApiOperation(value = "Get the amount of contents by the room-key", + nickname = "getContentCount") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/count", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getContentCount( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, + @RequestParam(value = "flashcardsonly", defaultValue = "false") boolean flashcardsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly + ) { + String roomId = roomService.getIdByShortId(roomShortId); + int count; + if (lectureContentsOnly) { + count = contentService.countByRoomIdAndGroup(roomId, "lecture"); + } else if (preparationContentsOnly) { + count = contentService.countByRoomIdAndGroup(roomId, "preparation"); + } else if (flashcardsOnly) { + count = contentService.countByRoomIdAndGroup(roomId, "flashcard"); + } else { + count = contentService.countByRoomId(roomId); + } + + return String.valueOf(count); + } + + @ApiOperation(value = "Delete answers and content", + nickname = "deleteAnswersAndContent") + @RequestMapping(value = "/{contentId}", method = RequestMethod.DELETE) + public void deleteAnswersAndContent( + @PathVariable final String contentId + ) { + contentService.delete(contentId); + } + + @ApiOperation(value = "Get unanswered content IDs by provided room short ID", + nickname = "getUnAnsweredContentIds") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/unanswered", method = RequestMethod.GET) + public List<String> getUnAnsweredContentIds( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly + ) { + String roomId = roomService.getIdByShortId(roomShortId); + List<String> answers; + /* FIXME: Content variant is ignored for now */ + lectureContentsOnly = preparationContentsOnly = false; + if (lectureContentsOnly) { + answers = contentService.getUnAnsweredLectureContentIds(roomId); + } else if (preparationContentsOnly) { + answers = contentService.getUnAnsweredPreparationContentIds(roomId); + } else { + answers = contentService.getUnAnsweredContentIds(roomId); + } + if (answers == null || answers.isEmpty()) { + throw new NoContentException(); + } + + return answers; + } + + /** + * returns a JSON document which represents the given answer of a content. + * + * @param contentId + * CouchDB Content ID for which the given answer should be + * retrieved + * @return JSON Document of {@link Answer} or {@link NotFoundException} + * @throws NotFoundException + * if wrong room, wrong content or no answer was given by + * the current user + * @throws ForbiddenException + * if not logged in + */ + @ApiOperation(value = "Get my answer for a content, identified by provided content ID", + nickname = "getMyAnswer") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/{contentId}/myanswer", method = RequestMethod.GET) + public Answer getMyAnswer( + @PathVariable final String contentId, + final HttpServletResponse response + ) { + final de.thm.arsnova.model.Content content = contentService.get(contentId); + final de.thm.arsnova.model.Answer answer = answerService.getMyAnswer(contentId); + if (answer == null) { + response.setStatus(HttpStatus.NO_CONTENT.value()); + return null; + } + + if (content.getFormat().equals(de.thm.arsnova.model.Content.Format.TEXT)) { + return toV2Migrator.migrate((TextAnswer) answer); + } else { + return toV2Migrator.migrate((ChoiceAnswer) answer, (ChoiceQuestionContent) content); + } + } + + /** + * returns a list of {@link Answer}s encoded as a JSON document for a given + * content id. In this case only {@link Answer} <tt>contentId</tt>, + * <tt>answerText</tt>, <tt>answerSubject</tt> and <tt>answerCount</tt> + * properties are set + * + * @param contentId + * CouchDB Content ID for which the given answers should be + * retrieved + * @throws NotFoundException + * if wrong room, wrong content or no answers was given + * @throws ForbiddenException + * if not logged in + */ + @ApiOperation(value = "Get answers for a content, identified by provided content ID", + nickname = "getAnswers") + @RequestMapping(value = "/{contentId}/answer/", method = RequestMethod.GET) + public List<Answer> getAnswers( + @PathVariable final String contentId, + @RequestParam(value = "piround", required = false) final Integer piRound, + @RequestParam(value = "all", required = false, defaultValue = "false") final Boolean allAnswers, + final HttpServletResponse response) { + final de.thm.arsnova.model.Content content = contentService.get(contentId); + if (content instanceof ChoiceQuestionContent) { + return toV2Migrator.migrate(answerService.getAllStatistics(contentId), + (ChoiceQuestionContent) content, content.getState().getRound()); + } else { + List<de.thm.arsnova.model.TextAnswer> answers; + if (allAnswers) { + answers = answerService.getAllTextAnswers(contentId, -1, -1); + } else if (null == piRound) { + answers = answerService.getTextAnswers(contentId, offset, limit); + } else { + if (piRound < 1 || piRound > 2) { + response.setStatus(HttpStatus.BAD_REQUEST.value()); + + return null; + } + answers = answerService.getTextAnswers(contentId, piRound, offset, limit); + } + if (answers == null) { + return new ArrayList<>(); + } + return answers.stream().map(toV2Migrator::migrate).collect(Collectors.toList()); + } + } + + @ApiOperation(value = "Save answer, provided in the Request Body, for a content, identified by provided content ID", + nickname = "saveAnswer") + @RequestMapping(value = "/{contentId}/answer/", method = RequestMethod.POST) + public Answer saveAnswer( + @PathVariable final String contentId, + @RequestBody final Answer answer, + final HttpServletResponse response + ) { + final de.thm.arsnova.model.Content content = contentService.get(contentId); + final de.thm.arsnova.model.Answer answerV3 = fromV2Migrator.migrate(answer, content); + + if (answerV3 instanceof TextAnswer) { + return toV2Migrator.migrate((TextAnswer) answerService.saveAnswer(contentId, answerV3)); + } else { + return toV2Migrator.migrate((ChoiceAnswer) answerService.saveAnswer(contentId, answerV3), (ChoiceQuestionContent) content); + } + } + + @ApiOperation(value = "Update answer, provided in Request Body, identified by content ID and answer ID", + nickname = "updateAnswer") + @RequestMapping(value = "/{contentId}/answer/{answerId}", method = RequestMethod.PUT) + public Answer updateAnswer( + @PathVariable final String contentId, + @PathVariable final String answerId, + @RequestBody final Answer answer, + final HttpServletResponse response + ) { + final de.thm.arsnova.model.Content content = contentService.get(contentId); + final de.thm.arsnova.model.Answer answerV3 = fromV2Migrator.migrate(answer, content); + + if (answerV3 instanceof TextAnswer) { + return toV2Migrator.migrate((TextAnswer) answerService.updateAnswer(answerV3)); + } else { + return toV2Migrator.migrate((ChoiceAnswer) answerService.updateAnswer(answerV3), (ChoiceQuestionContent) content); + } + } + + @ApiOperation(value = "Get Image, identified by content ID and answer ID", + nickname = "getImage") + @RequestMapping(value = "/{contentId}/answer/{answerId}/image", method = RequestMethod.GET) + public String getImage( + @PathVariable final String contentId, + @PathVariable final String answerId, + final HttpServletResponse response + ) { + + throw new NotImplementedException(); + } + + @ApiOperation(value = "Delete answer, identified by content ID and answer ID", + nickname = "deleteAnswer") + @RequestMapping(value = "/{contentId}/answer/{answerId}", method = RequestMethod.DELETE) + public void deleteAnswer( + @PathVariable final String contentId, + @PathVariable final String answerId, + final HttpServletResponse response + ) { + answerService.deleteAnswer(contentId, answerId); + } + + @ApiOperation(value = "Delete answers from a content, identified by content ID", + nickname = "deleteAnswers") + @RequestMapping(value = "/{contentId}/answer/", method = RequestMethod.DELETE) + public void deleteAnswers( + @PathVariable final String contentId, + final HttpServletResponse response + ) { + answerService.deleteAnswers(contentId); + } + + @ApiOperation(value = "Delete all answers and contents from a room, identified by room short ID", + nickname = "deleteAllContentsAnswers") + @RequestMapping(value = "/answers", method = RequestMethod.DELETE) + public void deleteAllContentsAnswers( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly, + final HttpServletResponse response + ) { + String roomId = roomService.getIdByShortId(roomShortId); + /* FIXME: Content variant is ignored for now */ + lectureContentsOnly = preparationContentsOnly = false; + if (lectureContentsOnly) { + contentService.deleteAllLectureAnswers(roomId); + } else if (preparationContentsOnly) { + contentService.deleteAllPreparationAnswers(roomId); + } else { + contentService.deleteAllContentsAnswers(roomId); + } + } + + /** + * + * @param contentId + * Content ID for which the given answers should be + * retrieved + * @return count of answers for given content ID + * @throws NotFoundException + * if wrong room or wrong content + * @throws ForbiddenException + * if not logged in + */ + @ApiOperation(value = "Get the amount of answers for a content, identified by content ID", + nickname = "getAnswerCount") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/{contentId}/answercount", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getAnswerCount(@PathVariable final String contentId) { + return String.valueOf(answerService.countAnswersByContentIdAndRound(contentId)); + } + + @ApiOperation(value = "Get the amount of answers for a content, identified by the content ID", + nickname = "getAllAnswerCount") + @RequestMapping(value = "/{contentId}/allroundanswercount", method = RequestMethod.GET) + public List<Integer> getAllAnswerCount(@PathVariable final String contentId) { + return Arrays.asList( + answerService.countAnswersByContentIdAndRound(contentId, 1), + answerService.countAnswersByContentIdAndRound(contentId, 2) + ); + } + + @ApiOperation(value = "Get the total amount of answers by a content, identified by the content ID", + nickname = "getTotalAnswerCountByContent") + @RequestMapping(value = "/{contentId}/totalanswercount", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getTotalAnswerCountByContent(@PathVariable final String contentId) { + return String.valueOf(answerService.countTotalAnswersByContentId(contentId)); + } + + @ApiOperation(value = "Get the amount of answers and abstention answers by a content, identified by the content ID", + nickname = "getAnswerAndAbstentionCount") + @RequestMapping(value = "/{contentId}/answerandabstentioncount", method = RequestMethod.GET) + public List<Integer> getAnswerAndAbstentionCount(@PathVariable final String contentId) { + return Arrays.asList( + answerService.countAnswersByContentIdAndRound(contentId), + answerService.countTotalAbstentionsByContentId(contentId) + ); + } + + @ApiOperation(value = "Get all Freetext answers by a content, identified by the content ID", + nickname = "getFreetextAnswers") + @RequestMapping(value = "/{contentId}/freetextanswer/", method = RequestMethod.GET) + @Pagination + public List<Answer> getFreetextAnswers(@PathVariable final String contentId) { + return answerService.getTextAnswersByContentId(contentId, offset, limit).stream() + .map(toV2Migrator::migrate).collect(Collectors.toList()); + } + + @ApiOperation(value = "Get my answers of an room, identified by the room short ID", + nickname = "getMyAnswers") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/myanswers", method = RequestMethod.GET) + public List<Answer> getMyAnswers(@RequestParam(value = "sessionkey") final String roomShortId) throws OperationNotSupportedException { + return answerService.getMyAnswersByRoomId(roomService.getIdByShortId(roomShortId)).stream() + .map(a -> { + if (a instanceof ChoiceAnswer) { + return toV2Migrator.migrate( + (ChoiceAnswer) a, (ChoiceQuestionContent) contentService.get(a.getContentId())); + } else { + return toV2Migrator.migrate((TextAnswer) a); + } + }).collect(Collectors.toList()); + } + + @ApiOperation(value = "Get the total amount of answers of a room, identified by the room short ID", + nickname = "getTotalAnswerCount") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/answercount", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getTotalAnswerCount( + @RequestParam(value = "sessionkey") final String roomShortId, + @RequestParam(value = "lecturequestionsonly", defaultValue = "false") boolean lectureContentsOnly, + @RequestParam(value = "preparationquestionsonly", defaultValue = "false") boolean preparationContentsOnly + ) { + String roomId = roomService.getIdByShortId(roomShortId); + int count = 0; + /* FIXME: Content variant is ignored for now */ + lectureContentsOnly = preparationContentsOnly = false; + if (lectureContentsOnly) { + count = answerService.countLectureContentAnswers(roomId); + } else if (preparationContentsOnly) { + count = answerService.countPreparationContentAnswers(roomId); + } else { + count = answerService.countTotalAnswersByRoomId(roomId); + } + + return String.valueOf(count); + } +} diff --git a/src/main/java/de/thm/arsnova/controller/CourseController.java b/src/main/java/de/thm/arsnova/controller/v2/CourseController.java similarity index 86% rename from src/main/java/de/thm/arsnova/controller/CourseController.java rename to src/main/java/de/thm/arsnova/controller/v2/CourseController.java index 04d44372590c4d2c63451d7d787abaaf1ba1e070..17a766d38b3d293a41250fab579a67ee7a4e72f4 100644 --- a/src/main/java/de/thm/arsnova/controller/CourseController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/CourseController.java @@ -15,15 +15,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.controller; +package de.thm.arsnova.controller.v2; import de.thm.arsnova.connector.client.ConnectorClient; import de.thm.arsnova.connector.model.Course; import de.thm.arsnova.connector.model.UserRole; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.exceptions.NotImplementedException; -import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.services.UserService; +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.web.exceptions.NotImplementedException; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.service.UserService; import io.swagger.annotations.ApiParam; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; @@ -40,7 +41,7 @@ import java.util.List; /** * Provides access to a user's courses in an LMS such as Moodle. */ -@RestController +@RestController("v2CourseController") public class CourseController extends AbstractController { @Autowired(required = false) private ConnectorClient connectorClient; @@ -48,13 +49,13 @@ public class CourseController extends AbstractController { @Autowired private UserService userService; - @RequestMapping(value = "/mycourses", method = RequestMethod.GET) + @RequestMapping(value = "/v2/mycourses", method = RequestMethod.GET) public List<Course> myCourses( @ApiParam(value = "sort my courses by name", required = true) @RequestParam(value = "sortby", defaultValue = "name") final String sortby ) { - final User currentUser = userService.getCurrentUser(); + final ClientAuthentication currentUser = userService.getCurrentUser(); if (currentUser == null || currentUser.getUsername() == null) { throw new UnauthorizedException(); diff --git a/src/main/java/de/thm/arsnova/controller/v2/FeedbackController.java b/src/main/java/de/thm/arsnova/controller/v2/FeedbackController.java new file mode 100644 index 0000000000000000000000000000000000000000..3374251f6521b08aa0d4148b99fd529aa5e06c76 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/FeedbackController.java @@ -0,0 +1,112 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.model.Feedback; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.service.FeedbackService; +import de.thm.arsnova.service.RoomService; +import de.thm.arsnova.service.UserService; +import de.thm.arsnova.web.DeprecatedApi; +import de.thm.arsnova.websocket.ArsnovaSocketioServerImpl; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +/** + * Handles requests concerning the user's feedback, i.e., "too fast" or "faster, please". This HTTP API is + * deprecated in favor of the socket implementation. + * + * @see ArsnovaSocketioServerImpl + */ +@RestController("v2FeedbackController") +@RequestMapping("/v2/session/{shortId}") +public class FeedbackController extends AbstractController { + @Autowired + private FeedbackService feedbackService; + + @Autowired + private RoomService roomService; + + @Autowired + private UserService userService; + + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/feedback", method = RequestMethod.GET) + public Feedback getFeedback(@PathVariable final String shortId) { + return feedbackService.getByRoomId(roomService.getIdByShortId(shortId)); + } + + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/myfeedback", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getMyFeedback(@PathVariable final String shortId) { + String roomId = roomService.getIdByShortId(shortId); + Integer value = feedbackService.getByRoomIdAndUser(roomId, userService.getCurrentUser()); + if (value != null && value >= Feedback.MIN_FEEDBACK_TYPE && value <= Feedback.MAX_FEEDBACK_TYPE) { + return value.toString(); + } + throw new NotFoundException(); + } + + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/feedbackcount", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getFeedbackCount(@PathVariable final String shortId) { + return String.valueOf(feedbackService.countFeedbackByRoomId(roomService.getIdByShortId(shortId))); + } + + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/roundedaveragefeedback", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getAverageFeedbackRounded(@PathVariable final String shortId) { + return String.valueOf(feedbackService.calculateRoundedAverageFeedback(roomService.getIdByShortId(shortId))); + } + + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/averagefeedback", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String getAverageFeedback(@PathVariable final String shortId) { + return String.valueOf(feedbackService.calculateAverageFeedback(roomService.getIdByShortId(shortId))); + } + + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/feedback", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public Feedback postFeedback( + @PathVariable final String shortId, + @RequestBody final int value + ) { + String roomId = roomService.getIdByShortId(shortId); + ClientAuthentication user = userService.getCurrentUser(); + feedbackService.save(roomId, value, user); + Feedback feedback = feedbackService.getByRoomId(roomId); + + return feedback; + } +} diff --git a/src/main/java/de/thm/arsnova/controller/v2/LegacyController.java b/src/main/java/de/thm/arsnova/controller/v2/LegacyController.java new file mode 100644 index 0000000000000000000000000000000000000000..5ff4fe6301fbd6b8492eaa4929080d4f5718e07f --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/LegacyController.java @@ -0,0 +1,172 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.service.CommentService; +import de.thm.arsnova.service.ContentService; +import de.thm.arsnova.web.DeprecatedApi; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.ResponseBody; + +/** + * This controller forwards requests from deprecated URLs to their new controller, where the requests are handled. + */ +@Controller("v2LegacyController") +@RequestMapping("/v2") +public class LegacyController extends AbstractController { + + @Autowired + private ContentService contentService; + + @Autowired + private CommentService commentService; + + /* specific routes */ + + @DeprecatedApi + @RequestMapping(value = "/session/mysessions", method = RequestMethod.GET) + public String redirectSessionMy() { + return "forward:/v2/session/?ownedonly=true"; + } + + @DeprecatedApi + @RequestMapping(value = "/session/visitedsessions", method = RequestMethod.GET) + public String redirectSessionVisited() { + return "forward:/v2/session/?visitedonly=true"; + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/question") + public String redirectQuestionByLecturer(@PathVariable final String shortId) { + return String.format("forward:/v2/lecturerquestion/?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/skillquestions", method = RequestMethod.GET) + public String redirectQuestionByLecturerList(@PathVariable final String shortId) { + return String.format("forward:/v2/lecturerquestion/?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/skillquestioncount", method = RequestMethod.GET) + public String redirectQuestionByLecturerCount(@PathVariable final String shortId) { + return String.format("forward:/v2/lecturerquestion/count?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/answercount", method = RequestMethod.GET) + public String redirectQuestionByLecturerAnswerCount(@PathVariable final String shortId) { + return String.format("forward:/v2/lecturerquestion/answercount?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/unanswered", method = RequestMethod.GET) + public String redirectQuestionByLecturerUnnsweredCount(@PathVariable final String shortId) { + return String.format("forward:/v2/lecturerquestion/answercount?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/myanswers", method = RequestMethod.GET) + public String redirectQuestionByLecturerMyAnswers(@PathVariable final String shortId) { + return String.format("forward:/v2/lecturerquestion/myanswers?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/interposed") + public String redirectQuestionByAudience(@PathVariable final String shortId) { + return String.format("forward:/v2/audiencequestion/?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/interposed", method = RequestMethod.DELETE) + @ResponseBody + public void deleteAllInterposedQuestions(@PathVariable final String shortId) { + commentService.deleteByRoomId(shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/interposedcount", method = RequestMethod.GET) + public String redirectQuestionByAudienceCount(@PathVariable final String shortId) { + return String.format("forward:/v2/audiencequestion/count?sessionkey=%s", shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/interposedreadingcount", method = RequestMethod.GET) + public String redirectQuestionByAudienceReadCount(@PathVariable final String shortId) { + return String.format("forward:/v2/audiencequestion/readcount?sessionkey=%s", shortId); + } + + /* generalized routes */ + + @DeprecatedApi + @RequestMapping(value = { "/session/{shortId}/question/{arg1}", "/session/{shortId}/questions/{arg1}" }) + public String redirectQuestionByLecturerWithOneArgument( + @PathVariable final String shortId, + @PathVariable final String arg1 + ) { + return String.format("forward:/v2/lecturerquestion/%s/?sessionkey=%s", arg1, shortId); + } + + @DeprecatedApi + @RequestMapping( + value = { "/session/{shortId}/question/{arg1}/{arg2}", "/session/{shortId}/questions/{arg1}/{arg2}" } + ) + public String redirectQuestionByLecturerWithTwoArguments( + @PathVariable final String shortId, + @PathVariable final String arg1, + @PathVariable final String arg2 + ) { + return String.format("forward:/v2/lecturerquestion/%s/%s/?sessionkey=%s", arg1, arg2, shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/interposed/{arg1}") + public String redirectQuestionByAudienceWithOneArgument( + @PathVariable final String shortId, + @PathVariable final String arg1 + ) { + return String.format("forward:/v2/audiencequestion/%s/?sessionkey=%s", arg1, shortId); + } + + @DeprecatedApi + @RequestMapping(value = "/session/{shortId}/interposed/{arg1}/{arg2}") + public String redirectQuestionByAudienceWithTwoArguments( + @PathVariable final String shortId, + @PathVariable final String arg1, + @PathVariable final String arg2 + ) { + return String.format("forward:/v2/audiencequestion/%s/%s/?sessionkey=%s", arg1, arg2, shortId); + } + + @DeprecatedApi + @RequestMapping(value = { "/whoami", "/whoami.json" }) + public String redirectWhoami() { + return "forward:/v2/auth/whoami"; + } + + @DeprecatedApi + @RequestMapping(value = "/doLogin") + public String redirectLogin() { + return "forward:/v2/auth/login"; + } +} diff --git a/src/main/java/de/thm/arsnova/controller/v2/MotdController.java b/src/main/java/de/thm/arsnova/controller/v2/MotdController.java new file mode 100644 index 0000000000000000000000000000000000000000..416fa0918547bd041b6a20339877e4a301bf6faa --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/MotdController.java @@ -0,0 +1,177 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.model.migration.FromV2Migrator; +import de.thm.arsnova.model.migration.ToV2Migrator; +import de.thm.arsnova.model.migration.v2.Motd; +import de.thm.arsnova.model.migration.v2.MotdList; +import de.thm.arsnova.web.exceptions.ForbiddenException; +import de.thm.arsnova.security.User; +import de.thm.arsnova.service.MotdService; +import de.thm.arsnova.service.RoomService; +import de.thm.arsnova.service.UserService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * + */ +@RestController("v2MotdController") +@RequestMapping("/v2/motd") +@Api(value = "/motd", description = "Message of the Day API") +public class MotdController extends AbstractController { + @Autowired + private MotdService motdService; + + @Autowired + private RoomService roomService; + + @Autowired + private UserService userService; + + @Autowired + private ToV2Migrator toV2Migrator; + + @Autowired + private FromV2Migrator fromV2Migrator; + + @ApiOperation(value = "get messages. if adminview=false, only messages with startdate<clientdate<enddate are returned") + @RequestMapping(value = "/", method = RequestMethod.GET) + @ApiResponses(value = { + @ApiResponse(code = 204, message = HTML_STATUS_204), + @ApiResponse(code = 501, message = HTML_STATUS_501) + }) + public List<Motd> getMotd( + @ApiParam(value = "clientdate", required = false) @RequestParam(value = "clientdate", defaultValue = "") final String clientdate, + @ApiParam(value = "adminview", required = false) @RequestParam(value = "adminview", defaultValue = "false") final Boolean adminview, + @ApiParam(value = "audience", required = false) @RequestParam(value = "audience", defaultValue = "all") final String audience, + @ApiParam(value = "sessionkey", required = false) @RequestParam(value = "sessionkey", required = false) final String roomShortId + ) { + List<de.thm.arsnova.model.Motd> motds; + Date date = new Date(System.currentTimeMillis()); + if (!clientdate.isEmpty()) { + date.setTime(Long.parseLong(clientdate)); + } + String roomId = ""; + if (roomShortId != null) { + roomId = roomService.getIdByShortId(roomShortId); + } + if (adminview) { + motds = roomShortId != null ? + motdService.getAllRoomMotds(roomId) : + motdService.getAdminMotds(); + } else { + motds = roomShortId != null ? + motdService.getCurrentRoomMotds(date, roomId) : + motdService.getCurrentMotds(date, audience); + } + + return motds.stream().map(toV2Migrator::migrate).collect(Collectors.toList()); + } + + @ApiOperation(value = "create a new message of the day", nickname = "createMotd") + @ApiResponses(value = { + @ApiResponse(code = 201, message = HTML_STATUS_201), + @ApiResponse(code = 503, message = HTML_STATUS_503) + }) + @RequestMapping(value = "/", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public Motd postNewMotd( + @ApiParam(value = "current motd", required = true) @RequestBody final Motd motd, + final HttpServletResponse response + ) { + de.thm.arsnova.model.Motd motdV3 = fromV2Migrator.migrate(motd); + String roomId = roomService.getIdByShortId(motd.getSessionkey()); + if (de.thm.arsnova.model.Motd.Audience.ROOM == motdV3.getAudience() && roomId != null) { + motdService.save(roomId, motdV3); + } else { + motdService.save(motdV3); + } + + return toV2Migrator.migrate(motdV3); + } + + @ApiOperation(value = "update a message of the day", nickname = "updateMotd") + @RequestMapping(value = "/{motdId}", method = RequestMethod.PUT) + public Motd updateMotd( + @ApiParam(value = "motdkey from current motd", required = true) @PathVariable final String motdId, + @ApiParam(value = "current motd", required = true) @RequestBody final Motd motd + ) { + de.thm.arsnova.model.Motd motdV3 = fromV2Migrator.migrate(motd); + String roomId = roomService.getIdByShortId(motd.getSessionkey()); + if (motdV3.getAudience() == de.thm.arsnova.model.Motd.Audience.ROOM && roomId != null) { + motdService.update(roomId, motdV3); + } else { + motdService.update(motdV3); + } + + return toV2Migrator.migrate(motdV3); + } + + @ApiOperation(value = "deletes a message of the day", nickname = "deleteMotd") + @RequestMapping(value = "/{motdId}", method = RequestMethod.DELETE) + public void deleteMotd(@ApiParam(value = "Motd-key from the message that shall be deleted", required = true) @PathVariable final String motdId) { + de.thm.arsnova.model.Motd motd = motdService.get(motdId); + if (motd.getAudience() == de.thm.arsnova.model.Motd.Audience.ROOM) { + motdService.deleteByRoomId(motd.getRoomId(), motd); + } else { + motdService.delete(motd); + } + } + + @RequestMapping(value = "/userlist", method = RequestMethod.GET) + public MotdList getAcknowledgedIds(@AuthenticationPrincipal User user, @RequestParam final String username) { + if (user == null || !user.getUsername().equals(username)) { + throw new ForbiddenException(); + } + UserProfile profile = userService.get(user.getId()); + + return toV2Migrator.migrateMotdList(profile); + } + + @RequestMapping(value = "/userlist", method = RequestMethod.PUT) + public void putAcknowledgedIds(@AuthenticationPrincipal User user, @RequestBody final MotdList motdList) { + if (user == null || !user.getUsername().equals(motdList.getUsername())) { + throw new ForbiddenException(); + } + UserProfile profile = userService.get(user.getId()); + profile.setAcknowledgedMotds(fromV2Migrator.migrate(motdList)); + userService.update(profile); + } +} diff --git a/src/main/java/de/thm/arsnova/controller/v2/RoomController.java b/src/main/java/de/thm/arsnova/controller/v2/RoomController.java new file mode 100644 index 0000000000000000000000000000000000000000..9c051b469e06138bf99761f72e4f0ff68558d85f --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/RoomController.java @@ -0,0 +1,523 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.controller.PaginationController; +import de.thm.arsnova.model.migration.FromV2Migrator; +import de.thm.arsnova.model.migration.ToV2Migrator; +import de.thm.arsnova.model.migration.v2.Room; +import de.thm.arsnova.model.migration.v2.RoomFeature; +import de.thm.arsnova.model.migration.v2.RoomInfo; +import de.thm.arsnova.model.transport.ImportExportContainer; +import de.thm.arsnova.model.transport.ScoreStatistics; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.service.RoomService; +import de.thm.arsnova.service.RoomServiceImpl; +import de.thm.arsnova.service.RoomServiceImpl.RoomNameComparator; +import de.thm.arsnova.service.RoomServiceImpl.RoomShortNameComparator; +import de.thm.arsnova.service.UserService; +import de.thm.arsnova.web.DeprecatedApi; +import de.thm.arsnova.web.Pagination; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiResponses; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Handles requests related to ARSnova Rooms. + */ +@RestController("v2RoomController") +@RequestMapping("/v2/session") +@Api(value = "/session", description = "Room (Session) API") +public class RoomController extends PaginationController { + @Autowired + private RoomService roomService; + + @Autowired + private UserService userService; + + @Autowired + private ToV2Migrator toV2Migrator; + + @Autowired + private FromV2Migrator fromV2Migrator; + + @ApiOperation(value = "join a Room", + nickname = "joinRoom") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/{shortId}", method = RequestMethod.GET) + public Room joinRoom( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "Adminflag", required = false) @RequestParam(value = "admin", defaultValue = "false") final boolean admin + ) { + if (admin) { + return toV2Migrator.migrate(roomService.getForAdmin(shortId)); + } else { + return toV2Migrator.migrate(roomService.getByShortId(shortId)); + } + } + + @ApiOperation(value = "deletes a Room", + nickname = "deleteRoom") + @RequestMapping(value = "/{shortId}", method = RequestMethod.DELETE) + public void deleteRoom(@ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId) { + de.thm.arsnova.model.Room room = roomService.getByShortId(shortId); + roomService.deleteCascading(room); + } + + @ApiOperation(value = "count active users", + nickname = "countActiveUsers") + @DeprecatedApi + @Deprecated + @RequestMapping(value = "/{shortId}/activeusercount", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) + public String countActiveUsers(@ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId) { + return String.valueOf(roomService.activeUsers(roomService.getIdByShortId(shortId))); + } + + @ApiOperation(value = "Creates a new Room and returns the Room's data", + nickname = "postNewRoom") + @ApiResponses(value = { + @ApiResponse(code = 201, message = HTML_STATUS_201), + @ApiResponse(code = 503, message = HTML_STATUS_503) + }) + @RequestMapping(value = "/", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public Room postNewRoom(@ApiParam(value = "current Room", required = true) @RequestBody final Room room, final HttpServletResponse response) { + /* FIXME: migrate LMS course support + if (room != null && room.isCourseSession()) { + final List<Course> courses = new ArrayList<>(); + final Course course = new Course(); + course.setId(room.getCourseId()); + courses.add(course); + final int sessionCount = roomService.countSessionsByCourses(courses); + if (sessionCount > 0) { + final String appendix = " (" + (sessionCount + 1) + ")"; + room.setName(room.getName() + appendix); + room.setAbbreviation(room.getAbbreviation() + appendix); + } + } + */ + + return toV2Migrator.migrate(roomService.create(fromV2Migrator.migrate(room))); + } + + @ApiOperation(value = "updates a Room", + nickname = "postNewRoom") + @RequestMapping(value = "/{shortId}", method = RequestMethod.PUT) + public Room updateRoom( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "current room", required = true) @RequestBody final Room room + ) { + return toV2Migrator.migrate(roomService.update(fromV2Migrator.migrate(room))); + } + + @ApiOperation(value = "change the Room creator (owner)", nickname = "changeRoomCreator") + @RequestMapping(value = "/{shortId}/changecreator", method = RequestMethod.PUT) + public Room changeRoomCreator( + @ApiParam(value = "Room-key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "new Room creator", required = true) @RequestBody final String newCreator + ) { + return toV2Migrator.migrate(roomService.updateCreator(roomService.getIdByShortId(shortId), newCreator)); + } + + @ApiOperation(value = "Retrieves a list of Rooms", + nickname = "getRooms") + @ApiResponses(value = { + @ApiResponse(code = 204, message = HTML_STATUS_204), + @ApiResponse(code = 501, message = HTML_STATUS_501) + }) + @RequestMapping(value = "/", method = RequestMethod.GET) + @Pagination + public List<Room> getRooms( + @ApiParam(value = "ownedOnly", required = true) @RequestParam(value = "ownedonly", defaultValue = "false") final boolean ownedOnly, + @ApiParam(value = "visitedOnly", required = true) @RequestParam(value = "visitedonly", defaultValue = "false") final boolean visitedOnly, + @ApiParam(value = "sortby", required = true) @RequestParam(value = "sortby", defaultValue = "name") final String sortby, + @ApiParam(value = "for a given username. admin rights needed", required = false) @RequestParam(value = + "username", defaultValue = "") final String username, + final HttpServletResponse response + ) { + List<de.thm.arsnova.model.Room> rooms; + + if (!"".equals(username)) { + final String userId = userService.getByUsername(username).getId(); + try { + if (ownedOnly && !visitedOnly) { + rooms = roomService.getUserRooms(userId); + } else if (visitedOnly && !ownedOnly) { + rooms = roomService.getUserRoomHistory(username); + } else { + response.setStatus(HttpStatus.NOT_IMPLEMENTED.value()); + return null; + } + } catch (final AccessDeniedException e) { + throw new UnauthorizedException(); + } + } else { + /* TODO implement all parameter combinations, implement use of user parameter */ + try { + if (ownedOnly && !visitedOnly) { + rooms = roomService.getMyRooms(offset, limit); + } else if (visitedOnly && !ownedOnly) { + rooms = roomService.getMyRoomHistory(offset, limit); + } else { + response.setStatus(HttpStatus.NOT_IMPLEMENTED.value()); + return null; + } + } catch (final AccessDeniedException e) { + throw new UnauthorizedException(); + } + } + + if (rooms == null || rooms.isEmpty()) { + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + return null; + } + + if ("shortname".equals(sortby)) { + Collections.sort(rooms, new RoomShortNameComparator()); + } else { + Collections.sort(rooms, new RoomServiceImpl.RoomNameComparator()); + } + + return rooms.stream().map(toV2Migrator::migrate).collect(Collectors.toList()); + } + + /** + * Returns a list of my own Rooms with only the necessary information like name, keyword, or counters. + */ + @ApiOperation(value = "Retrieves a Room", + nickname = "getMyRooms") + @ApiResponses(value = { + @ApiResponse(code = 204, message = HTML_STATUS_204) + }) + @RequestMapping(value = "/", method = RequestMethod.GET, params = "statusonly=true") + @Pagination + public List<RoomInfo> getMyRooms( + @ApiParam(value = "visitedOnly", required = true) @RequestParam(value = "visitedonly", defaultValue = "false") final boolean visitedOnly, + @ApiParam(value = "sort by", required = false) @RequestParam(value = "sortby", defaultValue = "name") final String sortby, + final HttpServletResponse response + ) { + List<de.thm.arsnova.model.Room> rooms; + if (!visitedOnly) { + rooms = roomService.getMyRoomsInfo(offset, limit); + } else { + rooms = roomService.getMyRoomHistoryInfo(offset, limit); + } + + if (rooms == null || rooms.isEmpty()) { + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + return null; + } + + if ("shortname".equals(sortby)) { + Collections.sort(rooms, new RoomShortNameComparator()); + } else { + Collections.sort(rooms, new RoomNameComparator()); + } + + return rooms.stream().map(toV2Migrator::migrateStats).collect(Collectors.toList()); + } + + @ApiOperation(value = "Retrieves all public pool Rooms for the current user", + nickname = "getMyPublicPoolRooms") + @ApiResponses(value = { + @ApiResponse(code = 204, message = HTML_STATUS_204) + }) + @RequestMapping(value = "/publicpool", method = RequestMethod.GET, params = "statusonly=true") + public List<RoomInfo> getMyPublicPoolRooms( + final HttpServletResponse response + ) { + List<de.thm.arsnova.model.Room> rooms = roomService.getMyPublicPoolRoomsInfo(); + + if (rooms == null || rooms.isEmpty()) { + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + return null; + } + + return rooms.stream().map(toV2Migrator::migrateStats).collect(Collectors.toList()); + } + + @ApiOperation(value = "Retrieves all public pool Rooms", + nickname = "getMyPublicPoolRooms") + @ApiResponses(value = { + @ApiResponse(code = 204, message = HTML_STATUS_204) + }) + @RequestMapping(value = "/publicpool", method = RequestMethod.GET) + public List<Room> getPublicPoolRooms( + final HttpServletResponse response + ) { + List<de.thm.arsnova.model.Room> rooms = roomService.getPublicPoolRoomsInfo(); + + if (rooms == null || rooms.isEmpty()) { + response.setStatus(HttpServletResponse.SC_NO_CONTENT); + return null; + } + + return rooms.stream().map(toV2Migrator::migrate).collect(Collectors.toList()); + } + + @ApiOperation(value = "imports a Room", + nickname = "importRoom") + @RequestMapping(value = "/import", method = RequestMethod.POST) + public Room importRoom( + @ApiParam(value = "current Room", required = true) @RequestBody final ImportExportContainer room, + final HttpServletResponse response + ) { + return toV2Migrator.migrate(roomService.importRooms(room)); + } + + @ApiOperation(value = "export Rooms", nickname = "exportRoom") + @RequestMapping(value = "/export", method = RequestMethod.GET) + public List<ImportExportContainer> getExport( + @ApiParam(value = "Room-Key", required = true) @RequestParam(value = "sessionkey", defaultValue = "") final List<String> shortIds, + @ApiParam(value = "wether statistics shall be exported", required = true) @RequestParam(value = "withAnswerStatistics", defaultValue = "false") final Boolean withAnswerStatistics, + @ApiParam(value = "wether comments shall be exported", required = true) @RequestParam(value = "withFeedbackQuestions", defaultValue = "false") final Boolean withFeedbackQuestions, + final HttpServletResponse response + ) { + List<ImportExportContainer> rooms = new ArrayList<>(); + ImportExportContainer temp; + for (String shortId : shortIds) { + String id = roomService.getIdByShortId(shortId); + roomService.setActive(id, false); + temp = roomService.exportRoom(id, withAnswerStatistics, withFeedbackQuestions); + if (temp != null) { + rooms.add(temp); + } + roomService.setActive(id, true); + } + return rooms; + } + + @ApiOperation(value = "copy a Rooms to the public pool if enabled") + @RequestMapping(value = "/{shortId}/copytopublicpool", method = RequestMethod.POST) + public Room copyToPublicPool( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "public pool attributes for Room", required = true) @RequestBody final ImportExportContainer.PublicPool publicPool + ) { + String id = roomService.getIdByShortId(shortId); + roomService.setActive(id, false); + de.thm.arsnova.model.Room roomInfo = roomService.copyRoomToPublicPool(shortId, publicPool); + roomService.setActive(id, true); + + return toV2Migrator.migrate(roomInfo); + } + + + @ApiOperation(value = "Locks or unlocks a Room", + nickname = "lockRoom") + @ApiResponses(value = { + @ApiResponse(code = 404, message = HTML_STATUS_404) + }) + @RequestMapping(value = "/{shortId}/lock", method = RequestMethod.POST) + public Room lockRoom( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "lock", required = true) @RequestParam(required = false) final Boolean lock, + final HttpServletResponse response + ) { + if (lock != null) { + return toV2Migrator.migrate(roomService.setActive(roomService.getIdByShortId(shortId), lock)); + } + response.setStatus(HttpStatus.NOT_FOUND.value()); + return null; + } + + @ApiOperation(value = "retrieves a value for the score", + nickname = "getLearningProgress") + @RequestMapping(value = "/{shortId}/learningprogress", method = RequestMethod.GET) + public ScoreStatistics getLearningProgress( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "type", required = false) @RequestParam(value = "type", defaultValue = "questions") final String type, + @ApiParam(value = "question variant", required = false) @RequestParam(value = "questionVariant", required = false) final String questionVariant, + final HttpServletResponse response + ) { + return roomService.getLearningProgress(roomService.getIdByShortId(shortId), type, questionVariant); + } + + @ApiOperation(value = "retrieves a value for the learning progress for the current user", + nickname = "getMyLearningProgress") + @RequestMapping(value = "/{shortId}/mylearningprogress", method = RequestMethod.GET) + public ScoreStatistics getMyLearningProgress( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @RequestParam(value = "type", defaultValue = "questions") final String type, + @RequestParam(value = "questionVariant", required = false) final String questionVariant, + final HttpServletResponse response + ) { + return roomService.getMyLearningProgress(roomService.getIdByShortId(shortId), type, questionVariant); + } + + @ApiOperation(value = "retrieves all Room features", + nickname = "getRoomFeatures") + @RequestMapping(value = "/{shortId}/features", method = RequestMethod.GET) + public RoomFeature getRoomFeatures( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + final HttpServletResponse response + ) { + de.thm.arsnova.model.Room room = roomService.getByShortId(shortId); + return toV2Migrator.migrate(room.getSettings()); + } + + @RequestMapping(value = "/{shortId}/features", method = RequestMethod.PUT) + @ApiOperation(value = "change all Room features", + nickname = "changeRoomFeatures") + public RoomFeature changeRoomFeatures( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "Room feature", required = true) @RequestBody final RoomFeature features, + final HttpServletResponse response + ) { + de.thm.arsnova.model.Room room = roomService.getByShortId(shortId); + room.setSettings(fromV2Migrator.migrate(features)); + roomService.update(room); + + return toV2Migrator.migrate(room.getSettings()); + } + + @RequestMapping(value = "/{shortId}/lockfeedbackinput", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE) + @ApiOperation(value = "locks input of user live feedback", + nickname = "lockFeedbackInput") + public String lockFeedbackInput( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "lock", required = true) @RequestParam(required = true) final Boolean lock, + final HttpServletResponse response + ) { + return String.valueOf(roomService.lockFeedbackInput(roomService.getIdByShortId(shortId), lock)); + } + + @RequestMapping(value = "/{shortId}/flipflashcards", method = RequestMethod.POST, produces = MediaType.TEXT_PLAIN_VALUE) + @ApiOperation(value = "flip all flashcards in Room", + nickname = "lockFeedbackInput") + public String flipFlashcards( + @ApiParam(value = "Room-Key from current Room", required = true) @PathVariable final String shortId, + @ApiParam(value = "flip", required = true) @RequestParam(required = true) final Boolean flip, + final HttpServletResponse response + ) { + return String.valueOf(roomService.flipFlashcards(roomService.getIdByShortId(shortId), flip)); + } + + /* internal redirections */ + + @RequestMapping(value = "/{shortId}/lecturerquestion") + public String redirectLecturerQuestion( + @PathVariable final String shortId, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/lecturerquestion/?sessionkey=%s", shortId); + } + + @RequestMapping(value = "/{shortId}/lecturerquestion/{arg1}") + public String redirectLecturerQuestionWithOneArgument( + @PathVariable final String shortId, + @PathVariable final String arg1, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/lecturerquestion/%s/?sessionkey=%s", arg1, shortId); + } + + @RequestMapping(value = "/{shortId}/lecturerquestion/{arg1}/{arg2}") + public String redirectLecturerQuestionWithTwoArguments( + @PathVariable final String shortId, + @PathVariable final String arg1, + @PathVariable final String arg2, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/lecturerquestion/%s/%s/?sessionkey=%s", arg1, arg2, shortId); + } + + @RequestMapping(value = "/{shortId}/lecturerquestion/{arg1}/{arg2}/{arg3}") + public String redirectLecturerQuestionWithThreeArguments( + @PathVariable final String shortId, + @PathVariable final String arg1, + @PathVariable final String arg2, + @PathVariable final String arg3, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/lecturerquestion/%s/%s/%s/?sessionkey=%s", arg1, arg2, arg3, shortId); + } + + @RequestMapping(value = "/{shortId}/audiencequestion") + public String redirectAudienceQuestion( + @PathVariable final String shortId, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/audiencequestion/?sessionkey=%s", shortId); + } + + @RequestMapping(value = "/{shortId}/audiencequestion/{arg1}") + public String redirectAudienceQuestionWithOneArgument( + @PathVariable final String shortId, + @PathVariable final String arg1, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/audiencequestion/%s/?sessionkey=%s", arg1, shortId); + } + + @RequestMapping(value = "/{shortId}/audiencequestion/{arg1}/{arg2}") + public String redirectAudienceQuestionWithTwoArguments( + @PathVariable final String shortId, + @PathVariable final String arg1, + @PathVariable final String arg2, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/audiencequestion/%s/%s/?sessionkey=%s", arg1, arg2, shortId); + } + + @RequestMapping(value = "/{shortId}/audiencequestion/{arg1}/{arg2}/{arg3}") + public String redirectAudienceQuestionWithThreeArguments( + @PathVariable final String shortId, + @PathVariable final String arg1, + @PathVariable final String arg2, + @PathVariable final String arg3, + final HttpServletResponse response + ) { + response.addHeader(X_FORWARDED, "1"); + + return String.format("forward:/audiencequestion/%s/%s/%s/?sessionkey=%s", arg1, arg2, arg3, shortId); + } +} diff --git a/src/main/java/de/thm/arsnova/controller/SocketController.java b/src/main/java/de/thm/arsnova/controller/v2/SocketController.java similarity index 84% rename from src/main/java/de/thm/arsnova/controller/SocketController.java rename to src/main/java/de/thm/arsnova/controller/v2/SocketController.java index c01a466e16802ad4bd03b63c3e15de745819b328..ab9afeef05b24bf5d2dfc09f9e39a2d11ea8644d 100644 --- a/src/main/java/de/thm/arsnova/controller/SocketController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/SocketController.java @@ -15,11 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.controller; +package de.thm.arsnova.controller.v2; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.services.UserService; -import de.thm.arsnova.services.UserSessionService; +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.service.UserService; import de.thm.arsnova.websocket.ArsnovaSocketioServer; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @@ -43,17 +44,14 @@ import java.util.UUID; /** * Initiates the socket communication. */ -@RestController -@RequestMapping("/socket") -@Api(value = "/socket", description = "the Socket API") +@RestController("v2SocketController") +@RequestMapping("/v2/socket") +@Api(value = "/socket", description = "WebSocket Initialization API") public class SocketController extends AbstractController { @Autowired private UserService userService; - @Autowired - private UserSessionService userSessionService; - @Autowired private ArsnovaSocketioServer server; @@ -74,20 +72,19 @@ public class SocketController extends AbstractController { response.setStatus(HttpStatus.BAD_REQUEST.value()); return; } - User u = userService.getCurrentUser(); + ClientAuthentication u = userService.getCurrentUser(); if (null == u) { logger.debug("Client {} requested to assign Websocket session but has not authenticated.", socketid); response.setStatus(HttpStatus.FORBIDDEN.value()); return; } - userService.putUser2SocketId(UUID.fromString(socketid), u); - userSessionService.setSocketId(UUID.fromString(socketid)); + userService.putUserToSocketId(UUID.fromString(socketid), u); response.setStatus(HttpStatus.NO_CONTENT.value()); } @ApiOperation(value = "retrieves a socket url", nickname = "getSocketUrl") - @RequestMapping(value = "/url", method = RequestMethod.GET) + @RequestMapping(value = "/url", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE) public String getSocketUrl(final HttpServletRequest request) { return (server.isUseSSL() ? "https://" : "http://") + request.getServerName() + ":" + server.getPortNumber(); } diff --git a/src/main/java/de/thm/arsnova/controller/StatisticsController.java b/src/main/java/de/thm/arsnova/controller/v2/StatisticsController.java similarity index 69% rename from src/main/java/de/thm/arsnova/controller/StatisticsController.java rename to src/main/java/de/thm/arsnova/controller/v2/StatisticsController.java index 2f0962cc0943eda0b91002d0968d15c2e4c0f20e..a4529ae54d1a88a1b8c9f1924701ebd3fa67375b 100644 --- a/src/main/java/de/thm/arsnova/controller/StatisticsController.java +++ b/src/main/java/de/thm/arsnova/controller/v2/StatisticsController.java @@ -15,15 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.controller; +package de.thm.arsnova.controller.v2; -import de.thm.arsnova.entities.Statistics; -import de.thm.arsnova.services.StatisticsService; +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.model.Statistics; +import de.thm.arsnova.service.StatisticsService; import de.thm.arsnova.web.CacheControl; import de.thm.arsnova.web.DeprecatedApi; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; @@ -31,8 +33,9 @@ import org.springframework.web.bind.annotation.RestController; /** * Allows retrieval of several statistics such as the number of active users. */ -@RestController -@Api(value = "/statistics", description = "the Statistic API") +@RestController("v2StatisticsController") +@Api(value = "/statistics", description = "Statistics API") +@RequestMapping("/v2/statistics") public class StatisticsController extends AbstractController { @Autowired @@ -40,7 +43,7 @@ public class StatisticsController extends AbstractController { @ApiOperation(value = "Retrieves global statistics", nickname = "getStatistics") - @RequestMapping(method = RequestMethod.GET, value = "/statistics") + @RequestMapping(method = RequestMethod.GET, value = "/") @CacheControl(maxAge = 60, policy = CacheControl.Policy.PUBLIC) public Statistics getStatistics() { return statisticsService.getStatistics(); @@ -50,27 +53,27 @@ public class StatisticsController extends AbstractController { nickname = "countActiveUsers") @DeprecatedApi @Deprecated - @RequestMapping(method = RequestMethod.GET, value = "/statistics/activeusercount", produces = "text/plain") + @RequestMapping(method = RequestMethod.GET, value = "/activeusercount", produces = MediaType.TEXT_PLAIN_VALUE) public String countActiveUsers() { - return Integer.toString(statisticsService.getStatistics().getActiveUsers()); + return String.valueOf(statisticsService.getStatistics().getActiveUsers()); } @ApiOperation(value = "Retrieves the amount of all currently logged in users", nickname = "countLoggedInUsers") @DeprecatedApi @Deprecated - @RequestMapping(method = RequestMethod.GET, value = "/statistics/loggedinusercount", produces = "text/plain") + @RequestMapping(method = RequestMethod.GET, value = "/loggedinusercount", produces = MediaType.TEXT_PLAIN_VALUE) public String countLoggedInUsers() { - return Integer.toString(statisticsService.getStatistics().getLoggedinUsers()); + return String.valueOf(statisticsService.getStatistics().getLoggedinUsers()); } @ApiOperation(value = "Retrieves the total amount of all sessions", nickname = "countSessions") @DeprecatedApi @Deprecated - @RequestMapping(method = RequestMethod.GET, value = "/statistics/sessioncount", produces = "text/plain") + @RequestMapping(method = RequestMethod.GET, value = "/sessioncount", produces = MediaType.TEXT_PLAIN_VALUE) public String countSessions() { - return Integer.toString(statisticsService.getStatistics().getOpenSessions() + return String.valueOf(statisticsService.getStatistics().getOpenSessions() + statisticsService.getStatistics().getClosedSessions()); } } diff --git a/src/main/java/de/thm/arsnova/controller/v2/UserController.java b/src/main/java/de/thm/arsnova/controller/v2/UserController.java new file mode 100644 index 0000000000000000000000000000000000000000..ea20a0782a7948eaf8ef9ccd8892280037273aa3 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/UserController.java @@ -0,0 +1,107 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.controller.v2; + +import de.thm.arsnova.controller.AbstractController; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Handles requests related to ARSnova's own user registration and login process. + */ +@Controller("v2UserController") +@RequestMapping("/v2/user") +public class UserController extends AbstractController { + @Autowired + private DaoAuthenticationProvider daoProvider; + + @Autowired + private UserService userService; + + @RequestMapping(value = "/register", method = RequestMethod.POST) + public void register(@RequestParam final String username, + @RequestParam final String password, + final HttpServletRequest request, final HttpServletResponse response) { + if (null != userService.create(username, password)) { + return; + } + + /* TODO: Improve error handling: send reason to client */ + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @RequestMapping(value = "/{username}/activate", method = { RequestMethod.POST, + RequestMethod.GET }) + public void activate( + @PathVariable final String username, + @RequestParam final String key, final HttpServletRequest request, + final HttpServletResponse response) { + UserProfile userProfile = userService.getByUsername(username); + if (null != userProfile && key.equals(userProfile.getAccount().getActivationKey())) { + userProfile.getAccount().setActivationKey(null); + userService.update(userProfile); + + return; + } + + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @RequestMapping(value = "/{username}/", method = RequestMethod.DELETE) + public void activate( + @PathVariable final String username, + final HttpServletRequest request, + final HttpServletResponse response) { + if (null == userService.deleteByUsername(username)) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + } + } + + @RequestMapping(value = "/{username}/resetpassword", method = RequestMethod.POST) + public void resetPassword( + @PathVariable final String username, + @RequestParam(required = false) final String key, + @RequestParam(required = false) final String password, + final HttpServletRequest request, + final HttpServletResponse response) { + UserProfile userProfile = userService.getByUsername(username); + if (null == userProfile) { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + + return; + } + + if (null != key) { + if (!userService.resetPassword(userProfile, key, password)) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + } else { + userService.initiatePasswordReset(username); + } + } +} diff --git a/src/main/java/de/thm/arsnova/controller/v2/WelcomeController.java b/src/main/java/de/thm/arsnova/controller/v2/WelcomeController.java new file mode 100644 index 0000000000000000000000000000000000000000..ca93646db03b9811c48bee1c8deb2a502ed47fb5 --- /dev/null +++ b/src/main/java/de/thm/arsnova/controller/v2/WelcomeController.java @@ -0,0 +1,13 @@ +package de.thm.arsnova.controller.v2; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller("v2WelcomeController") +@RequestMapping("/v2") +public class WelcomeController { + @RequestMapping(value = "/") + public String forwardHome() { + return "forward:/"; + } +} diff --git a/src/main/java/de/thm/arsnova/entities/User.java b/src/main/java/de/thm/arsnova/entities/User.java deleted file mode 100644 index 8d17c2df476b8f9d996c96f5cb9ff4f4dc33fa2d..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/entities/User.java +++ /dev/null @@ -1,148 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.entities; - -import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; -import de.thm.arsnova.services.UserSessionService; -import org.jasig.cas.client.authentication.AttributePrincipal; -import org.pac4j.oauth.profile.facebook.FacebookProfile; -import org.pac4j.oauth.profile.google2.Google2Profile; -import org.pac4j.oauth.profile.twitter.TwitterProfile; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; - -import java.io.Serializable; - -/** - * Represents a user. - */ -public class User implements Serializable { - public static final String GOOGLE = "google"; - public static final String TWITTER = "twitter"; - public static final String FACEBOOK = "facebook"; - public static final String THM = "thm"; - public static final String LDAP = "ldap"; - public static final String ARSNOVA = "arsnova"; - public static final String ANONYMOUS = "anonymous"; - public static final String GUEST = "guest"; - - public static final String FACEBOOK_LINK_PATTERN = "https://www.facebook.com/app_scoped_user_id/%s/"; - - private static final long serialVersionUID = 1L; - private String username; - private String type; - private UserSessionService.Role role; - private boolean isAdmin; - - public User(Google2Profile profile) { - setUsername(profile.getEmail()); - setType(User.GOOGLE); - } - - public User(TwitterProfile profile) { - setUsername(profile.getUsername()); - setType(User.TWITTER); - } - - public User(FacebookProfile profile) { - /* A URL is built for backwards compatibility. */ - setUsername(String.format(FACEBOOK_LINK_PATTERN, profile.getId())); - setType(User.FACEBOOK); - } - - public User(AttributePrincipal principal) { - setUsername(principal.getName()); - setType(User.THM); - } - - public User(AnonymousAuthenticationToken token) { - setUsername(User.ANONYMOUS); - setType(User.ANONYMOUS); - } - - public User(UsernamePasswordAuthenticationToken token) { - setUsername(token.getName()); - setType(LDAP); - } - - @JsonView(View.Public.class) - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - @JsonView(View.Public.class) - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public UserSessionService.Role getRole() { - return role; - } - - public void setRole(UserSessionService.Role role) { - this.role = role; - } - - public boolean hasRole(UserSessionService.Role role) { - return this.role == role; - } - - public void setAdmin(boolean a) { - this.isAdmin = a; - } - - @JsonView(View.Public.class) - public boolean isAdmin() { - return this.isAdmin; - } - - @Override - public String toString() { - return "User [username=" + username + ", type=" + type + "]"; - } - - @Override - public int hashCode() { - // See http://stackoverflow.com/a/113600 - final int theAnswer = 42; - final int theOthers = 37; - - int result = theAnswer; - result = theOthers * result + this.username.hashCode(); - return theOthers * result + this.type.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null || !obj.getClass().equals(this.getClass())) { - return false; - } - User other = (User) obj; - return this.username.equals(other.username) && this.type.equals(other.type); - } - -} diff --git a/src/main/java/de/thm/arsnova/events/ArsnovaEvent.java b/src/main/java/de/thm/arsnova/event/ArsnovaEvent.java similarity index 97% rename from src/main/java/de/thm/arsnova/events/ArsnovaEvent.java rename to src/main/java/de/thm/arsnova/event/ArsnovaEvent.java index 8fab76461697c211118415fe35be7d77dbbe02e5..8c5ac9498433190c1b722cf73a3363016c82405e 100644 --- a/src/main/java/de/thm/arsnova/events/ArsnovaEvent.java +++ b/src/main/java/de/thm/arsnova/event/ArsnovaEvent.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; import org.springframework.context.ApplicationEvent; diff --git a/src/main/java/de/thm/arsnova/events/ArsnovaEventVisitor.java b/src/main/java/de/thm/arsnova/event/ArsnovaEventVisitor.java similarity index 90% rename from src/main/java/de/thm/arsnova/events/ArsnovaEventVisitor.java rename to src/main/java/de/thm/arsnova/event/ArsnovaEventVisitor.java index 6f79e6413a7f71e661f6a5a4e75b5858e3960c50..4587c4284920c0038a4a3a4f410a9fb913321899 100644 --- a/src/main/java/de/thm/arsnova/events/ArsnovaEventVisitor.java +++ b/src/main/java/de/thm/arsnova/event/ArsnovaEventVisitor.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; /** * Listeners wanting to receive ARSnova's internal events should implement this interface. @@ -52,9 +52,9 @@ public interface ArsnovaEventVisitor { void visit(NewFeedbackEvent newFeedbackEvent); - void visit(DeleteFeedbackForSessionsEvent deleteFeedbackEvent); + void visit(DeleteFeedbackForRoomsEvent deleteFeedbackEvent); - void visit(StatusSessionEvent statusSessionEvent); + void visit(StatusRoomEvent statusSessionEvent); void visit(ChangeScoreEvent changeLearningProgress); @@ -66,9 +66,9 @@ public interface ArsnovaEventVisitor { void visit(PiRoundResetEvent piRoundResetEvent); - void visit(NewSessionEvent newSessionEvent); + void visit(NewRoomEvent newSessionEvent); - void visit(DeleteSessionEvent deleteSessionEvent); + void visit(DeleteRoomEvent deleteSessionEvent); void visit(LockVoteEvent lockVoteEvent); diff --git a/src/main/java/de/thm/arsnova/events/ChangeScoreEvent.java b/src/main/java/de/thm/arsnova/event/ChangeScoreEvent.java similarity index 82% rename from src/main/java/de/thm/arsnova/events/ChangeScoreEvent.java rename to src/main/java/de/thm/arsnova/event/ChangeScoreEvent.java index 54b2c9fe7ea6e61254aab129a3837c52fdd3c283..e67fc341e88ff29f1307420f4c4324610f9d1007 100644 --- a/src/main/java/de/thm/arsnova/events/ChangeScoreEvent.java +++ b/src/main/java/de/thm/arsnova/event/ChangeScoreEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever a score related value changes. */ -public class ChangeScoreEvent extends SessionEvent { +public class ChangeScoreEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public ChangeScoreEvent(Object source, Session session) { - super(source, session); + public ChangeScoreEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/DeleteAllLectureAnswersEvent.java b/src/main/java/de/thm/arsnova/event/DeleteAllLectureAnswersEvent.java similarity index 81% rename from src/main/java/de/thm/arsnova/events/DeleteAllLectureAnswersEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteAllLectureAnswersEvent.java index bdd05354ed2a2d91e9f384773f46562c4f5474f9..210ede956a073bcec5187689831ca07fa8b623a9 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteAllLectureAnswersEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteAllLectureAnswersEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever all answers of all lecture questions of a session are deleted. */ -public class DeleteAllLectureAnswersEvent extends SessionEvent { +public class DeleteAllLectureAnswersEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public DeleteAllLectureAnswersEvent(Object source, Session session) { - super(source, session); + public DeleteAllLectureAnswersEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/DeleteAllPreparationAnswersEvent.java b/src/main/java/de/thm/arsnova/event/DeleteAllPreparationAnswersEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/DeleteAllPreparationAnswersEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteAllPreparationAnswersEvent.java index 557be91aea880916b392416b4868c7d3898e5ced..94d7a67c445e2ae7840853496b752879ea4a19ef 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteAllPreparationAnswersEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteAllPreparationAnswersEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever all answers of all preparation questions of a session are deleted. */ -public class DeleteAllPreparationAnswersEvent extends SessionEvent { +public class DeleteAllPreparationAnswersEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public DeleteAllPreparationAnswersEvent(Object source, Session session) { - super(source, session); + public DeleteAllPreparationAnswersEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/DeleteAllQuestionsAnswersEvent.java b/src/main/java/de/thm/arsnova/event/DeleteAllQuestionsAnswersEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/DeleteAllQuestionsAnswersEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteAllQuestionsAnswersEvent.java index f716671ed96b85f39004125dbfedf7ac11ed9852..eb587c21300acfb74c21367a55670e2b6d8b6287 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteAllQuestionsAnswersEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteAllQuestionsAnswersEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever all answers of all questions of a session are deleted. */ -public class DeleteAllQuestionsAnswersEvent extends SessionEvent { +public class DeleteAllQuestionsAnswersEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public DeleteAllQuestionsAnswersEvent(Object source, Session session) { - super(source, session); + public DeleteAllQuestionsAnswersEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/DeleteAllQuestionsEvent.java b/src/main/java/de/thm/arsnova/event/DeleteAllQuestionsEvent.java similarity index 83% rename from src/main/java/de/thm/arsnova/events/DeleteAllQuestionsEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteAllQuestionsEvent.java index 6a2872eea473049180647078a781a935290ee375..7dd76e612659c55ee4fcf9f43feefafe3428a44f 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteAllQuestionsEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteAllQuestionsEvent.java @@ -15,20 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever all questions of a session are deleted. Note that this implies that all answers are deleted as well, * even though the specific answer events are not fired. */ -public class DeleteAllQuestionsEvent extends SessionEvent { +public class DeleteAllQuestionsEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public DeleteAllQuestionsEvent(Object source, Session session) { - super(source, session); + public DeleteAllQuestionsEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java b/src/main/java/de/thm/arsnova/event/DeleteAnswerEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteAnswerEvent.java index 5b303ac425cac8fba8ace17fa91c21b21acfa890..eb9f816bffc8a8db93820c77ef0f69ad47e9ebc9 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteAnswerEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteAnswerEvent.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; /** * Fires whenever a single answer is deleted. */ -public class DeleteAnswerEvent extends SessionEvent { +public class DeleteAnswerEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Content content; - public DeleteAnswerEvent(Object source, Session session, Content content) { - super(source, session); + public DeleteAnswerEvent(Object source, Room room, Content content) { + super(source, room); this.content = content; } diff --git a/src/main/java/de/thm/arsnova/events/DeleteCommentEvent.java b/src/main/java/de/thm/arsnova/event/DeleteCommentEvent.java similarity index 79% rename from src/main/java/de/thm/arsnova/events/DeleteCommentEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteCommentEvent.java index ff812418e496dfc957257cdf6823f9b12d8fab22..f62f3c64e39bb1cd39b583208ed7f3154fdc72f1 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteCommentEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteCommentEvent.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.Room; /** * Fires whenever an comment is deleted. */ -public class DeleteCommentEvent extends SessionEvent { +public class DeleteCommentEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Comment comment; - public DeleteCommentEvent(Object source, Session session, Comment comment) { - super(source, session); + public DeleteCommentEvent(Object source, Room room, Comment comment) { + super(source, room); this.comment = comment; } diff --git a/src/main/java/de/thm/arsnova/events/DeleteFeedbackForSessionsEvent.java b/src/main/java/de/thm/arsnova/event/DeleteFeedbackForRoomsEvent.java similarity index 70% rename from src/main/java/de/thm/arsnova/events/DeleteFeedbackForSessionsEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteFeedbackForRoomsEvent.java index d463dd85d2291f4b44937e3cdb0a0d72670d58f0..5942e19adedc838bcd9dbe8ec1169c85b01ad201 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteFeedbackForSessionsEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteFeedbackForRoomsEvent.java @@ -15,35 +15,35 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.Room; import java.util.Set; /** * Fires whenever the feedback of a specific user has been reset. */ -public class DeleteFeedbackForSessionsEvent extends ArsnovaEvent { +public class DeleteFeedbackForRoomsEvent extends ArsnovaEvent { private static final long serialVersionUID = 1L; - private final Set<Session> sessions; + private final Set<Room> sessions; - private final User user; + private final ClientAuthentication user; - public DeleteFeedbackForSessionsEvent(Object source, Set<Session> sessions, User user) { + public DeleteFeedbackForRoomsEvent(Object source, Set<Room> rooms, ClientAuthentication user) { super(source); - this.sessions = sessions; + this.sessions = rooms; this.user = user; } - public Set<Session> getSessions() { + public Set<Room> getSessions() { return sessions; } - public User getUser() { + public ClientAuthentication getUser() { return user; } diff --git a/src/main/java/de/thm/arsnova/events/DeleteQuestionEvent.java b/src/main/java/de/thm/arsnova/event/DeleteQuestionEvent.java similarity index 79% rename from src/main/java/de/thm/arsnova/events/DeleteQuestionEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteQuestionEvent.java index 2cf1a83139c185d6b41652dea558667d24d0c0e1..b2a40f6cfb769c00ed2088e03ce7e5b078b6f8de 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteQuestionEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteQuestionEvent.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; /** * Fires whenever a content is deleted. */ -public class DeleteQuestionEvent extends SessionEvent { +public class DeleteQuestionEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Content content; - public DeleteQuestionEvent(Object source, Session session, Content content) { - super(source, session); + public DeleteQuestionEvent(Object source, Room room, Content content) { + super(source, room); this.content = content; } diff --git a/src/main/java/de/thm/arsnova/events/DeleteSessionEvent.java b/src/main/java/de/thm/arsnova/event/DeleteRoomEvent.java similarity index 84% rename from src/main/java/de/thm/arsnova/events/DeleteSessionEvent.java rename to src/main/java/de/thm/arsnova/event/DeleteRoomEvent.java index 9d1a60d92618fc5cdf0c3428fcfa57a144a58ce7..2e470d680689775efbed767fe7438031820a15e7 100644 --- a/src/main/java/de/thm/arsnova/events/DeleteSessionEvent.java +++ b/src/main/java/de/thm/arsnova/event/DeleteRoomEvent.java @@ -15,20 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever a session is deleted. Note that this implies that all related data such as comments, * lecturer questions, and answers are deleted as well, even though those events are not fired. */ -public class DeleteSessionEvent extends SessionEvent { +public class DeleteRoomEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public DeleteSessionEvent(Object source, Session session) { - super(source, session); + public DeleteRoomEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/FeatureChangeEvent.java b/src/main/java/de/thm/arsnova/event/FeatureChangeEvent.java similarity index 81% rename from src/main/java/de/thm/arsnova/events/FeatureChangeEvent.java rename to src/main/java/de/thm/arsnova/event/FeatureChangeEvent.java index 05068069527ec85d852edaa2b85dbf0e6cc7f16c..890425edb912f192e319556de5bfa98d6c52f76c 100644 --- a/src/main/java/de/thm/arsnova/events/FeatureChangeEvent.java +++ b/src/main/java/de/thm/arsnova/event/FeatureChangeEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever a new session is created. */ -public class FeatureChangeEvent extends SessionEvent { +public class FeatureChangeEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public FeatureChangeEvent(Object source, Session session) { - super(source, session); + public FeatureChangeEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/FlipFlashcardsEvent.java b/src/main/java/de/thm/arsnova/event/FlipFlashcardsEvent.java similarity index 81% rename from src/main/java/de/thm/arsnova/events/FlipFlashcardsEvent.java rename to src/main/java/de/thm/arsnova/event/FlipFlashcardsEvent.java index 0ae01bedb6208507652594e2d23bc809c68c5ecc..10a17302f4be7e243e88980ea120e5a779c6161a 100644 --- a/src/main/java/de/thm/arsnova/events/FlipFlashcardsEvent.java +++ b/src/main/java/de/thm/arsnova/event/FlipFlashcardsEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever voting on a question is disabled. */ -public class FlipFlashcardsEvent extends SessionEvent { +public class FlipFlashcardsEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public FlipFlashcardsEvent(Object source, Session session) { - super(source, session); + public FlipFlashcardsEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/LockFeedbackEvent.java b/src/main/java/de/thm/arsnova/event/LockFeedbackEvent.java similarity index 82% rename from src/main/java/de/thm/arsnova/events/LockFeedbackEvent.java rename to src/main/java/de/thm/arsnova/event/LockFeedbackEvent.java index 6eea780073a71d64e0f140d105d9cf765237b573..b7a39b6b92b2dcc27d7794e1045bdce9ab47317e 100644 --- a/src/main/java/de/thm/arsnova/events/LockFeedbackEvent.java +++ b/src/main/java/de/thm/arsnova/event/LockFeedbackEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever voting on a question is disabled. */ -public class LockFeedbackEvent extends SessionEvent { +public class LockFeedbackEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public LockFeedbackEvent(Object source, Session session) { - super(source, session); + public LockFeedbackEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/LockQuestionEvent.java b/src/main/java/de/thm/arsnova/event/LockQuestionEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/LockQuestionEvent.java rename to src/main/java/de/thm/arsnova/event/LockQuestionEvent.java index 506ae91832be2550762f8a0443e9415fdc74ca61..54bc9ff6a955fbbcdcad17561aeef794eb0df7a4 100644 --- a/src/main/java/de/thm/arsnova/events/LockQuestionEvent.java +++ b/src/main/java/de/thm/arsnova/event/LockQuestionEvent.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; /** * Fires whenever a content is disabled, i.e., it is hidden from students. */ -public class LockQuestionEvent extends SessionEvent { +public class LockQuestionEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Content content; - public LockQuestionEvent(Object source, Session session, Content content) { - super(source, session); + public LockQuestionEvent(Object source, Room room, Content content) { + super(source, room); this.content = content; } diff --git a/src/main/java/de/thm/arsnova/events/LockQuestionsEvent.java b/src/main/java/de/thm/arsnova/event/LockQuestionsEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/LockQuestionsEvent.java rename to src/main/java/de/thm/arsnova/event/LockQuestionsEvent.java index 85d501ed089e490e95ac4a834582577962db665c..8c892967d3a15bec451edbfbef972783cb08701e 100644 --- a/src/main/java/de/thm/arsnova/events/LockQuestionsEvent.java +++ b/src/main/java/de/thm/arsnova/event/LockQuestionsEvent.java @@ -15,24 +15,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.List; /** * Fires whenever a set of contents are disabled, i.e., they are hidden from students. */ -public class LockQuestionsEvent extends SessionEvent { +public class LockQuestionsEvent extends RoomEvent { private static final long serialVersionUID = 1L; private List<Content> contents; - public LockQuestionsEvent(Object source, Session session, List<Content> contents) { - super(source, session); + public LockQuestionsEvent(Object source, Room room, List<Content> contents) { + super(source, room); this.contents = contents; } diff --git a/src/main/java/de/thm/arsnova/events/LockVoteEvent.java b/src/main/java/de/thm/arsnova/event/LockVoteEvent.java similarity index 72% rename from src/main/java/de/thm/arsnova/events/LockVoteEvent.java rename to src/main/java/de/thm/arsnova/event/LockVoteEvent.java index 4cdf67968e33529a9693164abb2967fbe925c118..1463e12e01dd0f6cf22953844ba0971c32b05aa4 100644 --- a/src/main/java/de/thm/arsnova/events/LockVoteEvent.java +++ b/src/main/java/de/thm/arsnova/event/LockVoteEvent.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.HashMap; import java.util.Map; @@ -26,14 +26,14 @@ import java.util.Map; /** * Fires whenever voting on a content is disabled. */ -public class LockVoteEvent extends SessionEvent { +public class LockVoteEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Content content; - public LockVoteEvent(Object source, Session session, Content content) { - super(source, session); + public LockVoteEvent(Object source, Room room, Content content) { + super(source, room); this.content = content; } @@ -41,19 +41,22 @@ public class LockVoteEvent extends SessionEvent { return this.content.getId(); } - public String getQuestionVariant() { - return this.content.getQuestionVariant(); + public String getGroup() { + /* FIXME: Event does not support content with multiple groups */ + return content.getGroups().toArray(new String[1])[0]; } public Boolean getVotingDisabled() { - return this.content.isVotingDisabled(); + /* FIXME: migrate */ + return false; + //return this.content.isVotingDisabled(); } public Map<String, Object> getVotingAdmission() { Map<String, Object> map = new HashMap<>(); map.put("_id", getQuestionId()); - map.put("variant", getQuestionVariant()); + map.put("variant", getGroup()); return map; } diff --git a/src/main/java/de/thm/arsnova/events/LockVotesEvent.java b/src/main/java/de/thm/arsnova/event/LockVotesEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/LockVotesEvent.java rename to src/main/java/de/thm/arsnova/event/LockVotesEvent.java index fc469749f43536aa89274a86f3444b9d07175f87..a18cf149938191b846924c2e7d8853bbc6b08f38 100644 --- a/src/main/java/de/thm/arsnova/events/LockVotesEvent.java +++ b/src/main/java/de/thm/arsnova/event/LockVotesEvent.java @@ -15,24 +15,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.List; /** * Fires whenever voting of multiple contents is disabled. */ -public class LockVotesEvent extends SessionEvent { +public class LockVotesEvent extends RoomEvent { private static final long serialVersionUID = 1L; private List<Content> contents; - public LockVotesEvent(Object source, Session session, List<Content> contents) { - super(source, session); + public LockVotesEvent(Object source, Room room, List<Content> contents) { + super(source, room); this.contents = contents; } diff --git a/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java b/src/main/java/de/thm/arsnova/event/NewAnswerEvent.java similarity index 72% rename from src/main/java/de/thm/arsnova/events/NewAnswerEvent.java rename to src/main/java/de/thm/arsnova/event/NewAnswerEvent.java index 649e241efc3205807fbb4d80a226e042874b2a96..5049bf32ca1465b25bac64c8b84792fba9353d18 100644 --- a/src/main/java/de/thm/arsnova/events/NewAnswerEvent.java +++ b/src/main/java/de/thm/arsnova/event/NewAnswerEvent.java @@ -15,28 +15,28 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; /** * Fires whenever a new answer is added. */ -public class NewAnswerEvent extends SessionEvent { +public class NewAnswerEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Answer answer; - private final User user; + private final ClientAuthentication user; private final Content content; - public NewAnswerEvent(Object source, Session session, Answer answer, User user, Content content) { - super(source, session); + public NewAnswerEvent(Object source, Room room, Answer answer, ClientAuthentication user, Content content) { + super(source, room); this.answer = answer; this.user = user; this.content = content; @@ -51,7 +51,7 @@ public class NewAnswerEvent extends SessionEvent { return answer; } - public User getUser() { + public ClientAuthentication getUser() { return user; } diff --git a/src/main/java/de/thm/arsnova/events/NewCommentEvent.java b/src/main/java/de/thm/arsnova/event/NewCommentEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/NewCommentEvent.java rename to src/main/java/de/thm/arsnova/event/NewCommentEvent.java index 7949ef50d319d2039ccfc4fca462f87ab81e41b2..3ad2620eaeb8cc8476fc0fea0b4dc41abc19dbdf 100644 --- a/src/main/java/de/thm/arsnova/events/NewCommentEvent.java +++ b/src/main/java/de/thm/arsnova/event/NewCommentEvent.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.Room; /** * Fires whenever a new comment is added. */ -public class NewCommentEvent extends SessionEvent { +public class NewCommentEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Comment comment; - public NewCommentEvent(Object source, Session session, Comment comment) { - super(source, session); + public NewCommentEvent(Object source, Room room, Comment comment) { + super(source, room); this.comment = comment; } diff --git a/src/main/java/de/thm/arsnova/events/NewFeedbackEvent.java b/src/main/java/de/thm/arsnova/event/NewFeedbackEvent.java similarity index 82% rename from src/main/java/de/thm/arsnova/events/NewFeedbackEvent.java rename to src/main/java/de/thm/arsnova/event/NewFeedbackEvent.java index 0f95625430578f239237cf0c2a4d1e6d5917fc44..f9bcdc5254a862cc7e99e3b848aa032a382bccf7 100644 --- a/src/main/java/de/thm/arsnova/events/NewFeedbackEvent.java +++ b/src/main/java/de/thm/arsnova/event/NewFeedbackEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever the feedback changes. */ -public class NewFeedbackEvent extends SessionEvent { +public class NewFeedbackEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public NewFeedbackEvent(Object source, Session session) { - super(source, session); + public NewFeedbackEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/NewQuestionEvent.java b/src/main/java/de/thm/arsnova/event/NewQuestionEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/NewQuestionEvent.java rename to src/main/java/de/thm/arsnova/event/NewQuestionEvent.java index 4b638229b0dffebfcabbcd311b2f37da3f9b58c7..4cf0da993809f66cc4698e2608d586742c1cfc98 100644 --- a/src/main/java/de/thm/arsnova/events/NewQuestionEvent.java +++ b/src/main/java/de/thm/arsnova/event/NewQuestionEvent.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; /** * Fires whenever a new content is added. */ -public class NewQuestionEvent extends SessionEvent { +public class NewQuestionEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Content content; - public NewQuestionEvent(Object source, Session session, Content content) { - super(source, session); + public NewQuestionEvent(Object source, Room room, Content content) { + super(source, room); this.content = content; } diff --git a/src/main/java/de/thm/arsnova/events/NewSessionEvent.java b/src/main/java/de/thm/arsnova/event/NewRoomEvent.java similarity index 82% rename from src/main/java/de/thm/arsnova/events/NewSessionEvent.java rename to src/main/java/de/thm/arsnova/event/NewRoomEvent.java index 34845b8b486d8e5e726d79ba93577c08f56b1b66..4dd067ba6fa1fff1c32979aa1b8097c4247bdc1d 100644 --- a/src/main/java/de/thm/arsnova/events/NewSessionEvent.java +++ b/src/main/java/de/thm/arsnova/event/NewRoomEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever a new session is created. */ -public class NewSessionEvent extends SessionEvent { +public class NewRoomEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public NewSessionEvent(Object source, Session session) { - super(source, session); + public NewRoomEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/PiRoundCancelEvent.java b/src/main/java/de/thm/arsnova/event/PiRoundCancelEvent.java similarity index 82% rename from src/main/java/de/thm/arsnova/events/PiRoundCancelEvent.java rename to src/main/java/de/thm/arsnova/event/PiRoundCancelEvent.java index 48fb6796d29138295609ee3731ccbea667e20bf6..969d4a68aa092872a2be4223776db477c795a93f 100644 --- a/src/main/java/de/thm/arsnova/events/PiRoundCancelEvent.java +++ b/src/main/java/de/thm/arsnova/event/PiRoundCancelEvent.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; /** * Fires whenever a peer instruction round is canceled. @@ -27,8 +27,8 @@ public class PiRoundCancelEvent extends PiRoundEndEvent { private static final long serialVersionUID = 1L; - public PiRoundCancelEvent(Object source, Session session, Content content) { - super(source, session, content); + public PiRoundCancelEvent(Object source, Room room, Content content) { + super(source, room, content); } @Override diff --git a/src/main/java/de/thm/arsnova/events/PiRoundDelayedStartEvent.java b/src/main/java/de/thm/arsnova/event/PiRoundDelayedStartEvent.java similarity index 64% rename from src/main/java/de/thm/arsnova/events/PiRoundDelayedStartEvent.java rename to src/main/java/de/thm/arsnova/event/PiRoundDelayedStartEvent.java index 26da0943077ba23a21da1f92290f5b48392d4568..b8880c367497ac6cf20b09a5c527cef41b0251a6 100644 --- a/src/main/java/de/thm/arsnova/events/PiRoundDelayedStartEvent.java +++ b/src/main/java/de/thm/arsnova/event/PiRoundDelayedStartEvent.java @@ -15,11 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; +import java.util.Date; import java.util.HashMap; import java.util.Map; @@ -27,22 +28,23 @@ import java.util.Map; * Fires whenever a delayed peer instruction round is initiated. The delayed part denotes that this round might not * have been started yet. */ -public class PiRoundDelayedStartEvent extends SessionEvent { +public class PiRoundDelayedStartEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final String questionId; - private final Long startTime; - private final Long endTime; - private final String questionVariant; + private final Date startTime; + private final Date endTime; + private final String group; private int piRound; - public PiRoundDelayedStartEvent(Object source, Session session, Content content) { - super(source, session); + public PiRoundDelayedStartEvent(Object source, Room room, Content content) { + super(source, room); this.questionId = content.getId(); - this.startTime = content.getPiRoundStartTime(); - this.endTime = content.getPiRoundEndTime(); - this.questionVariant = content.getQuestionVariant(); - this.piRound = content.getPiRound(); + /* FIXME: Event does not support content with multiple groups */ + this.group = content.getGroups().toArray(new String[1])[0]; + this.piRound = content.getState().getRound(); + this.endTime = content.getState().getRoundEndTimestamp(); + this.startTime = new Date(); } @Override @@ -54,16 +56,16 @@ public class PiRoundDelayedStartEvent extends SessionEvent { return questionId; } - public Long getStartTime() { + public Date getStartTime() { return startTime; } - public Long getEndTime() { + public Date getEndTime() { return endTime; } - public String getQuestionVariant() { - return questionVariant; + public String getGroup() { + return group; } public Integer getPiRound() { @@ -74,9 +76,9 @@ public class PiRoundDelayedStartEvent extends SessionEvent { Map<String, Object> map = new HashMap<>(); map.put("_id", getQuestionId()); - map.put("endTime", getEndTime()); - map.put("startTime", getStartTime()); - map.put("variant", getQuestionVariant()); + map.put("endTime", getEndTime().getTime()); + map.put("startTime", getStartTime().getTime()); + map.put("variant", getGroup()); map.put("round", getPiRound()); return map; diff --git a/src/main/java/de/thm/arsnova/events/PiRoundEndEvent.java b/src/main/java/de/thm/arsnova/event/PiRoundEndEvent.java similarity index 64% rename from src/main/java/de/thm/arsnova/events/PiRoundEndEvent.java rename to src/main/java/de/thm/arsnova/event/PiRoundEndEvent.java index edaa084e89b6d79ece7a9ef4055f8cfb0507cf1f..ebd864bf7ea003de4cfba5a525956b549008bf20 100644 --- a/src/main/java/de/thm/arsnova/events/PiRoundEndEvent.java +++ b/src/main/java/de/thm/arsnova/event/PiRoundEndEvent.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.HashMap; import java.util.Map; @@ -26,17 +26,18 @@ import java.util.Map; /** * Fires whenever a peer instruction round has ended. */ -public class PiRoundEndEvent extends SessionEvent { +public class PiRoundEndEvent extends RoomEvent { private static final long serialVersionUID = 1L; - private final String questionId; - private final String questionVariant; + private final String contentId; + private final String group; - public PiRoundEndEvent(Object source, Session session, Content content) { - super(source, session); - questionId = content.getId(); - questionVariant = content.getQuestionVariant(); + public PiRoundEndEvent(Object source, Room room, Content content) { + super(source, room); + contentId = content.getId(); + /* FIXME: Event does not support content with multiple groups */ + this.group = content.getGroups().toArray(new String[1])[0]; } @Override @@ -44,19 +45,19 @@ public class PiRoundEndEvent extends SessionEvent { visitor.visit(this); } - public String getQuestionId() { - return questionId; + public String getContentId() { + return contentId; } - public String getQuestionVariant() { - return questionVariant; + public String getGroup() { + return group; } public Map<String, String> getPiRoundEndInformations() { Map<String, String> map = new HashMap<>(); - map.put("_id", getQuestionId()); - map.put("variant", getQuestionVariant()); + map.put("_id", getContentId()); + map.put("variant", getGroup()); return map; } diff --git a/src/main/java/de/thm/arsnova/events/PiRoundResetEvent.java b/src/main/java/de/thm/arsnova/event/PiRoundResetEvent.java similarity index 64% rename from src/main/java/de/thm/arsnova/events/PiRoundResetEvent.java rename to src/main/java/de/thm/arsnova/event/PiRoundResetEvent.java index 3ff1a9d8101dbeb7dfbf6187b61607198efa2745..a7045abfd4da888dec2a42c6274924996c7533d1 100644 --- a/src/main/java/de/thm/arsnova/events/PiRoundResetEvent.java +++ b/src/main/java/de/thm/arsnova/event/PiRoundResetEvent.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.HashMap; import java.util.Map; @@ -26,17 +26,18 @@ import java.util.Map; /** * Fires whenever a peer instruction round is reset. */ -public class PiRoundResetEvent extends SessionEvent { +public class PiRoundResetEvent extends RoomEvent { private static final long serialVersionUID = 1L; - private final String questionId; - private final String questionVariant; + private final String contentId; + private final String group; - public PiRoundResetEvent(Object source, Session session, Content content) { - super(source, session); - questionId = content.getId(); - questionVariant = content.getQuestionVariant(); + public PiRoundResetEvent(Object source, Room room, Content content) { + super(source, room); + contentId = content.getId(); + /* FIXME: Event does not support content with multiple groups */ + this.group = content.getGroups().toArray(new String[1])[0]; } @Override @@ -44,19 +45,19 @@ public class PiRoundResetEvent extends SessionEvent { visitor.visit(this); } - public String getQuestionId() { - return questionId; + public String getContentId() { + return contentId; } - public String getQuestionVariant() { - return questionVariant; + public String getGroup() { + return group; } public Map<String, String> getPiRoundResetInformations() { Map<String, String> map = new HashMap<>(); - map.put("_id", getQuestionId()); - map.put("variant", getQuestionVariant()); + map.put("_id", getContentId()); + map.put("variant", getGroup()); return map; } diff --git a/src/main/java/de/thm/arsnova/events/SessionEvent.java b/src/main/java/de/thm/arsnova/event/RoomEvent.java similarity index 75% rename from src/main/java/de/thm/arsnova/events/SessionEvent.java rename to src/main/java/de/thm/arsnova/event/RoomEvent.java index fc393ddfb130ae78d4db3ac6a68c782f1693e7d2..eb32f2aaa923c64b342da10300ce938bfdeac1fe 100644 --- a/src/main/java/de/thm/arsnova/events/SessionEvent.java +++ b/src/main/java/de/thm/arsnova/event/RoomEvent.java @@ -15,25 +15,25 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** - * Base class for all {@link ArsnovaEvent}s that are related to a session. + * Base class for all {@link ArsnovaEvent}s that are related to a room. */ -public abstract class SessionEvent extends ArsnovaEvent { +public abstract class RoomEvent extends ArsnovaEvent { private static final long serialVersionUID = 1L; - private final Session session; + private final Room room; - public SessionEvent(Object source, Session session) { + public RoomEvent(Object source, Room room) { super(source); - this.session = session; + this.room = room; } - public Session getSession() { - return session; + public Room getRoom() { + return room; } } diff --git a/src/main/java/de/thm/arsnova/events/StatusSessionEvent.java b/src/main/java/de/thm/arsnova/event/StatusRoomEvent.java similarity index 82% rename from src/main/java/de/thm/arsnova/events/StatusSessionEvent.java rename to src/main/java/de/thm/arsnova/event/StatusRoomEvent.java index 6ee65ef98104efdceced586b0a587f26cbb5b61a..a981d2f31022df067147d200a3f0cb1e5d21f5e2 100644 --- a/src/main/java/de/thm/arsnova/events/StatusSessionEvent.java +++ b/src/main/java/de/thm/arsnova/event/StatusRoomEvent.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Room; /** * Fires whenever the status of a session changes, i.e., it is enabled or disabled. */ -public class StatusSessionEvent extends SessionEvent { +public class StatusRoomEvent extends RoomEvent { private static final long serialVersionUID = 1L; - public StatusSessionEvent(Object source, Session session) { - super(source, session); + public StatusRoomEvent(Object source, Room room) { + super(source, room); } @Override diff --git a/src/main/java/de/thm/arsnova/events/UnlockQuestionEvent.java b/src/main/java/de/thm/arsnova/event/UnlockQuestionEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/UnlockQuestionEvent.java rename to src/main/java/de/thm/arsnova/event/UnlockQuestionEvent.java index f38fe0af59da7299f436f27cd8445cd749688608..5e09ed349ffb1b08f8c1ffbbaa131d584013c4df 100644 --- a/src/main/java/de/thm/arsnova/events/UnlockQuestionEvent.java +++ b/src/main/java/de/thm/arsnova/event/UnlockQuestionEvent.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; /** * Fires whenever a content is enabled, i.e., it becomes visible to students. */ -public class UnlockQuestionEvent extends SessionEvent { +public class UnlockQuestionEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Content content; - public UnlockQuestionEvent(Object source, Session session, Content content) { - super(source, session); + public UnlockQuestionEvent(Object source, Room room, Content content) { + super(source, room); this.content = content; } diff --git a/src/main/java/de/thm/arsnova/events/UnlockQuestionsEvent.java b/src/main/java/de/thm/arsnova/event/UnlockQuestionsEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/UnlockQuestionsEvent.java rename to src/main/java/de/thm/arsnova/event/UnlockQuestionsEvent.java index 53ccddeba4478eee7bf8b1d3dc3514ae19a837d0..a4e5d8c6a1916b3d07f8b33794af26f2fd2dcc75 100644 --- a/src/main/java/de/thm/arsnova/events/UnlockQuestionsEvent.java +++ b/src/main/java/de/thm/arsnova/event/UnlockQuestionsEvent.java @@ -15,24 +15,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.List; /** * Fires whenever a set of contents are enabled, i.e., they become visible to students. */ -public class UnlockQuestionsEvent extends SessionEvent { +public class UnlockQuestionsEvent extends RoomEvent { private static final long serialVersionUID = 1L; private List<Content> contents; - public UnlockQuestionsEvent(Object source, Session session, List<Content> contents) { - super(source, session); + public UnlockQuestionsEvent(Object source, Room room, List<Content> contents) { + super(source, room); this.contents = contents; } diff --git a/src/main/java/de/thm/arsnova/events/UnlockVoteEvent.java b/src/main/java/de/thm/arsnova/event/UnlockVoteEvent.java similarity index 73% rename from src/main/java/de/thm/arsnova/events/UnlockVoteEvent.java rename to src/main/java/de/thm/arsnova/event/UnlockVoteEvent.java index 605defea422e9f0af7292aed061d2b36d61dd23f..962f7c54678b7a10cca7ef7b890d56489ef48270 100644 --- a/src/main/java/de/thm/arsnova/events/UnlockVoteEvent.java +++ b/src/main/java/de/thm/arsnova/event/UnlockVoteEvent.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.HashMap; import java.util.Map; @@ -26,14 +26,14 @@ import java.util.Map; /** * Fires whenever voting on a content is enabled. */ -public class UnlockVoteEvent extends SessionEvent { +public class UnlockVoteEvent extends RoomEvent { private static final long serialVersionUID = 1L; private final Content content; - public UnlockVoteEvent(Object source, Session session, Content content) { - super(source, session); + public UnlockVoteEvent(Object source, Room room, Content content) { + super(source, room); this.content = content; } @@ -41,19 +41,20 @@ public class UnlockVoteEvent extends SessionEvent { return this.content.getId(); } - public String getQuestionVariant() { - return this.content.getQuestionVariant(); + public String getGroup() { + /* FIXME: Event does not support content with multiple groups */ + return content.getGroups().toArray(new String[1])[0]; } public Boolean getVotingDisabled() { - return this.content.isVotingDisabled(); + return !this.content.getState().isResponsesEnabled(); } public Map<String, Object> getVotingAdmission() { Map<String, Object> map = new HashMap<>(); map.put("_id", getQuestionId()); - map.put("variant", getQuestionVariant()); + map.put("variant", getGroup()); return map; } diff --git a/src/main/java/de/thm/arsnova/events/UnlockVotesEvent.java b/src/main/java/de/thm/arsnova/event/UnlockVotesEvent.java similarity index 80% rename from src/main/java/de/thm/arsnova/events/UnlockVotesEvent.java rename to src/main/java/de/thm/arsnova/event/UnlockVotesEvent.java index bbdd513ae913c0b7238318e37db7ccd8df1b879f..fdc50180ad5b8caf777ce001919f56cdefe8ca65 100644 --- a/src/main/java/de/thm/arsnova/events/UnlockVotesEvent.java +++ b/src/main/java/de/thm/arsnova/event/UnlockVotesEvent.java @@ -15,24 +15,24 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; import java.util.List; /** * Fires whenever voting of multiple contents is enabled. */ -public class UnlockVotesEvent extends SessionEvent { +public class UnlockVotesEvent extends RoomEvent { private static final long serialVersionUID = 1L; private List<Content> contents; - public UnlockVotesEvent(Object source, Session session, List<Content> contents) { - super(source, session); + public UnlockVotesEvent(Object source, Room room, List<Content> contents) { + super(source, room); this.contents = contents; } diff --git a/src/main/java/de/thm/arsnova/events/package-info.java b/src/main/java/de/thm/arsnova/event/package-info.java similarity index 58% rename from src/main/java/de/thm/arsnova/events/package-info.java rename to src/main/java/de/thm/arsnova/event/package-info.java index 838fc4cd66928cc72c857d8cef1f65a2b1fb1cd7..636c0e300c3f8a9fd12657b7b8f44ce35b86bea8 100644 --- a/src/main/java/de/thm/arsnova/events/package-info.java +++ b/src/main/java/de/thm/arsnova/event/package-info.java @@ -1,4 +1,4 @@ /** * ARSnova's internal event system */ -package de.thm.arsnova.events; +package de.thm.arsnova.event; diff --git a/src/main/java/de/thm/arsnova/model/Answer.java b/src/main/java/de/thm/arsnova/model/Answer.java new file mode 100644 index 0000000000000000000000000000000000000000..38eb277f74e71fa0bf9b30ce74a2617d40a4272b --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/Answer.java @@ -0,0 +1,124 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; +import de.thm.arsnova.model.serialization.FormatAnswerTypeIdResolver; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Map; +import java.util.Objects; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.CUSTOM, + property = "format", + visible = true, + defaultImpl = Answer.class +) +@JsonTypeIdResolver(FormatAnswerTypeIdResolver.class) +public class Answer extends Entity { + private String contentId; + private String roomId; + private String creatorId; + private Content.Format format; + private int round; + private Map<String, Map<String, ?>> extensions; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getContentId() { + return contentId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setContentId(final String contentId) { + this.contentId = contentId; + } + + @JsonView(View.Persistence.class) + public String getRoomId() { + return roomId; + } + + @JsonView(View.Persistence.class) + public void setRoomId(String roomId) { + this.roomId = roomId; + } + + @JsonView(View.Persistence.class) + public String getCreatorId() { + return creatorId; + } + + public void setCreatorId(final String creatorId) { + this.creatorId = creatorId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Content.Format getFormat() { + return format; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setFormat(final Content.Format format) { + this.format = format; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public int getRound() { + return round; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRound(final int round) { + this.round = round; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, Map<String, ?>> getExtensions() { + return extensions; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setExtensions(Map<String, Map<String, ?>> extensions) { + this.extensions = extensions; + } + + @JsonView(View.Persistence.class) + @Override + public Class<? extends Entity> getType() { + return Answer.class; + } + + /** + * {@inheritDoc} + * + * The following fields of <tt>Answer</tt> are excluded from equality checks: + * {@link #extensions}. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final Answer answer = (Answer) o; + + return round == answer.round && + Objects.equals(contentId, answer.contentId) && + Objects.equals(roomId, answer.roomId) && + Objects.equals(creatorId, answer.creatorId); + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("contentId", contentId) + .append("roomId", roomId) + .append("creatorId", creatorId) + .append("format", format) + .append("round", round); + } +} diff --git a/src/main/java/de/thm/arsnova/model/AnswerStatistics.java b/src/main/java/de/thm/arsnova/model/AnswerStatistics.java new file mode 100644 index 0000000000000000000000000000000000000000..bf90c2b044154e5e26216e30e459de9824c7b738 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/AnswerStatistics.java @@ -0,0 +1,184 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Collection; +import java.util.List; + +public class AnswerStatistics { + public static class RoundStatistics { + public static class Combination { + private List<Integer> selectedChoiceIndexes; + private int count; + + public Combination(final List<Integer> selectedChoiceIndexes, final int count) { + this.selectedChoiceIndexes = selectedChoiceIndexes; + this.count = count; + } + + @JsonView(View.Public.class) + public List<Integer> getSelectedChoiceIndexes() { + return selectedChoiceIndexes; + } + + @JsonView(View.Public.class) + public int getCount() { + return count; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("selectedChoiceIndexes", selectedChoiceIndexes) + .append("count", count) + .toString(); + } + } + + private int round; + private List<Integer> independentCounts; + private Collection<Combination> combinatedCounts; + private int abstentionCount; + + @JsonView(View.Public.class) + public int getRound() { + return round; + } + + public void setRound(int round) { + this.round = round; + } + + @JsonView(View.Public.class) + public List<Integer> getIndependentCounts() { + return independentCounts; + } + + public void setIndependentCounts(final List<Integer> independentCounts) { + this.independentCounts = independentCounts; + } + + @JsonView(View.Public.class) + public Collection<Combination> getCombinatedCounts() { + return combinatedCounts; + } + + public void setCombinatedCounts(Collection<Combination> combinatedCounts) { + this.combinatedCounts = combinatedCounts; + } + + @JsonView(View.Public.class) + public int getAbstentionCount() { + return abstentionCount; + } + + public void setAbstentionCount(int abstentionCount) { + this.abstentionCount = abstentionCount; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("round", round) + .append("independentCounts", independentCounts) + .append("combinatedCounts", combinatedCounts) + .append("abstentionCount", abstentionCount) + .toString(); + } + } + + public static class RoundTransition { + private int roundA; + private int roundB; + private List<Integer> selectedChoiceIndexesA; + private List<Integer> selectedChoiceIndexesB; + private int count; + + public RoundTransition(final int roundA, final List<Integer> selectedChoiceIndexesA, + final int roundB, final List<Integer> selectedChoiceIndexesB, final int count) { + this.roundA = roundA; + this.roundB = roundB; + this.selectedChoiceIndexesA = selectedChoiceIndexesA; + this.selectedChoiceIndexesB = selectedChoiceIndexesB; + this.count = count; + } + + @JsonView(View.Public.class) + public int getRoundA() { + return roundA; + } + + @JsonView(View.Public.class) + public int getRoundB() { + return roundB; + } + + @JsonView(View.Public.class) + public List<Integer> getSelectedChoiceIndexesA() { + return selectedChoiceIndexesA; + } + + @JsonView(View.Public.class) + public List<Integer> getSelectedChoiceIndexesB() { + return selectedChoiceIndexesB; + } + + @JsonView(View.Public.class) + public int getCount() { + return count; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("roundA", roundA) + .append("selectedChoiceIndexesA", selectedChoiceIndexesA) + .append("roundB", roundB) + .append("selectedChoiceIndexesB", selectedChoiceIndexesB) + .append("count", count) + .toString(); + } + } + + private String contentId; + private List<RoundStatistics> roundStatistics; + private List<RoundTransition> roundTransitions; + + @JsonView(View.Public.class) + public String getContentId() { + return contentId; + } + + public void setContentId(final String contentId) { + this.contentId = contentId; + } + + @JsonView(View.Public.class) + public List<RoundStatistics> getRoundStatistics() { + return roundStatistics; + } + + public void setRoundStatistics(List<RoundStatistics> roundStatistics) { + this.roundStatistics = roundStatistics; + } + + @JsonView(View.Public.class) + public List<RoundTransition> getRoundTransitions() { + return roundTransitions; + } + + public void setRoundTransitions(List<RoundTransition> roundTransitions) { + this.roundTransitions = roundTransitions; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("contentId", contentId) + .append("roundStatistics", roundStatistics) + .append("roundTransitions", roundTransitions) + .toString(); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Attachment.java b/src/main/java/de/thm/arsnova/model/Attachment.java new file mode 100644 index 0000000000000000000000000000000000000000..a3905ccd907e03202eff5bff03b8df9acb6e63eb --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/Attachment.java @@ -0,0 +1,108 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Objects; + +public class Attachment extends Entity { + private String mediaType; + private long size; + private String originalSourceUrl; + private String storageLocation; + + @Override + @JsonView({View.Persistence.class, View.Public.class}) + public String getId() { + return id; + } + + @Override + @JsonView(View.Persistence.class) + public void setId(final String id) { + this.id = id; + } + + @Override + @JsonView({View.Persistence.class, View.Public.class}) + public String getRevision() { + return rev; + } + + @Override + @JsonView(View.Public.class) + public void setRevision(final String rev) { + this.rev = rev; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getMediaType() { + return mediaType; + } + + @JsonView(View.Persistence.class) + public void setMediaType(String mediaType) { + this.mediaType = mediaType; + } + + @JsonView(View.Persistence.class) + public long getSize() { + return size; + } + + @JsonView(View.Persistence.class) + public void setSize(long size) { + this.size = size; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getOriginalSourceUrl() { + return originalSourceUrl; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setOriginalSourceUrl(String originalSourceUrl) { + this.originalSourceUrl = originalSourceUrl; + } + + @JsonView(View.Persistence.class) + public String getStorageLocation() { + return storageLocation; + } + + @JsonView(View.Persistence.class) + public void setStorageLocation(String storageLocation) { + this.storageLocation = storageLocation; + } + + /** + * {@inheritDoc} + * + * All fields of <tt>Attachment</tt> are included in equality checks. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final Attachment that = (Attachment) o; + + return size == that.size && + Objects.equals(mediaType, that.mediaType) && + Objects.equals(originalSourceUrl, that.originalSourceUrl) && + Objects.equals(storageLocation, that.storageLocation); + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("mediaType", mediaType) + .append("size", size) + .append("originalSourceUrl", originalSourceUrl) + .append("storageLocation", storageLocation); + } +} diff --git a/src/main/java/de/thm/arsnova/entities/Authorize.java b/src/main/java/de/thm/arsnova/model/Authorize.java similarity index 94% rename from src/main/java/de/thm/arsnova/entities/Authorize.java rename to src/main/java/de/thm/arsnova/model/Authorize.java index 643cd1289649f4c133d010540f96340d6b7e5396..0458dbd8ad3f5d74c120b0fa18d8936a0955cafd 100644 --- a/src/main/java/de/thm/arsnova/entities/Authorize.java +++ b/src/main/java/de/thm/arsnova/model/Authorize.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; public class Authorize { private String user; diff --git a/src/main/java/de/thm/arsnova/model/ChoiceAnswer.java b/src/main/java/de/thm/arsnova/model/ChoiceAnswer.java new file mode 100644 index 0000000000000000000000000000000000000000..00d667f7f1291875f6b0074651f2487b05c18f75 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/ChoiceAnswer.java @@ -0,0 +1,27 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.List; + +public class ChoiceAnswer extends Answer { + private List<Integer> selectedChoiceIndexes; + + @JsonView({View.Persistence.class, View.Public.class}) + public List<Integer> getSelectedChoiceIndexes() { + return selectedChoiceIndexes; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setSelectedChoiceIndexes(final List<Integer> selectedChoiceIndexes) { + this.selectedChoiceIndexes = selectedChoiceIndexes; + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("selectedChoiceIndexes", selectedChoiceIndexes); + } +} diff --git a/src/main/java/de/thm/arsnova/model/ChoiceQuestionContent.java b/src/main/java/de/thm/arsnova/model/ChoiceQuestionContent.java new file mode 100644 index 0000000000000000000000000000000000000000..400a4c00d29fca6efd422547dc960d1d3fb12f2b --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/ChoiceQuestionContent.java @@ -0,0 +1,87 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.ArrayList; +import java.util.List; + +public class ChoiceQuestionContent extends Content { + public static class AnswerOption { + private String label; + private int points; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getLabel() { + return label; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setLabel(String label) { + this.label = label; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public int getPoints() { + return points; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setPoints(int points) { + this.points = points; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("label", label) + .append("points", points) + .toString(); + } + + } + + private List<AnswerOption> options = new ArrayList<>(); + private List<Integer> correctOptionIndexes = new ArrayList<>(); + private boolean multiple; + + @JsonView({View.Persistence.class, View.Public.class}) + public List<AnswerOption> getOptions() { + return options; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setOptions(final List<AnswerOption> options) { + this.options = options; + } + + /* TODO: A new JsonView is needed here. */ + @JsonView(View.Persistence.class) + public List<Integer> getCorrectOptionIndexes() { + return correctOptionIndexes; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setCorrectOptionIndexes(final List<Integer> correctOptionIndexes) { + this.correctOptionIndexes = correctOptionIndexes; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isMultiple() { + return multiple; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setMultiple(final boolean multiple) { + this.multiple = multiple; + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("options", options) + .append("correctOptionIndexes", correctOptionIndexes) + .append("multiple", multiple); + } +} diff --git a/src/main/java/de/thm/arsnova/model/ClientAuthentication.java b/src/main/java/de/thm/arsnova/model/ClientAuthentication.java new file mode 100644 index 0000000000000000000000000000000000000000..3d9d22b662bb62c229d0a394062d6db94a169624 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/ClientAuthentication.java @@ -0,0 +1,50 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +public class ClientAuthentication { + private String userId; + private String loginId; + private UserProfile.AuthProvider authProvider; + private String token; + + public ClientAuthentication(final String userId, final String loginId, final UserProfile.AuthProvider authProvider, + final String token) { + this.userId = userId; + this.loginId = loginId; + this.authProvider = authProvider; + this.token = token; + } + + @JsonView(View.Public.class) + public String getUserId() { + return userId; + } + + @JsonView(View.Public.class) + public String getLoginId() { + return loginId; + } + + @JsonView(View.Public.class) + public UserProfile.AuthProvider getAuthProvider() { + return authProvider; + } + + @JsonView(View.Public.class) + public String getToken() { + return token; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("userId", userId) + .append("loginId", loginId) + .append("authProvider", authProvider) + .append("token", token) + .toString(); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Comment.java b/src/main/java/de/thm/arsnova/model/Comment.java new file mode 100644 index 0000000000000000000000000000000000000000..f25331a4ab77696de9a0fc2ad1aad85213ed5b00 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/Comment.java @@ -0,0 +1,125 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +public class Comment extends Entity { + private String roomId; + private String creatorId; + private String subject; + private String body; + private Date timestamp; + private boolean read; + private Map<String, Map<String, ?>> extensions; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getRoomId() { + return roomId; + } + + @JsonView(View.Persistence.class) + public void setRoomId(final String roomId) { + this.roomId = roomId; + } + + @JsonView(View.Persistence.class) + public String getCreatorId() { + return creatorId; + } + + @JsonView(View.Persistence.class) + public void setCreatorId(final String creatorId) { + this.creatorId = creatorId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getSubject() { + return subject; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setSubject(final String subject) { + this.subject = subject; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getBody() { + return body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setBody(final String body) { + this.body = body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Date getTimestamp() { + return timestamp; + } + + @JsonView(View.Persistence.class) + public void setTimestamp(final Date timestamp) { + this.timestamp = timestamp; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isRead() { + return read; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRead(final boolean read) { + this.read = read; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, Map<String, ?>> getExtensions() { + return extensions; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setExtensions(Map<String, Map<String, ?>> extensions) { + this.extensions = extensions; + } + + /** + * {@inheritDoc} + * + * The following fields of <tt>LogEntry</tt> are excluded from equality checks: + * {@link #extensions}. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final Comment comment = (Comment) o; + + return read == comment.read && + Objects.equals(roomId, comment.roomId) && + Objects.equals(creatorId, comment.creatorId) && + Objects.equals(subject, comment.subject) && + Objects.equals(body, comment.body) && + Objects.equals(timestamp, comment.timestamp) && + Objects.equals(extensions, comment.extensions); + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("roomId", roomId) + .append("creatorId", creatorId) + .append("subject", subject) + .append("body", body) + .append("timestamp", timestamp) + .append("read", read); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Content.java b/src/main/java/de/thm/arsnova/model/Content.java new file mode 100644 index 0000000000000000000000000000000000000000..471c98a2c44c6158ffb426adc3ac9ad1be3ff13b --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/Content.java @@ -0,0 +1,277 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonView; +import com.fasterxml.jackson.databind.annotation.JsonTypeIdResolver; +import de.thm.arsnova.model.serialization.FormatContentTypeIdResolver; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.CUSTOM, + property = "format", + visible = true, + defaultImpl = Content.class +) +@JsonTypeIdResolver(FormatContentTypeIdResolver.class) +public class Content extends Entity { + public enum Format { + CHOICE, + BINARY, + SCALE, + NUMBER, + TEXT, + GRID + } + + public static class State { + private int round = 1; + private Date roundEndTimestamp; + private boolean visible = true; + private boolean solutionVisible = false; + private boolean responsesEnabled = true; + private boolean responsesVisible = false; + + @JsonView({View.Persistence.class, View.Public.class}) + public int getRound() { + return round; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRound(final int round) { + this.round = round; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Date getRoundEndTimestamp() { + return roundEndTimestamp; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRoundEndTimestamp(Date roundEndTimestamp) { + this.roundEndTimestamp = roundEndTimestamp; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isVisible() { + return visible; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isSolutionVisible() { + return solutionVisible; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setSolutionVisible(final boolean solutionVisible) { + this.solutionVisible = solutionVisible; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setVisible(final boolean visible) { + this.visible = visible; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isResponsesEnabled() { + return responsesEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setResponsesEnabled(final boolean responsesEnabled) { + this.responsesEnabled = responsesEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isResponsesVisible() { + return responsesVisible; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setResponsesVisible(final boolean responsesVisible) { + this.responsesVisible = responsesVisible; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("round", round) + .append("roundEndTimestamp", roundEndTimestamp) + .append("visible", visible) + .append("solutionVisible", solutionVisible) + .append("responsesEnabled", responsesEnabled) + .append("responsesVisible", responsesVisible) + .toString(); + } + } + + private String roomId; + private String subject; + private String body; + private Format format; + private Set<String> groups; + private boolean abstentionsAllowed; + private State state; + private Date timestamp; + private Map<String, Map<String, ?>> extensions; + private Map<String, String> attachments; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getRoomId() { + return roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRoomId(final String roomId) { + this.roomId = roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getSubject() { + return subject; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setSubject(final String subject) { + this.subject = subject; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getBody() { + return body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setBody(final String body) { + this.body = body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Format getFormat() { + return format; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setFormat(final Format format) { + this.format = format; + } + + @JsonView(View.Public.class) + public Set<String> getGroups() { + if (groups == null) { + groups = new HashSet<>(); + } + + return groups; + } + + /* Content groups are persisted in the Room */ + @JsonView(View.Public.class) + public void setGroups(final Set<String> groups) { + this.groups = groups; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public State getState() { + return state != null ? state : (state = new State()); + } + + public void resetState() { + this.state = new State(); + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setState(final State state) { + this.state = state; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Date getTimestamp() { + return timestamp; + } + + @JsonView(View.Persistence.class) + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, Map<String, ?>> getExtensions() { + return extensions; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setExtensions(Map<String, Map<String, ?>> extensions) { + this.extensions = extensions; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, String> getAttachments() { + return attachments; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAttachments(final Map<String, String> attachments) { + this.attachments = attachments; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isAbstentionsAllowed() { + return abstentionsAllowed; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAbstentionsAllowed(final boolean abstentionsAllowed) { + this.abstentionsAllowed = abstentionsAllowed; + } + + @JsonView(View.Persistence.class) + @Override + public Class<? extends Entity> getType() { + return Content.class; + } + + /** + * {@inheritDoc} + * + * The following fields of <tt>LogEntry</tt> are excluded from equality checks: + * {@link #state}, {@link #extensions}, {@link #attachments}. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final Content content = (Content) o; + + return Objects.equals(roomId, content.roomId) && + Objects.equals(subject, content.subject) && + Objects.equals(body, content.body) && + format == content.format && + Objects.equals(groups, content.groups) && + Objects.equals(timestamp, content.timestamp); + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("roomId", roomId) + .append("subject", subject) + .append("body", body) + .append("format", format) + .append("groups", groups) + .append("abstentionsAllowed", abstentionsAllowed) + .append("state", state) + .append("timestamp", timestamp) + .append("attachments", attachments); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Entity.java b/src/main/java/de/thm/arsnova/model/Entity.java new file mode 100644 index 0000000000000000000000000000000000000000..93add05c967e7df2f2a4b4bff56ea2603c004cde --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/Entity.java @@ -0,0 +1,154 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; +import java.util.Objects; + +/** + * Used as base for classes that represent persistent data with an unique ID. + * + * @author Daniel Gerhardt + */ +public abstract class Entity { + protected String id; + protected String rev; + protected Date creationTimestamp; + protected Date updateTimestamp; + private boolean internal; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getId() { + return id; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setId(final String id) { + this.id = id; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getRevision() { + return rev; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRevision(final String rev) { + this.rev = rev; + } + + @JsonView(View.Persistence.class) + public Date getCreationTimestamp() { + return creationTimestamp; + } + + @JsonView(View.Persistence.class) + public void setCreationTimestamp(final Date creationTimestamp) { + this.creationTimestamp = creationTimestamp; + } + + @JsonView(View.Persistence.class) + public Date getUpdateTimestamp() { + return updateTimestamp; + } + + @JsonView(View.Persistence.class) + public void setUpdateTimestamp(final Date updateTimestamp) { + this.updateTimestamp = updateTimestamp; + } + + @JsonView(View.Persistence.class) + public Class<? extends Entity> getType() { + return getClass(); + } + + public boolean isInternal() { + return internal; + } + + public void setInternal(final boolean internal) { + this.internal = internal; + } + + @Override + public int hashCode() { + return Objects.hash(id, rev, creationTimestamp, updateTimestamp); + } + + /** + * Use this helper method when overriding {@link #hashCode()}. + * + * @param init The result of <tt>super.hashCode()</tt> + * @param additionalFields Fields introduced by the subclass which should be included in the hash code generation + * + * @see java.util.Arrays#hashCode(Object[]) + */ + protected int hashCode(final int init, final Object... additionalFields) { + int result = init; + if (additionalFields == null) { + return result; + } + for (Object element : additionalFields) { + result = 31 * result + (element == null ? 0 : element.hashCode()); + } + + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final Entity entity = (Entity) o; + + return Objects.equals(id, entity.id) && + Objects.equals(rev, entity.rev) && + Objects.equals(creationTimestamp, entity.creationTimestamp) && + Objects.equals(updateTimestamp, entity.updateTimestamp); + } + + @Override + public String toString() { + return buildToString().toString(); + } + + /** + * Use this helper method to adjust the output of {@link #toString()}. + * Override this method instead of <tt>toString()</tt> and call <tt>super.buildToString()</tt>. + * Additional fields can be added to the String by calling + * {@link org.springframework.core.style.ToStringCreator#append} on the <tt>ToStringCreator</tt>. + */ + protected ToStringCreator buildToString() { + ToStringCreator toStringCreator = new ToStringCreator(this); + toStringCreator + .append("id", id) + .append("revision", rev) + .append("creationTimestamp", creationTimestamp) + .append("updateTimestamp", updateTimestamp); + + return toStringCreator; + } +} diff --git a/src/main/java/de/thm/arsnova/entities/Feedback.java b/src/main/java/de/thm/arsnova/model/Feedback.java similarity index 94% rename from src/main/java/de/thm/arsnova/entities/Feedback.java rename to src/main/java/de/thm/arsnova/model/Feedback.java index b46616432da88c7f436ae96c692bbc6950bbe187..22964d81c561fa84eacdcf48c2ab19ac26f81824 100644 --- a/src/main/java/de/thm/arsnova/entities/Feedback.java +++ b/src/main/java/de/thm/arsnova/model/Feedback.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import java.util.ArrayList; import java.util.List; @@ -51,7 +51,7 @@ public class Feedback { } @Override - public boolean equals(Object obj) { + public boolean equals(final Object obj) { if (obj == null) { return false; } diff --git a/src/main/java/de/thm/arsnova/model/FindQuery.java b/src/main/java/de/thm/arsnova/model/FindQuery.java new file mode 100644 index 0000000000000000000000000000000000000000..d0364c3b76481df3962111966ddcadfc866c7f5a --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/FindQuery.java @@ -0,0 +1,71 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Map; + +public class FindQuery<E extends Entity> { + enum LogicalOperator { + AND, + OR + } + + private LogicalOperator operator = LogicalOperator.AND; + private E properties; + private Map<String, Object> externalFilters; + + public LogicalOperator getOperator() { + return operator; + } + + @JsonView(View.Public.class) + public void setOperator(final LogicalOperator operator) { + this.operator = operator; + } + + public E getProperties() { + return properties; + } + + @JsonView(View.Public.class) + public void setProperties(final E properties) { + this.properties = properties; + } + + public Map<String, Object> getExternalFilters() { + return externalFilters; + } + + @JsonView(View.Public.class) + public void setExternalFilters(final Map<String, Object> externalFilters) { + this.externalFilters = externalFilters; + } + + @Override + public String toString() { + return new ToStringCreator(getClass()) + .append("operator", operator) + .append("properties", properties) + .append("externalFilters", externalFilters) + .toString(); + } +} diff --git a/src/main/java/de/thm/arsnova/model/LogEntry.java b/src/main/java/de/thm/arsnova/model/LogEntry.java new file mode 100644 index 0000000000000000000000000000000000000000..e66d65c852c818c048ff3c7575a498d201c0cdde --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/LogEntry.java @@ -0,0 +1,137 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; + +import java.util.Date; +import java.util.Map; +import java.util.Objects; + +public class LogEntry extends Entity { + public enum LogLevel { + TRACE, + DEBUG, + INFO, + WARN, + ERROR, + FATAL + } + + private String id; + private String rev; + private Date creationTimestamp; + private Date updateTimestamp; + private String event; + private int level; + private Map<String, Object> payload; + + public LogEntry(@JsonProperty String event, @JsonProperty int level, @JsonProperty Map<String, Object> payload) { + this.event = event; + this.level = level; + this.payload = payload; + } + + @JsonView(View.Persistence.class) + public String getId() { + return id; + } + + @JsonView(View.Persistence.class) + public void setId(final String id) { + this.id = id; + } + + @JsonView(View.Persistence.class) + public String getRevision() { + return rev; + } + + @JsonView(View.Persistence.class) + public void setRevision(final String rev) { + this.rev = rev; + } + + @Override + @JsonView(View.Persistence.class) + public Date getCreationTimestamp() { + return creationTimestamp; + } + + @Override + @JsonView(View.Persistence.class) + public void setCreationTimestamp(final Date creationTimestamp) { + this.creationTimestamp = creationTimestamp; + } + + @Override + @JsonView(View.Persistence.class) + public Date getUpdateTimestamp() { + return updateTimestamp; + } + + @Override + @JsonView(View.Persistence.class) + public void setUpdateTimestamp(final Date updateTimestamp) { + this.updateTimestamp = updateTimestamp; + } + + @JsonView(View.Persistence.class) + public String getEvent() { + return event; + } + + @JsonView(View.Persistence.class) + public void setEvent(final String event) { + this.event = event; + } + + @JsonView(View.Persistence.class) + public int getLevel() { + return level; + } + + @JsonView(View.Persistence.class) + public void setLevel(final int level) { + this.level = level; + } + + @JsonView(View.Persistence.class) + public void setLevel(final de.thm.arsnova.model.migration.v2.LogEntry.LogLevel level) { + this.level = level.ordinal(); + } + + @JsonView(View.Persistence.class) + public Map<String, Object> getPayload() { + return payload; + } + + @JsonView(View.Persistence.class) + public void setPayload(final Map<String, Object> payload) { + this.payload = payload; + } + + /** + * {@inheritDoc} + * + * The following fields of <tt>LogEntry</tt> are excluded from equality checks: + * {@link #payload}. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final LogEntry logEntry = (LogEntry) o; + + return level == logEntry.level && + Objects.equals(id, logEntry.id) && + Objects.equals(rev, logEntry.rev) && + Objects.equals(creationTimestamp, logEntry.creationTimestamp) && + Objects.equals(updateTimestamp, logEntry.updateTimestamp) && + Objects.equals(event, logEntry.event); + } +} diff --git a/src/main/java/de/thm/arsnova/model/LoginCredentials.java b/src/main/java/de/thm/arsnova/model/LoginCredentials.java new file mode 100644 index 0000000000000000000000000000000000000000..3e90b53a6ba067d5e1d59559574c45fbd5e0c4d4 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/LoginCredentials.java @@ -0,0 +1,36 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +public class LoginCredentials { + private String loginId; + private String password; + + public String getLoginId() { + return loginId; + } + + @JsonView(View.Public.class) + public void setLoginId(final String loginId) { + this.loginId = loginId; + } + + public String getPassword() { + return password; + } + + @JsonView(View.Public.class) + public void setPassword(final String password) { + this.password = password; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("loginId", loginId) + .append("password", password) + .toString(); + } +} diff --git a/src/main/java/de/thm/arsnova/model/MigrationState.java b/src/main/java/de/thm/arsnova/model/MigrationState.java new file mode 100644 index 0000000000000000000000000000000000000000..f27bb2f85ed339fb371085e41d8575419d7515e9 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/MigrationState.java @@ -0,0 +1,117 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Objects; + +public class MigrationState extends Entity { + public static class Migration { + private String id; + private Date start; + + public Migration() { + + } + + public Migration(String id, Date start) { + this.id = id; + this.start = start; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getId() { + return id; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Date getStart() { + return start; + } + + @Override + public String toString() { + return "Migration " + id + " started at " + start; + } + } + + public static final String ID = "MigrationState"; + private Migration active; + private List<String> completed = new ArrayList<>(); + + { + id = ID; + } + + @Override + @JsonView(View.Persistence.class) + public String getId() { + return ID; + } + + @Override + @JsonView(View.Persistence.class) + public void setId(final String id) { + if (!id.equals(this.ID)) { + throw new IllegalArgumentException("ID of this entity must not be changed."); + }; + } + + @Override + @JsonView({View.Persistence.class, View.Public.class}) + public String getRevision() { + return rev; + } + + @Override + @JsonView(View.Persistence.class) + public void setRevision(final String rev) { + this.rev = rev; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Migration getActive() { + return active; + } + + @JsonView(View.Persistence.class) + public void setActive(final Migration active) { + this.active = active; + } + + public void setActive(final String id, final Date start) { + this.setActive(new Migration(id, start)); + } + + @JsonView({View.Persistence.class, View.Public.class}) + public List<String> getCompleted() { + return completed; + } + + @JsonView(View.Persistence.class) + public void setCompleted(final List<String> completed) { + this.completed = completed; + } + + /** + * {@inheritDoc} + * + * All fields of <tt>MigrationState</tt> are included in equality checks. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final MigrationState that = (MigrationState) o; + + return Objects.equals(active, that.active) && + Objects.equals(completed, that.completed); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Motd.java b/src/main/java/de/thm/arsnova/model/Motd.java new file mode 100644 index 0000000000000000000000000000000000000000..d67c19fb15739a29ab1c2f1010eaf1839e8d2e96 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/Motd.java @@ -0,0 +1,131 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; +import java.util.Objects; + +public class Motd extends Entity { + public enum Audience { + ALL, + AUTHENTICATED, + AUTHORS, + PARTICIPANTS, + ROOM + } + + private String roomId; + private Date startDate; + private Date endDate; + private String title; + private String body; + private Audience audience; + + @Override + @JsonView(View.Persistence.class) + public Date getUpdateTimestamp() { + return updateTimestamp; + } + + @Override + @JsonView(View.Persistence.class) + public void setUpdateTimestamp(final Date updateTimestamp) { + this.updateTimestamp = updateTimestamp; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getRoomId() { + return roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRoomId(final String roomId) { + this.roomId = roomId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Date getStartDate() { + return startDate; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setStartDate(final Date startDate) { + this.startDate = startDate; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Date getEndDate() { + return endDate; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setEndDate(final Date endDate) { + this.endDate = endDate; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getTitle() { + return title; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setTitle(final String title) { + this.title = title; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getBody() { + return body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setBody(final String body) { + this.body = body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Audience getAudience() { + return audience; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAudience(final Audience audience) { + this.audience = audience; + } + + /** + * {@inheritDoc} + * + * All fields of <tt>Motd</tt> are included in equality checks. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final Motd motd = (Motd) o; + + return Objects.equals(roomId, motd.roomId) && + Objects.equals(startDate, motd.startDate) && + Objects.equals(endDate, motd.endDate) && + Objects.equals(title, motd.title) && + Objects.equals(body, motd.body) && + audience == motd.audience; + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("roomId", roomId) + .append("startDate", startDate) + .append("endDate", endDate) + .append("title", title) + .append("body", body) + .append("audience", audience); + } +} diff --git a/src/main/java/de/thm/arsnova/model/Room.java b/src/main/java/de/thm/arsnova/model/Room.java new file mode 100644 index 0000000000000000000000000000000000000000..cb85893efb65ea757f8da5cefe27c5beaaf83ca5 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/Room.java @@ -0,0 +1,486 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class Room extends Entity { + public static class ContentGroup { + private Set<String> contentIds; + private boolean autoSort; + + @JsonView({View.Persistence.class, View.Public.class}) + public Set<String> getContentIds() { + if (contentIds == null) { + contentIds = new HashSet<>(); + } + + return contentIds; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setContentIds(final Set<String> contentIds) { + this.contentIds = contentIds; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isAutoSort() { + return autoSort; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAutoSort(final boolean autoSort) { + this.autoSort = autoSort; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("contentIds", contentIds) + .append("autoSort", autoSort) + .toString(); + } + } + + public static class Settings { + private boolean questionsEnabled = true; + private boolean slidesEnabled = true; + private boolean commentsEnabled = true; + private boolean flashcardsEnabled = true; + private boolean quickSurveyEnabled = true; + private boolean quickFeedbackEnabled = true; + private boolean scoreEnabled = true; + private boolean multipleRoundsEnabled = true; + private boolean timerEnabled = true; + private boolean feedbackLocked = false; + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isQuestionsEnabled() { + return questionsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setQuestionsEnabled(final boolean questionsEnabled) { + this.questionsEnabled = questionsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isSlidesEnabled() { + return slidesEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setSlidesEnabled(final boolean slidesEnabled) { + this.slidesEnabled = slidesEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isCommentsEnabled() { + return commentsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setCommentsEnabled(final boolean commentsEnabled) { + this.commentsEnabled = commentsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isFlashcardsEnabled() { + return flashcardsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setFlashcardsEnabled(final boolean flashcardsEnabled) { + this.flashcardsEnabled = flashcardsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isQuickSurveyEnabled() { + return quickSurveyEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setQuickSurveyEnabled(boolean quickSurveyEnabled) { + this.quickSurveyEnabled = quickSurveyEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isQuickFeedbackEnabled() { + return quickFeedbackEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setQuickFeedbackEnabled(boolean quickFeedbackEnabled) { + this.quickFeedbackEnabled = quickFeedbackEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isScoreEnabled() { + return scoreEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setScoreEnabled(final boolean scoreEnabled) { + this.scoreEnabled = scoreEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isMultipleRoundsEnabled() { + return multipleRoundsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setMultipleRoundsEnabled(final boolean multipleRoundsEnabled) { + this.multipleRoundsEnabled = multipleRoundsEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isTimerEnabled() { + return timerEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setTimerEnabled(final boolean timerEnabled) { + this.timerEnabled = timerEnabled; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isFeedbackLocked() { + return feedbackLocked; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setFeedbackLocked(final boolean feedbackLocked) { + this.feedbackLocked = feedbackLocked; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("questionsEnabled", questionsEnabled) + .append("slidesEnabled", slidesEnabled) + .append("commentsEnabled", commentsEnabled) + .append("flashcardsEnabled", flashcardsEnabled) + .append("quickSurveyEnabled", quickSurveyEnabled) + .append("quickFeedbackEnabled", quickFeedbackEnabled) + .append("scoreEnabled", scoreEnabled) + .append("multipleRoundsEnabled", multipleRoundsEnabled) + .append("timerEnabled", timerEnabled) + .append("feedbackLocked", feedbackLocked) + .toString(); + } + } + + public static class Author { + private String name; + private String mail; + private String organizationName; + private String organizationLogo; + private String organizationUnit; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getName() { + return name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setName(final String name) { + this.name = name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getMail() { + return mail; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setMail(final String mail) { + this.mail = mail; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getOrganizationName() { + return organizationName; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setOrganizationName(final String organizationName) { + this.organizationName = organizationName; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getOrganizationLogo() { + return organizationLogo; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setOrganizationLogo(final String organizationLogo) { + this.organizationLogo = organizationLogo; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getOrganizationUnit() { + return organizationUnit; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setOrganizationUnit(final String organizationUnit) { + this.organizationUnit = organizationUnit; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("name", name) + .append("mail", mail) + .append("organizationName", organizationName) + .append("organizationLogo", organizationLogo) + .append("organizationUnit", organizationUnit) + .toString(); + } + } + + public static class PoolProperties { + private String category; + private String level; + private String license; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getCategory() { + return category; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setCategory(final String category) { + this.category = category; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getLevel() { + return level; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setLevel(final String level) { + this.level = level; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getLicense() { + return license; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setLicense(final String license) { + this.license = license; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("category", category) + .append("level", level) + .append("license", license) + .toString(); + } + } + + private String shortId; + private String ownerId; + private String name; + private String abbreviation; + private String description; + private boolean closed; + private Map<String, ContentGroup> contentGroups; + private Settings settings; + private Author author; + private PoolProperties poolProperties; + private Map<String, Map<String, ?>> extensions; + private Map<String, String> attachments; + private RoomStatistics statistics; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getShortId() { + return shortId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setShortId(final String shortId) { + this.shortId = shortId; + } + + @JsonView(View.Persistence.class) + public String getOwnerId() { + return ownerId; + } + + @JsonView(View.Persistence.class) + public void setOwnerId(final String ownerId) { + this.ownerId = ownerId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getName() { + return name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setName(final String name) { + this.name = name; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getAbbreviation() { + return abbreviation; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAbbreviation(final String abbreviation) { + this.abbreviation = abbreviation; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getDescription() { + return description; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setDescription(final String description) { + this.description = description; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isClosed() { + return closed; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setClosed(final boolean closed) { + this.closed = closed; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, ContentGroup> getContentGroups() { + if (contentGroups == null) { + contentGroups = new HashMap<>(); + } + + return contentGroups; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setContentGroups(final Map<String, ContentGroup> contentGroups) { + this.contentGroups = contentGroups; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Settings getSettings() { + if (settings == null) { + settings = new Settings(); + } + + return settings; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setSettings(Settings settings) { + this.settings = settings; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Author getAuthor() { + return author; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAuthor(final Author author) { + this.author = author; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public PoolProperties getPoolProperties() { + return poolProperties; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setPoolProperties(final PoolProperties poolProperties) { + this.poolProperties = poolProperties; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, Map<String, ?>> getExtensions() { + return extensions; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setExtensions(final Map<String, Map<String, ?>> extensions) { + this.extensions = extensions; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, String> getAttachments() { + return attachments; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setAttachments(final Map<String, String> attachments) { + this.attachments = attachments; + } + + @JsonView(View.Public.class) + public RoomStatistics getStatistics() { + return statistics; + } + + public void setStatistics(final RoomStatistics statistics) { + this.statistics = statistics; + } + + /** + * {@inheritDoc} + * + * The following fields of <tt>Room</tt> are excluded from equality checks: + * {@link #contentGroups}, {@link #settings}, {@link #author}, {@link #poolProperties}, {@link #extensions}, + * {@link #attachments}, {@link #statistics}. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final Room room = (Room) o; + + return closed == room.closed && + Objects.equals(shortId, room.shortId) && + Objects.equals(ownerId, room.ownerId) && + Objects.equals(name, room.name) && + Objects.equals(abbreviation, room.abbreviation) && + Objects.equals(description, room.description); + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("shortId", shortId) + .append("ownerId", ownerId) + .append("name", name) + .append("abbreviation", abbreviation) + .append("description", description) + .append("closed", closed) + .append("contentGroups", contentGroups) + .append("settings", settings) + .append("author", author) + .append("poolProperties", poolProperties) + .append("attachments", attachments) + .append("statistics", statistics); + } +} diff --git a/src/main/java/de/thm/arsnova/model/RoomStatistics.java b/src/main/java/de/thm/arsnova/model/RoomStatistics.java new file mode 100644 index 0000000000000000000000000000000000000000..c2a94122ce12144b524dfc1ef8961bd7fd228935 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/RoomStatistics.java @@ -0,0 +1,81 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +public class RoomStatistics { + private int contentCount = 0; + private int unansweredContentCount = 0; + private int answerCount = 0; + private int unreadAnswerCount = 0; + private int commentCount = 0; + private int unreadCommentCount = 0; + + @JsonView(View.Public.class) + public int getUnansweredContentCount() { + return unansweredContentCount; + } + + @JsonView(View.Public.class) + public void setUnansweredContentCount(final int unansweredContentCount) { + this.unansweredContentCount = unansweredContentCount; + } + + @JsonView(View.Public.class) + public int getContentCount() { + return contentCount; + } + + public void setContentCount(final int contentCount) { + this.contentCount = contentCount; + } + + @JsonView(View.Public.class) + public int getAnswerCount() { + return answerCount; + } + + public void setAnswerCount(final int answerCount) { + this.answerCount = answerCount; + } + + @JsonView(View.Public.class) + public int getUnreadAnswerCount() { + return unreadAnswerCount; + } + + public void setUnreadAnswerCount(final int unreadAnswerCount) { + this.unreadAnswerCount = unreadAnswerCount; + } + + @JsonView(View.Public.class) + public int getCommentCount() { + return commentCount; + } + + public void setCommentCount(final int commentCount) { + this.commentCount = commentCount; + } + + @JsonView(View.Public.class) + public int getUnreadCommentCount() { + return unreadCommentCount; + } + + public void setUnreadCommentCount(final int unreadCommentCount) { + this.unreadCommentCount = unreadCommentCount; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("contentCount", contentCount) + .append("unansweredContentCount", unansweredContentCount) + .append("answerCount", answerCount) + .append("unreadAnswerCount", unreadAnswerCount) + .append("commentCount", commentCount) + .append("unreadCommentCount", unreadCommentCount) + .toString(); + } +} diff --git a/src/main/java/de/thm/arsnova/entities/ScoreOptions.java b/src/main/java/de/thm/arsnova/model/ScoreOptions.java similarity index 88% rename from src/main/java/de/thm/arsnova/entities/ScoreOptions.java rename to src/main/java/de/thm/arsnova/model/ScoreOptions.java index 4b032b1027819627c7a39a46b41ec661a338ce59..c1791b437afa2a4369477f3542bbebb8d0800521 100644 --- a/src/main/java/de/thm/arsnova/entities/ScoreOptions.java +++ b/src/main/java/de/thm/arsnova/model/ScoreOptions.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -34,7 +34,7 @@ public class ScoreOptions implements Serializable { private String questionVariant = ""; - public ScoreOptions(ScoreOptions scoreOptions) { + public ScoreOptions(final ScoreOptions scoreOptions) { this(); this.type = scoreOptions.getType(); this.questionVariant = scoreOptions.getQuestionVariant(); @@ -49,7 +49,7 @@ public class ScoreOptions implements Serializable { } @JsonView({View.Persistence.class, View.Public.class}) - public void setType(String type) { + public void setType(final String type) { this.type = type; } @@ -60,7 +60,7 @@ public class ScoreOptions implements Serializable { } @JsonView({View.Persistence.class, View.Public.class}) - public void setQuestionVariant(String questionVariant) { + public void setQuestionVariant(final String questionVariant) { this.questionVariant = questionVariant; } } diff --git a/src/main/java/de/thm/arsnova/entities/ServiceDescription.java b/src/main/java/de/thm/arsnova/model/ServiceDescription.java similarity index 75% rename from src/main/java/de/thm/arsnova/entities/ServiceDescription.java rename to src/main/java/de/thm/arsnova/model/ServiceDescription.java index 33c1599581206b4199584a4149378a769de7ec62..b7724a921300591a7b153046d6da2c23aa26793f 100644 --- a/src/main/java/de/thm/arsnova/entities/ServiceDescription.java +++ b/src/main/java/de/thm/arsnova/model/ServiceDescription.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; /** * A login service description. For example, this class is used to display the login buttons in ARSnova mobile. @@ -31,20 +31,20 @@ public class ServiceDescription { private int order = 0; private String[] allowedRoles; - public ServiceDescription(String id, String name, String dialogUrl) { + public ServiceDescription(final String id, final String name, final String dialogUrl) { this.id = id; this.name = name; this.dialogUrl = dialogUrl; } - public ServiceDescription(String id, String name, String dialogUrl, String[] allowedRoles) { + public ServiceDescription(final String id, final String name, final String dialogUrl, final String[] allowedRoles) { this.id = id; this.name = name; this.dialogUrl = dialogUrl; this.allowedRoles = allowedRoles; } - public ServiceDescription(String id, String name, String dialogUrl, String[] allowedRoles, String image) { + public ServiceDescription(final String id, final String name, final String dialogUrl, final String[] allowedRoles, final String image) { this.id = id; this.name = name; this.dialogUrl = dialogUrl; @@ -59,7 +59,7 @@ public class ServiceDescription { return id; } - public void setId(String id) { + public void setId(final String id) { this.id = id; } @@ -68,7 +68,7 @@ public class ServiceDescription { return name; } - public void setName(String name) { + public void setName(final String name) { this.name = name; } @@ -77,7 +77,7 @@ public class ServiceDescription { return dialogUrl; } - public void setDialogUrl(String dialogUrl) { + public void setDialogUrl(final String dialogUrl) { this.dialogUrl = dialogUrl; } @@ -86,7 +86,7 @@ public class ServiceDescription { return image; } - public void setImage(String image) { + public void setImage(final String image) { this.image = image; } @@ -95,7 +95,7 @@ public class ServiceDescription { return order; } - public void setOrder(int order) { + public void setOrder(final int order) { this.order = order; } @@ -104,7 +104,7 @@ public class ServiceDescription { return allowedRoles; } - public void setAllowedRoles(String[] roles) { + public void setAllowedRoles(final String[] roles) { this.allowedRoles = roles; } } diff --git a/src/main/java/de/thm/arsnova/entities/Statistics.java b/src/main/java/de/thm/arsnova/model/Statistics.java similarity index 93% rename from src/main/java/de/thm/arsnova/entities/Statistics.java rename to src/main/java/de/thm/arsnova/model/Statistics.java index ae3eda70d1b43e9f308d9d73d597f8f6f186b66a..fe7f38f4c91b40a725d7bc549ebd0fc8287521c4 100644 --- a/src/main/java/de/thm/arsnova/entities/Statistics.java +++ b/src/main/java/de/thm/arsnova/model/Statistics.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -129,7 +129,7 @@ public class Statistics { return interposedQuestions; } - public void setInterposedQuestions(int interposedQuestions) { + public void setInterposedQuestions(final int interposedQuestions) { this.interposedQuestions = interposedQuestions; } @@ -139,7 +139,7 @@ public class Statistics { return flashcards; } - public void setFlashcards(int flashcards) { + public void setFlashcards(final int flashcards) { this.flashcards = flashcards; } @@ -149,7 +149,7 @@ public class Statistics { return creators; } - public void setCreators(int creators) { + public void setCreators(final int creators) { this.creators = creators; } @@ -159,7 +159,7 @@ public class Statistics { return conceptQuestions; } - public void setConceptQuestions(int conceptQuestions) { + public void setConceptQuestions(final int conceptQuestions) { this.conceptQuestions = conceptQuestions; } @@ -169,7 +169,7 @@ public class Statistics { return activeStudents; } - public void setActiveStudents(int activeStudents) { + public void setActiveStudents(final int activeStudents) { this.activeStudents = activeStudents; } diff --git a/src/main/java/de/thm/arsnova/model/TextAnswer.java b/src/main/java/de/thm/arsnova/model/TextAnswer.java new file mode 100644 index 0000000000000000000000000000000000000000..2f109c085b31571227c91706310a23ed9369b9eb --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/TextAnswer.java @@ -0,0 +1,57 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; + +public class TextAnswer extends Answer { + private String subject; + private String body; + private boolean read; + + @JsonView({View.Persistence.class, View.Public.class}) + public String getSubject() { + return subject; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setSubject(final String subject) { + this.subject = subject; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getBody() { + return body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setBody(final String body) { + this.body = body; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public boolean isRead() { + return read; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setRead(boolean read) { + this.read = read; + } + + @Override + @JsonView({View.Persistence.class, View.Public.class}) + public Date getCreationTimestamp() { + return creationTimestamp; + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("subject", subject) + .append("body", body) + .append("read", read); + } +} diff --git a/src/main/java/de/thm/arsnova/model/UserProfile.java b/src/main/java/de/thm/arsnova/model/UserProfile.java new file mode 100644 index 0000000000000000000000000000000000000000..0fc7932733ddc4f3514ebc04dab0323202ad94e0 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/UserProfile.java @@ -0,0 +1,265 @@ +package de.thm.arsnova.model; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.serialization.View; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +public class UserProfile extends Entity { + public enum AuthProvider { + NONE, + UNKNOWN, + ARSNOVA, + ARSNOVA_GUEST, + LDAP, + CAS, + GOOGLE, + FACEBOOK, + TWITTER + } + + public static class Account { + private String password; + private String activationKey; + private String passwordResetKey; + private Date passwordResetTime; + + @JsonView(View.Persistence.class) + public String getPassword() { + return password; + } + + @JsonView(View.Persistence.class) + public void setPassword(final String password) { + this.password = password; + } + + @JsonView(View.Persistence.class) + public String getActivationKey() { + return activationKey; + } + + @JsonView(View.Persistence.class) + public void setActivationKey(final String activationKey) { + this.activationKey = activationKey; + } + + @JsonView(View.Persistence.class) + public String getPasswordResetKey() { + return passwordResetKey; + } + + @JsonView(View.Persistence.class) + public void setPasswordResetKey(final String passwordResetKey) { + this.passwordResetKey = passwordResetKey; + } + + @JsonView(View.Persistence.class) + public Date getPasswordResetTime() { + return passwordResetTime; + } + + @JsonView(View.Persistence.class) + public void setPasswordResetTime(final Date passwordResetTime) { + this.passwordResetTime = passwordResetTime; + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("password", password) + .append("activationKey", activationKey) + .append("passwordResetKey", passwordResetKey) + .append("passwordResetTime", passwordResetTime) + .toString(); + } + } + + public static class RoomHistoryEntry { + private String roomId; + private Date lastVisit; + + public RoomHistoryEntry() { + + } + + public RoomHistoryEntry(String roomId, Date lastVisit) { + this.roomId = roomId; + this.lastVisit = lastVisit; + } + + @JsonView(View.Persistence.class) + public String getRoomId() { + return roomId; + } + + @JsonView(View.Persistence.class) + public void setRoomId(String roomId) { + this.roomId = roomId; + } + + @JsonView(View.Persistence.class) + public Date getLastVisit() { + return lastVisit; + } + + @JsonView(View.Persistence.class) + public void setLastVisit(Date lastVisit) { + this.lastVisit = lastVisit; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final RoomHistoryEntry that = (RoomHistoryEntry) o; + + return Objects.equals(roomId, that.roomId); + } + + @Override + public int hashCode() { + return Objects.hash(roomId); + } + + @Override + public String toString() { + return new ToStringCreator(this) + .append("roomId", roomId) + .append("lastVisit", lastVisit) + .toString(); + } + } + + private AuthProvider authProvider; + private String loginId; + private Date lastLoginTimestamp; + private Account account; + /* TODO: Review - is a Map more appropriate? + * pro List: can be ordered by date + * pro Map (roomId -> RoomHistoryEntry): easier to access for updating lastVisit + * -> Map but custom serialization to array? */ + private Set<RoomHistoryEntry> roomHistory = new HashSet<>(); + private Set<String> acknowledgedMotds = new HashSet<>(); + private Map<String, Map<String, ?>> extensions; + + public UserProfile() { + + } + + public UserProfile(final AuthProvider authProvider, final String loginId) { + this.authProvider = authProvider; + this.loginId = loginId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public AuthProvider getAuthProvider() { + return authProvider; + } + + @JsonView(View.Persistence.class) + public void setAuthProvider(final AuthProvider authProvider) { + this.authProvider = authProvider; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public String getLoginId() { + return loginId; + } + + @JsonView(View.Persistence.class) + public void setLoginId(final String loginId) { + this.loginId = loginId; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Date getLastLoginTimestamp() { + return lastLoginTimestamp; + } + + @JsonView(View.Persistence.class) + public void setLastLoginTimestamp(final Date lastLoginTimestamp) { + this.lastLoginTimestamp = lastLoginTimestamp; + } + + @JsonView(View.Persistence.class) + public Account getAccount() { + return account; + } + + @JsonView(View.Persistence.class) + public void setAccount(final Account account) { + this.account = account; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Set<RoomHistoryEntry> getRoomHistory() { + return roomHistory; + } + + @JsonView(View.Persistence.class) + public void setRoomHistory(final Set<RoomHistoryEntry> roomHistory) { + this.roomHistory = roomHistory; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Set<String> getAcknowledgedMotds() { + return acknowledgedMotds; + } + + @JsonView(View.Persistence.class) + public void setAcknowledgedMotds(final Set<String> acknowledgedMotds) { + this.acknowledgedMotds = acknowledgedMotds; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public Map<String, Map<String, ?>> getExtensions() { + return extensions; + } + + @JsonView({View.Persistence.class, View.Public.class}) + public void setExtensions(Map<String, Map<String, ?>> extensions) { + this.extensions = extensions; + } + + /** + * {@inheritDoc} + * + * The following fields of <tt>UserProfile</tt> are excluded from equality checks: + * {@link #account}, {@link #roomHistory}, {@link #acknowledgedMotds}, {@link #extensions}. + */ + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (!super.equals(o)) { + return false; + } + final UserProfile that = (UserProfile) o; + + return authProvider == that.authProvider && + Objects.equals(loginId, that.loginId) && + Objects.equals(lastLoginTimestamp, that.lastLoginTimestamp); + } + + @Override + protected ToStringCreator buildToString() { + return super.buildToString() + .append("authProvider", authProvider) + .append("loginId", loginId) + .append("lastLoginTimestamp", lastLoginTimestamp) + .append("account", account) + .append("roomHistory", roomHistory) + .append("acknowledgedMotds", acknowledgedMotds); + } +} diff --git a/src/main/java/de/thm/arsnova/model/migration/FromV2Migrator.java b/src/main/java/de/thm/arsnova/model/migration/FromV2Migrator.java new file mode 100644 index 0000000000000000000000000000000000000000..281e6c3fada5c1d30c90a6410dabb4c8e732ebff --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/migration/FromV2Migrator.java @@ -0,0 +1,360 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model.migration; + +import de.thm.arsnova.model.ChoiceAnswer; +import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.TextAnswer; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.model.migration.v2.*; +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Converts legacy entities from version 2 to current model version. + * + * @author Daniel Gerhardt + */ +public class FromV2Migrator { + static final String V2_TYPE_ABCD = "abcd"; + static final String V2_TYPE_SC = "sc"; + static final String V2_TYPE_MC = "mc"; + static final String V2_TYPE_VOTE = "vote"; + static final String V2_TYPE_SCHOOL = "school"; + static final String V2_TYPE_YESNO = "yesno"; + static final String V2_TYPE_FREETEXT = "freetext"; + static final String V2_TYPE_GRID = "grid"; + private static final Map<String, de.thm.arsnova.model.Content.Format> formatMapping; + + private boolean ignoreRevision = false; + + static { + formatMapping = new HashMap<>(); + formatMapping.put(V2_TYPE_ABCD, de.thm.arsnova.model.Content.Format.CHOICE); + formatMapping.put(V2_TYPE_SC, de.thm.arsnova.model.Content.Format.CHOICE); + formatMapping.put(V2_TYPE_MC, de.thm.arsnova.model.Content.Format.CHOICE); + formatMapping.put(V2_TYPE_VOTE, de.thm.arsnova.model.Content.Format.SCALE); + formatMapping.put(V2_TYPE_SCHOOL, de.thm.arsnova.model.Content.Format.SCALE); + formatMapping.put(V2_TYPE_YESNO, de.thm.arsnova.model.Content.Format.BINARY); + formatMapping.put(V2_TYPE_FREETEXT, de.thm.arsnova.model.Content.Format.TEXT); + formatMapping.put(V2_TYPE_GRID, de.thm.arsnova.model.Content.Format.GRID); + } + + private void copyCommonProperties(final Entity from, final de.thm.arsnova.model.Entity to) { + to.setId(from.getId()); + if (!ignoreRevision) { + to.setRevision(from.getRevision()); + } + } + + public UserProfile migrate(final DbUser dbUser, final LoggedIn loggedIn, final MotdList motdList) { + if (dbUser != null && loggedIn != null && !loggedIn.getUser().equals(dbUser.getUsername())) { + throw new IllegalArgumentException("Username of loggedIn object does not match."); + } + if (dbUser != null && motdList != null && !motdList.getUsername().equals(dbUser.getUsername())) { + throw new IllegalArgumentException("Username of motdList object does not match."); + } + if (loggedIn != null && motdList != null && !loggedIn.getUser().equals(motdList.getUsername())) { + throw new IllegalArgumentException("Usernames of loggedIn and motdList objects do not match."); + } + final UserProfile profile = new UserProfile(); + if (dbUser != null) { + copyCommonProperties(dbUser, profile); + profile.setLoginId(dbUser.getUsername()); + profile.setAuthProvider(UserProfile.AuthProvider.ARSNOVA); + profile.setCreationTimestamp(new Date(dbUser.getCreation())); + profile.setUpdateTimestamp(new Date()); + UserProfile.Account account = new UserProfile.Account(); + profile.setAccount(account); + account.setPassword(dbUser.getPassword()); + account.setActivationKey(dbUser.getActivationKey()); + account.setPasswordResetKey(dbUser.getPasswordResetKey()); + account.setPasswordResetTime(new Date(dbUser.getPasswordResetTime())); + } + if (loggedIn != null) { + if (dbUser == null) { + copyCommonProperties(loggedIn, profile); + profile.setLoginId(loggedIn.getUser()); + profile.setAuthProvider(detectAuthProvider(profile.getLoginId())); + profile.setCreationTimestamp(new Date()); + } + profile.setLastLoginTimestamp(new Date(loggedIn.getTimestamp())); + Set<UserProfile.RoomHistoryEntry> sessionHistory = loggedIn.getVisitedSessions().stream() + .map(entry -> new UserProfile.RoomHistoryEntry(entry.getId(), new Date(0))) + .collect(Collectors.toSet()); + profile.setRoomHistory(sessionHistory); + } + if (motdList != null && motdList.getMotdkeys() != null) { + profile.setAcknowledgedMotds(migrate(motdList)); + } + + return profile; + } + + public Set<String> migrate(final MotdList motdList) { + return Arrays.stream(motdList.getMotdkeys().split(",")).collect(Collectors.toSet()); + } + + public de.thm.arsnova.model.Room migrate(final Room from, final Optional<UserProfile> owner) { + if (!owner.isPresent() && from.getCreator() != null || + owner.isPresent() && !owner.get().getLoginId().equals(from.getCreator())) { + throw new IllegalArgumentException("Username of owner object does not match session creator."); + } + final de.thm.arsnova.model.Room to = new de.thm.arsnova.model.Room(); + copyCommonProperties(from, to); + to.setCreationTimestamp(new Date(from.getCreationTime())); + to.setUpdateTimestamp(new Date()); + to.setShortId(from.getKeyword()); + if (owner.isPresent()) { + to.setOwnerId(owner.get().getId()); + } + to.setName(from.getName()); + to.setAbbreviation(from.getShortName()); + to.setDescription(from.getPpDescription()); + to.setClosed(!from.isActive()); + if (from.hasAuthorDetails()) { + final de.thm.arsnova.model.Room.Author author = new de.thm.arsnova.model.Room.Author(); + to.setAuthor(author); + author.setName(from.getPpAuthorName()); + author.setMail(from.getPpAuthorMail()); + author.setOrganizationName(from.getPpUniversity()); + author.setOrganizationUnit(from.getPpFaculty()); + author.setOrganizationLogo(from.getPpLogo()); + } + if ("public_pool".equals(from.getSessionType())) { + final de.thm.arsnova.model.Room.PoolProperties poolProperties = new de.thm.arsnova.model.Room.PoolProperties(); + to.setPoolProperties(poolProperties); + poolProperties.setLevel(from.getPpLevel()); + poolProperties.setCategory(from.getPpSubject()); + poolProperties.setLicense(from.getPpLicense()); + } + to.setSettings(migrate(from.getFeatures())); + + return to; + } + + public de.thm.arsnova.model.Room migrate(final Room from) { + return migrate(from, Optional.empty()); + } + + public de.thm.arsnova.model.Room.Settings migrate(final RoomFeature feature) { + de.thm.arsnova.model.Room.Settings settings = new de.thm.arsnova.model.Room.Settings(); + if (feature != null) { + settings.setCommentsEnabled(feature.isInterposed() || feature.isInterposedFeedback() + || feature.isTwitterWall() || feature.isTotal()); + settings.setQuestionsEnabled(feature.isLecture() || feature.isJitt() || feature.isClicker() || feature.isTotal()); + settings.setSlidesEnabled(feature.isSlides() || feature.isTotal()); + settings.setFlashcardsEnabled(feature.isFlashcardFeature() || feature.isFlashcard() || feature.isTotal()); + settings.setQuickSurveyEnabled(feature.isLiveClicker()); + settings.setQuickFeedbackEnabled(feature.isFeedback() || feature.isLiveFeedback() || feature.isTotal()); + settings.setMultipleRoundsEnabled(feature.isPi() || feature.isClicker() || feature.isTotal()); + settings.setTimerEnabled(feature.isPi() || feature.isClicker() || feature.isTotal()); + settings.setScoreEnabled(feature.isLearningProgress() || feature.isTotal()); + } + + return settings; + } + + public de.thm.arsnova.model.Content migrate(final Content from) { + de.thm.arsnova.model.Content to; + switch (from.getQuestionType()) { + case V2_TYPE_ABCD: + case V2_TYPE_SC: + case V2_TYPE_MC: + case V2_TYPE_VOTE: + case V2_TYPE_SCHOOL: + case V2_TYPE_YESNO: + ChoiceQuestionContent choiceQuestionContent = new ChoiceQuestionContent(); + to = choiceQuestionContent; + to.setFormat(formatMapping.get(from.getQuestionType())); + choiceQuestionContent.setMultiple(V2_TYPE_MC.equals(from.getQuestionType())); + for (int i = 0; i < from.getPossibleAnswers().size(); i++) { + de.thm.arsnova.model.migration.v2.AnswerOption fromOption = from.getPossibleAnswers().get(i); + ChoiceQuestionContent.AnswerOption toOption = new ChoiceQuestionContent.AnswerOption(); + toOption.setLabel(fromOption.getText()); + toOption.setPoints(fromOption.getValue()); + choiceQuestionContent.getOptions().add(toOption); + if (fromOption.isCorrect()) { + choiceQuestionContent.getCorrectOptionIndexes().add(i); + } + } + + break; + case V2_TYPE_FREETEXT: + to = new de.thm.arsnova.model.Content(); + to.setFormat(de.thm.arsnova.model.Content.Format.TEXT); + break; + default: + throw new IllegalArgumentException("Unsupported content format."); + } + copyCommonProperties(from, to); + to.setRoomId(from.getSessionId()); + to.getGroups().add(from.getQuestionVariant()); + to.setSubject(from.getSubject()); + to.setBody(from.getText()); + to.setAbstentionsAllowed(from.isAbstention()); + to.setAbstentionsAllowed(from.isAbstention()); + de.thm.arsnova.model.Content.State state = to.getState(); + state.setRound(from.getPiRound()); + state.setVisible(from.isActive()); + state.setResponsesVisible(from.isShowStatistic()); + state.setSolutionVisible(from.isShowAnswer()); + state.setResponsesEnabled(!from.isVotingDisabled()); + + return to; + } + + public de.thm.arsnova.model.Answer migrate(final Answer from, final de.thm.arsnova.model.Content content) { + if (content instanceof ChoiceQuestionContent) { + ChoiceQuestionContent choiceQuestionContent = (ChoiceQuestionContent) content; + return migrate(from, choiceQuestionContent.getOptions(), choiceQuestionContent.isMultiple()); + } else { + return migrate(from); + } + } + + public ChoiceAnswer migrate(final Answer from, final List<ChoiceQuestionContent.AnswerOption> options, final boolean multiple) { + final ChoiceAnswer to = new ChoiceAnswer(); + copyCommonProperties(from, to); + to.setContentId(from.getQuestionId()); + to.setRoomId(from.getSessionId()); + to.setRound(from.getPiRound()); + List<Integer> selectedChoiceIndexes = new ArrayList<>(); + to.setSelectedChoiceIndexes(selectedChoiceIndexes); + + if (!from.isAbstention()) { + if (multiple) { + List<Boolean> flags = Arrays.stream(from.getAnswerText().split(",")) + .map("1"::equals).collect(Collectors.toList()); + if (flags.size() != options.size()) { + throw new IndexOutOfBoundsException( + "Number of answer's choice flags does not match number of content's answer options"); + } + int i = 0; + for (boolean flag : flags) { + if (flag) { + selectedChoiceIndexes.add(i); + } + i++; + } + } else { + int i = 0; + for (ChoiceQuestionContent.AnswerOption option : options) { + if (option.getLabel().equals(from.getAnswerText())) { + selectedChoiceIndexes.add(i); + break; + } + i++; + } + } + } + + return to; + } + + public TextAnswer migrate(final Answer from) { + final TextAnswer to = new TextAnswer(); + copyCommonProperties(from, to); + to.setContentId(from.getQuestionId()); + to.setRoomId(from.getSessionId()); + to.setRound(from.getPiRound()); + to.setSubject(from.getAnswerSubject()); + to.setBody(from.getAnswerText()); + + return to; + } + + public de.thm.arsnova.model.Comment migrate(final Comment from, @Nullable final UserProfile creator) { + if (creator == null && from.getCreator() != null || + creator != null && !creator.getLoginId().equals(from.getCreator())) { + throw new IllegalArgumentException("Username of creator object does not match comment creator."); + } + final de.thm.arsnova.model.Comment to = new de.thm.arsnova.model.Comment(); + copyCommonProperties(from, to); + to.setRoomId(from.getSessionId()); + if (creator != null) { + to.setCreatorId(creator.getId()); + } + to.setSubject(from.getSubject()); + to.setBody(from.getText()); + to.setTimestamp(new Date(from.getTimestamp())); + to.setRead(from.isRead()); + + return to; + } + + + public de.thm.arsnova.model.Comment migrate(final Comment from) { + return migrate(from, null); + } + + public de.thm.arsnova.model.Motd migrate(final Motd from) { + final de.thm.arsnova.model.Motd to = new de.thm.arsnova.model.Motd(); + copyCommonProperties(from, to); + to.setCreationTimestamp(from.getStartdate()); + to.setUpdateTimestamp(new Date()); + to.setStartDate(from.getStartdate()); + to.setEndDate(from.getEnddate()); + switch (from.getAudience()) { + case "all": + to.setAudience(de.thm.arsnova.model.Motd.Audience.ALL); + break; + case "tutors": + to.setAudience(de.thm.arsnova.model.Motd.Audience.AUTHORS); + break; + case "students": + to.setAudience(de.thm.arsnova.model.Motd.Audience.PARTICIPANTS); + break; + case "session": + to.setAudience(de.thm.arsnova.model.Motd.Audience.ROOM); + break; + } + to.setTitle(from.getTitle()); + to.setBody(from.getText()); + to.setRoomId(from.getSessionId()); + + return to; + } + + private UserProfile.AuthProvider detectAuthProvider(final String loginId) { + if (loginId.length() == 15 && loginId.startsWith("Guest")) { + return UserProfile.AuthProvider.ARSNOVA_GUEST; + } + if (loginId.startsWith("https://www.facebook.com/") || loginId.startsWith("http://www.facebook.com/")) { + return UserProfile.AuthProvider.FACEBOOK; + } + if (loginId.contains("@")) { + return UserProfile.AuthProvider.GOOGLE; + } + + return UserProfile.AuthProvider.UNKNOWN; + } + + public void setIgnoreRevision(final boolean ignoreRevision) { + this.ignoreRevision = ignoreRevision; + } +} diff --git a/src/main/java/de/thm/arsnova/model/migration/ToV2Migrator.java b/src/main/java/de/thm/arsnova/model/migration/ToV2Migrator.java new file mode 100644 index 0000000000000000000000000000000000000000..22ffea137c93846b8414e9df1d15eeff8cbd2266 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/migration/ToV2Migrator.java @@ -0,0 +1,395 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model.migration; + +import de.thm.arsnova.model.AnswerStatistics; +import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.RoomStatistics; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.model.migration.v2.*; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static de.thm.arsnova.model.migration.FromV2Migrator.*; + +/** + * Converts entities from current model version to legacy version 2. + * + * @author Daniel Gerhardt + */ +public class ToV2Migrator { + + private void copyCommonProperties(final de.thm.arsnova.model.Entity from, final Entity to) { + to.setId(from.getId()); + to.setRevision(from.getRevision()); + } + + public LoggedIn migrateLoggedIn(final UserProfile from) { + final LoggedIn to = new LoggedIn(); + copyCommonProperties(from, to); + to.setUser(from.getLoginId()); + to.setTimestamp(from.getLastLoginTimestamp().getTime()); + to.setVisitedSessions(from.getRoomHistory().stream() + .map(entry -> new VisitedRoom()) + .collect(Collectors.toList())); + + return to; + } + + public MotdList migrateMotdList(final UserProfile from) { + final MotdList to = new MotdList(); + copyCommonProperties(from, to); + to.setUsername(from.getLoginId()); + to.setMotdkeys(String.join(",", from.getAcknowledgedMotds())); + + return to; + } + + public Room migrate(final de.thm.arsnova.model.Room from, final Optional<UserProfile> owner) { + final Room to = new Room(); + copyCommonProperties(from, to); + to.setKeyword(from.getShortId()); + if (owner.isPresent()) { + to.setCreator(owner.get().getLoginId()); + } + to.setName(from.getName()); + to.setShortName(from.getAbbreviation()); + to.setActive(!from.isClosed()); + if (from.getAuthor() != null) { + to.setPpAuthorName(from.getAuthor().getName()); + to.setPpAuthorMail(from.getAuthor().getMail()); + to.setPpUniversity(from.getAuthor().getOrganizationName()); + to.setPpFaculty(from.getAuthor().getOrganizationUnit()); + to.setPpLogo(from.getAuthor().getOrganizationLogo()); + } + to.setFeatures(migrate(from.getSettings())); + + return to; + } + + public Room migrate(final de.thm.arsnova.model.Room from) { + return migrate(from, Optional.empty()); + } + + public RoomFeature migrate(final de.thm.arsnova.model.Room.Settings settings) { + RoomFeature feature = new RoomFeature(); + + /* Features */ + feature.setInterposed(settings.isCommentsEnabled()); + feature.setLecture(settings.isQuestionsEnabled()); + feature.setJitt(settings.isQuestionsEnabled()); + feature.setSlides(settings.isSlidesEnabled()); + feature.setFlashcardFeature(settings.isFlashcardsEnabled()); + feature.setFeedback(settings.isQuickFeedbackEnabled()); + feature.setPi(settings.isMultipleRoundsEnabled() || settings.isTimerEnabled()); + feature.setLearningProgress(settings.isScoreEnabled()); + + /* Use cases */ + int count = 0; + /* Single-feature use cases can be migrated */ + if (settings.isCommentsEnabled()) { + feature.setInterposedFeedback(true); + count++; + } + if (settings.isFlashcardsEnabled()) { + feature.setFlashcard(true); + count++; + } + if (settings.isQuickFeedbackEnabled()) { + feature.setLiveFeedback(true); + count++; + } + if (settings.isQuickSurveyEnabled()) { + feature.setLiveClicker(true); + count++; + } + /* For the following features an exact migration is not possible, so custom is set */ + if (settings.isQuestionsEnabled()) { + feature.setCustom(true); + count++; + } + if (settings.isSlidesEnabled()) { + feature.setCustom(true); + count++; + } + if (settings.isMultipleRoundsEnabled() || settings.isTimerEnabled()) { + feature.setCustom(true); + count++; + } + if (settings.isScoreEnabled()) { + feature.setCustom(true); + count++; + } + + if (count != 1) { + /* Reset single-feature use-cases since multiple features were detected */ + feature.setInterposedFeedback(false); + feature.setFlashcard(false); + feature.setLiveFeedback(false); + feature.setLiveClicker(false); + + if (count == 7) { + feature.setCustom(false); + feature.setTotal(true); + } else { + feature.setCustom(true); + } + } + + return feature; + } + + public RoomInfo migrateStats(final de.thm.arsnova.model.Room from) { + RoomInfo to = new RoomInfo(migrate(from)); + RoomStatistics stats = from.getStatistics(); + to.setNumQuestions(stats.getContentCount()); + to.setNumUnanswered(stats.getUnansweredContentCount()); + to.setNumAnswers(stats.getAnswerCount()); + to.setNumInterposed(stats.getCommentCount()); + to.setNumUnredInterposed(stats.getUnreadCommentCount()); + + return to; + } + + public Content migrate(final de.thm.arsnova.model.Content from) { + final Content to = new Content(); + copyCommonProperties(from, to); + to.setSessionId(from.getRoomId()); + to.setSubject(from.getSubject()); + to.setText(from.getBody()); + to.setAbstention(from.isAbstentionsAllowed()); + + if (from instanceof ChoiceQuestionContent) { + final ChoiceQuestionContent fromChoiceQuestionContent = (ChoiceQuestionContent) from; + switch (from.getFormat()) { + case CHOICE: + to.setQuestionType(fromChoiceQuestionContent.isMultiple() ? V2_TYPE_MC : V2_TYPE_ABCD); + break; + case BINARY: + to.setQuestionType(V2_TYPE_YESNO); + break; + case SCALE: + final int optionCount = fromChoiceQuestionContent.getOptions().size(); + /* The number of options for vote/school format is hard-coded by the legacy client */ + if (optionCount == 5) { + to.setQuestionType(V2_TYPE_VOTE); + } else if (optionCount == 6) { + to.setQuestionType(V2_TYPE_SCHOOL); + } else { + to.setQuestionType(V2_TYPE_ABCD); + } + break; + case GRID: + to.setQuestionType(V2_TYPE_GRID); + break; + default: + throw new IllegalArgumentException("Unsupported content format."); + } + final List<AnswerOption> toOptions = new ArrayList<>(); + to.setPossibleAnswers(toOptions); + for (int i = 0; i < fromChoiceQuestionContent.getOptions().size(); i++) { + AnswerOption option = new AnswerOption(); + option.setText(fromChoiceQuestionContent.getOptions().get(i).getLabel()); + option.setValue(fromChoiceQuestionContent.getOptions().get(i).getPoints()); + option.setCorrect(fromChoiceQuestionContent.getCorrectOptionIndexes().contains(i)); + toOptions.add(option); + } + } else { + switch (from.getFormat()) { + case NUMBER: + to.setQuestionType(V2_TYPE_FREETEXT); + break; + case TEXT: + to.setQuestionType(V2_TYPE_FREETEXT); + break; + default: + throw new IllegalArgumentException("Unsupported content format."); + } + } + de.thm.arsnova.model.Content.State state = from.getState(); + to.setPiRound(state.getRound()); + to.setActive(state.isVisible()); + to.setShowStatistic(state.isResponsesVisible()); + to.setShowAnswer(state.isSolutionVisible()); + to.setVotingDisabled(!state.isResponsesEnabled()); + if (from.getGroups().size() == 1) { + to.setQuestionVariant(from.getGroups().iterator().next()); + } + + return to; + } + + public Answer migrate(final de.thm.arsnova.model.ChoiceAnswer from, + final de.thm.arsnova.model.ChoiceQuestionContent content, final Optional<UserProfile> creator) { + final Answer to = new Answer(); + copyCommonProperties(from, to); + to.setQuestionId(from.getContentId()); + to.setSessionId(from.getRoomId()); + to.setPiRound(from.getRound()); + if (creator.isPresent()) { + to.setUser(creator.get().getLoginId()); + } + if (from.getSelectedChoiceIndexes().isEmpty()) { + to.setAbstention(true); + } else { + if (content.isMultiple()) { + to.setAnswerText(migrateChoice(from.getSelectedChoiceIndexes(), content.getOptions())); + } else { + int index = from.getSelectedChoiceIndexes().get(0); + to.setAnswerText(content.getOptions().get(index).getLabel()); + } + } + + return to; + } + + public String migrateChoice(final List<Integer> selectedChoiceIndexes, + final List<ChoiceQuestionContent.AnswerOption> options) { + List<String> answers = new ArrayList<>(); + for (int i = 0; i < options.size(); i++) { + answers.add(selectedChoiceIndexes.contains(i) ? "1" : "0"); + } + + return answers.stream().collect(Collectors.joining(",")); + } + + public Answer migrate(final de.thm.arsnova.model.ChoiceAnswer from, + final de.thm.arsnova.model.ChoiceQuestionContent content) { + return migrate(from, content, Optional.empty()); + } + + public Answer migrate(final de.thm.arsnova.model.TextAnswer from, + final Optional<de.thm.arsnova.model.Content> content, final Optional<UserProfile> creator) { + final Answer to = new Answer(); + copyCommonProperties(from, to); + to.setQuestionId(from.getContentId()); + to.setSessionId(from.getRoomId()); + to.setPiRound(from.getRound()); + if (creator.isPresent()) { + to.setUser(creator.get().getLoginId()); + } + + to.setAnswerSubject(from.getSubject()); + to.setAnswerText(from.getBody()); + + return to; + } + + public Answer migrate(final de.thm.arsnova.model.TextAnswer from) { + return migrate(from, Optional.empty(), Optional.empty()); + } + + public Comment migrate(final de.thm.arsnova.model.Comment from, final Optional<UserProfile> creator) { + final Comment to = new Comment(); + copyCommonProperties(from, to); + to.setSessionId(from.getRoomId()); + if (creator.isPresent()) { + to.setCreator(creator.get().getLoginId()); + } + to.setSubject(from.getSubject()); + to.setText(from.getBody()); + to.setTimestamp(from.getTimestamp().getTime()); + to.setRead(from.isRead()); + + return to; + } + + public Comment migrate(final de.thm.arsnova.model.Comment from) { + return migrate(from, Optional.empty()); + } + + public Motd migrate(final de.thm.arsnova.model.Motd from) { + final Motd to = new Motd(); + copyCommonProperties(from, to); + to.setMotdkey(from.getId()); + to.setStartdate(from.getCreationTimestamp()); + to.setStartdate(from.getStartDate()); + to.setEnddate(from.getEndDate()); + switch (from.getAudience()) { + case ALL: + to.setAudience("all"); + break; + case AUTHORS: + to.setAudience("tutors"); + break; + case PARTICIPANTS: + to.setAudience("students"); + break; + case ROOM: + to.setAudience("session"); + break; + } + to.setTitle(from.getTitle()); + to.setText(from.getBody()); + to.setSessionId(from.getRoomId()); + + return to; + } + + public List<Answer> migrate(final AnswerStatistics from, + final de.thm.arsnova.model.ChoiceQuestionContent content, int round) { + if (round < 1 || round > content.getState().getRound()) { + throw new IllegalArgumentException("Invalid value for round"); + } + final List<Answer> to = new ArrayList<>(); + final AnswerStatistics.RoundStatistics stats = from.getRoundStatistics().get(round - 1); + + if (content.isAbstentionsAllowed()) { + final Answer abstention = new Answer(); + abstention.setQuestionId(content.getId()); + abstention.setPiRound(round); + abstention.setAnswerCount(stats.getAbstentionCount()); + abstention.setAbstentionCount(stats.getAbstentionCount()); + to.add(abstention); + } + + Map<String, Integer> choices; + if (content.isMultiple()) { + /* Map selected choice indexes -> answer count */ + choices = stats.getCombinatedCounts().stream().collect(Collectors.toMap( + c -> migrateChoice(c.getSelectedChoiceIndexes(), content.getOptions()), + c -> c.getCount(), + (u, v) -> { throw new IllegalStateException(String.format("Duplicate key %s", u)); }, + LinkedHashMap::new)); + } else { + choices = new LinkedHashMap<>(); + int i = 0; + for (ChoiceQuestionContent.AnswerOption option : content.getOptions()) { + choices.put(option.getLabel(), stats.getIndependentCounts().get(i)); + i++; + } + } + + for (Map.Entry<String, Integer> choice : choices.entrySet()) { + Answer answer = new Answer(); + answer.setQuestionId(content.getId()); + answer.setPiRound(round); + answer.setAnswerCount(choice.getValue()); + answer.setAbstentionCount(stats.getAbstentionCount()); + answer.setAnswerText(choice.getKey()); + to.add(answer); + } + + return to; + } +} diff --git a/src/main/java/de/thm/arsnova/entities/Answer.java b/src/main/java/de/thm/arsnova/model/migration/v2/Answer.java similarity index 97% rename from src/main/java/de/thm/arsnova/entities/Answer.java rename to src/main/java/de/thm/arsnova/model/migration/v2/Answer.java index 50c9a795c3c74b5b388bcd07ac333f277a4e6b1f..681093b44697a66f2917966e979770b2701c3d75 100644 --- a/src/main/java/de/thm/arsnova/entities/Answer.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/Answer.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,22 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; -import java.io.Serializable; - /** * Both a regular (single choice, evaluation, etc.) as well as a freetext answer. * * This class has additional fields to transport generated answer statistics. */ -@ApiModel(value = "Answer", description = "the answer entity") +@ApiModel(value = "Answer", description = "Answer entity - Can represent a single answer or summarized statistics") public class Answer implements Entity { private String id; private String rev; diff --git a/src/main/java/de/thm/arsnova/entities/PossibleAnswer.java b/src/main/java/de/thm/arsnova/model/migration/v2/AnswerOption.java similarity index 84% rename from src/main/java/de/thm/arsnova/entities/PossibleAnswer.java rename to src/main/java/de/thm/arsnova/model/migration/v2/AnswerOption.java index 84e83b2d4c76544a476ea08fc50526aab56b614f..7e72b2734057ce958c3f122759e5d79963724ed4 100644 --- a/src/main/java/de/thm/arsnova/entities/PossibleAnswer.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/AnswerOption.java @@ -15,20 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import java.io.Serializable; /** - * This class represents an answer option of a question. + * Represents an Answer (Possible Answer) of Content. */ -@ApiModel(value = "session/answer", description = "the possible answer entity") -public class PossibleAnswer implements Serializable { +@ApiModel(value = "AnswerOption", description = "Answer Option (Possible Answer) entity") +public class AnswerOption implements Serializable { private String id; private String text; @@ -79,6 +79,6 @@ public class PossibleAnswer implements Serializable { @Override public String toString() { - return "PossibleAnswer [id=" + id + ", text=" + text + ", correct=" + correct + "]"; + return "AnswerOption [id=" + id + ", text=" + text + ", correct=" + correct + "]"; } } diff --git a/src/main/java/de/thm/arsnova/model/migration/v2/ClientAuthentication.java b/src/main/java/de/thm/arsnova/model/migration/v2/ClientAuthentication.java new file mode 100644 index 0000000000000000000000000000000000000000..b4c67c3d3532319333aff3485da06d1c9c5331a0 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/migration/v2/ClientAuthentication.java @@ -0,0 +1,130 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model.migration.v2; + +import com.fasterxml.jackson.annotation.JsonView; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.model.serialization.View; +import de.thm.arsnova.security.User; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Represents a user. + */ +public class ClientAuthentication implements Serializable { + public static final String ANONYMOUS = "anonymous"; + + private static final long serialVersionUID = 1L; + private String id; + private String username; + private UserProfile.AuthProvider authProvider; + private boolean isAdmin; + + public ClientAuthentication() { + username = ANONYMOUS; + authProvider = UserProfile.AuthProvider.NONE; + } + + public ClientAuthentication(User user) { + id = user.getId(); + username = user.getUsername(); + authProvider = user.getAuthProvider(); + isAdmin = user.isAdmin(); + } + + public ClientAuthentication(Authentication authentication) { + if (authentication instanceof AnonymousAuthenticationToken) { + setUsername(ClientAuthentication.ANONYMOUS); + } else { + if (!(authentication.getPrincipal() instanceof User)) { + throw new IllegalArgumentException("Unsupported authentication token"); + } + User user = (User) authentication.getPrincipal(); + id = user.getId(); + username = user.getUsername(); + authProvider = user.getAuthProvider(); + isAdmin = user.isAdmin(); + } + } + + public String getId() { + return id; + } + + public void setId(final String id) { + this.id = id; + } + + @JsonView(View.Public.class) + public String getUsername() { + return username; + } + + public void setUsername(final String username) { + this.username = username; + } + + @JsonView(View.Public.class) + public UserProfile.AuthProvider getAuthProvider() { + return authProvider; + } + + public void setAuthProvider(final UserProfile.AuthProvider authProvider) { + this.authProvider = authProvider; + } + + public void setAdmin(final boolean a) { + this.isAdmin = a; + } + + @JsonView(View.Public.class) + public boolean isAdmin() { + return this.isAdmin; + } + + @Override + public String toString() { + return "User [username=" + username + ", authProvider=" + authProvider + "]"; + } + + @Override + public int hashCode() { + // See http://stackoverflow.com/a/113600 + final int theAnswer = 42; + final int theOthers = 37; + + int result = theAnswer; + result = theOthers * result + this.username.hashCode(); + return theOthers * result + this.authProvider.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (obj == null || !obj.getClass().equals(this.getClass())) { + return false; + } + ClientAuthentication other = (ClientAuthentication) obj; + + return this.authProvider == other.authProvider && Objects.equals(this.id, other.id) && this.username.equals(other.username); + } + +} diff --git a/src/main/java/de/thm/arsnova/entities/Comment.java b/src/main/java/de/thm/arsnova/model/migration/v2/Comment.java similarity index 85% rename from src/main/java/de/thm/arsnova/entities/Comment.java rename to src/main/java/de/thm/arsnova/model/migration/v2/Comment.java index 4d69e84cd5d8c147f2009ed48b52c2d39d110b1c..de12c663051f087477a585b5769af5b46d8e0ca2 100644 --- a/src/main/java/de/thm/arsnova/entities/Comment.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/Comment.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,17 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; /** - * A question the user is asking the teacher. Also known as comment, feedback or audience question. + * A Comment (Interposed/Feedback/Audience question) from a attendee in a Room. */ -@ApiModel(value = "comment", description = "the comment entity") +@ApiModel(value = "Comment", description = "Comment (Interposed/Feedback/Audience Question) entity") public class Comment implements Entity { private String id; private String rev; @@ -42,6 +43,7 @@ public class Comment implements Entity { private String creator; @JsonView({View.Persistence.class, View.Public.class}) + @JsonProperty("_id") public String getId() { return id; } @@ -61,6 +63,13 @@ public class Comment implements Entity { return rev; } + /* Need because of an inconsistency in the v2 API */ + @JsonView(View.Public.class) + @JsonProperty("id") + public String getApiId() { + return id; + } + @ApiModelProperty(required = true, value = "is read") @JsonView({View.Persistence.class, View.Public.class}) public boolean isRead() { @@ -126,7 +135,7 @@ public class Comment implements Entity { this.creator = creator; } - public boolean isCreator(User user) { + public boolean isCreator(ClientAuthentication user) { return user.getUsername().equals(creator); } } diff --git a/src/main/java/de/thm/arsnova/entities/CommentReadingCount.java b/src/main/java/de/thm/arsnova/model/migration/v2/CommentReadingCount.java similarity index 86% rename from src/main/java/de/thm/arsnova/entities/CommentReadingCount.java rename to src/main/java/de/thm/arsnova/model/migration/v2/CommentReadingCount.java index 169b687aa9125292bda66b07949f4e1ecb28f22c..686a4149abf97b4eeed1cccc1056cf600f03b015 100644 --- a/src/main/java/de/thm/arsnova/entities/CommentReadingCount.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/CommentReadingCount.java @@ -15,17 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; /** - * Wrapper class for counting read and unread comments for a session or a single user. + * Wrapper class for counting read and unread Comments for a Room or a single user. */ -@ApiModel(value = "audiencequestion/readcount", description = "the comment reading count entity") +@ApiModel(value = "Comment Reading Count", description = "Comment Reading Count statistics entity") public class CommentReadingCount { private int read; diff --git a/src/main/java/de/thm/arsnova/entities/Content.java b/src/main/java/de/thm/arsnova/model/migration/v2/Content.java similarity index 96% rename from src/main/java/de/thm/arsnova/entities/Content.java rename to src/main/java/de/thm/arsnova/model/migration/v2/Content.java index 1bcb7943478d891f9c1034ce56c1c85776aac06a..f804e3259e4a20a4229b693bef825184c7eb85b3 100644 --- a/src/main/java/de/thm/arsnova/entities/Content.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/Content.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,20 +15,21 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import java.util.ArrayList; import java.util.Date; import java.util.List; /** - * A question the teacher is asking. + * Represents Content (Skill/Lecturer Question) in a Room. */ -@ApiModel(value = "content", description = "the content entity") +@ApiModel(value = "Content", description = "Content (Skill/Lecturer Question) entity") public class Content implements Entity { private String id; private String rev; @@ -38,7 +39,7 @@ public class Content implements Entity { private String text; private boolean active; private String releasedFor; - private List<PossibleAnswer> possibleAnswers; + private List<AnswerOption> possibleAnswers; private boolean noCorrect; // TODO: We currently need both sessionId and sessionKeyword, but sessionKeyword will not be persisted. private String sessionId; @@ -178,12 +179,12 @@ public class Content implements Entity { @ApiModelProperty(required = true, value = "list of possible answers") @JsonView({View.Persistence.class, View.Public.class}) - public final List<PossibleAnswer> getPossibleAnswers() { - return possibleAnswers; + public final List<AnswerOption> getPossibleAnswers() { + return possibleAnswers != null ? possibleAnswers : new ArrayList<>(); } @JsonView({View.Persistence.class, View.Public.class}) - public final void setPossibleAnswers(final List<PossibleAnswer> possibleAnswers) { + public final void setPossibleAnswers(final List<AnswerOption> possibleAnswers) { this.possibleAnswers = possibleAnswers; } @@ -216,11 +217,12 @@ public class Content implements Entity { sessionId = session; } - @ApiModelProperty(required = true, value = "the session keyword, the question is assigned to") + @ApiModelProperty(required = true, value = "the room keyword, the question is assigned to") public final String getSessionKeyword() { return sessionKeyword; } + //@JsonView(View.Public.class) public final void setSessionKeyword(final String keyword) { sessionKeyword = keyword; } @@ -847,7 +849,7 @@ public class Content implements Entity { private int calculateRegularValue(Answer answer) { String answerText = answer.getAnswerText(); - for (PossibleAnswer p : this.possibleAnswers) { + for (AnswerOption p : this.possibleAnswers) { if (answerText.equals(p.getText())) { return p.getValue(); } @@ -859,7 +861,7 @@ public class Content implements Entity { int value = 0; String[] answers = answer.getAnswerText().split(","); for (String a : answers) { - for (PossibleAnswer p : this.possibleAnswers) { + for (AnswerOption p : this.possibleAnswers) { if (a.equals(p.getText())) { value += p.getValue(); } @@ -873,7 +875,7 @@ public class Content implements Entity { String[] answers = answer.getAnswerText().split(","); for (int i = 0; i < this.possibleAnswers.size() && i < answers.length; i++) { if ("1".equals(answers[i])) { - PossibleAnswer p = this.possibleAnswers.get(i); + AnswerOption p = this.possibleAnswers.get(i); value += p.getValue(); } } diff --git a/src/main/java/de/thm/arsnova/entities/DbUser.java b/src/main/java/de/thm/arsnova/model/migration/v2/DbUser.java similarity index 97% rename from src/main/java/de/thm/arsnova/entities/DbUser.java rename to src/main/java/de/thm/arsnova/model/migration/v2/DbUser.java index 4c286712097256dd598ea24a5968bed1ae09862d..bcb6815e9df937d796e9201aeabdf9e8d8f8e484 100644 --- a/src/main/java/de/thm/arsnova/entities/DbUser.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/DbUser.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; /** * A user account for ARSnova's own registration and login process. diff --git a/src/main/java/de/thm/arsnova/entities/Entity.java b/src/main/java/de/thm/arsnova/model/migration/v2/Entity.java similarity index 87% rename from src/main/java/de/thm/arsnova/entities/Entity.java rename to src/main/java/de/thm/arsnova/model/migration/v2/Entity.java index 97b346ba406decd5d0462e4afa6c1cd2ee3caf54..c76a7c1dd8f557ce99db88035f756e97c2b5c942 100644 --- a/src/main/java/de/thm/arsnova/entities/Entity.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/Entity.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; public interface Entity { String getId(); diff --git a/src/main/java/de/thm/arsnova/entities/LogEntry.java b/src/main/java/de/thm/arsnova/model/migration/v2/LogEntry.java similarity index 86% rename from src/main/java/de/thm/arsnova/entities/LogEntry.java rename to src/main/java/de/thm/arsnova/model/migration/v2/LogEntry.java index eabb1fcea316105328c1b00d0d9a07c5a8c0b260..b3e8632d95ebe54ec952825b1256d7f7a0fbc61b 100644 --- a/src/main/java/de/thm/arsnova/entities/LogEntry.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/LogEntry.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,11 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import java.util.Map; @@ -57,13 +57,13 @@ public class LogEntry implements Entity { } @JsonView(View.Persistence.class) - public void setRevision(final String rev) { - this.rev = rev; + public String getRevision() { + return rev; } @JsonView(View.Persistence.class) - public String getRevision() { - return rev; + public void setRevision(final String rev) { + this.rev = rev; } @JsonView(View.Persistence.class) @@ -72,7 +72,7 @@ public class LogEntry implements Entity { } @JsonView(View.Persistence.class) - public void setTimestamp(long timestamp) { + public void setTimestamp(final long timestamp) { this.timestamp = timestamp; } @@ -82,7 +82,7 @@ public class LogEntry implements Entity { } @JsonView(View.Persistence.class) - public void setEvent(String event) { + public void setEvent(final String event) { this.event = event; } @@ -92,12 +92,12 @@ public class LogEntry implements Entity { } @JsonView(View.Persistence.class) - public void setLevel(int level) { + public void setLevel(final int level) { this.level = level; } @JsonView(View.Persistence.class) - public void setLevel(LogLevel level) { + public void setLevel(final LogLevel level) { this.level = level.ordinal(); } @@ -107,7 +107,7 @@ public class LogEntry implements Entity { } @JsonView(View.Persistence.class) - public void setPayload(Map<String, Object> payload) { + public void setPayload(final Map<String, Object> payload) { this.payload = payload; } } diff --git a/src/main/java/de/thm/arsnova/entities/LoggedIn.java b/src/main/java/de/thm/arsnova/model/migration/v2/LoggedIn.java similarity index 81% rename from src/main/java/de/thm/arsnova/entities/LoggedIn.java rename to src/main/java/de/thm/arsnova/model/migration/v2/LoggedIn.java index 275196fabf9682064f47c3eaff68b8942aad7211..98b87059270a044012570aa05d75f937d4c3b78f 100644 --- a/src/main/java/de/thm/arsnova/entities/LoggedIn.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/LoggedIn.java @@ -15,10 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; +import io.swagger.annotations.ApiModel; import java.util.ArrayList; import java.util.List; @@ -26,26 +27,27 @@ import java.util.List; /** * Once a user joins a session, this class is used to identify a returning user. */ +@ApiModel(value = "LoggedIn", description = "Logged In entity - Contains the Room History (Visited Sessions)") public class LoggedIn implements Entity { private String id; private String rev; private String user; private String sessionId; private long timestamp; - private List<VisitedSession> visitedSessions = new ArrayList<>(); + private List<VisitedRoom> visitedSessions = new ArrayList<>(); public LoggedIn() { this.updateTimestamp(); } - public void addVisitedSession(Session s) { + public void addVisitedSession(Room s) { if (!isAlreadyVisited(s)) { - this.visitedSessions.add(new VisitedSession(s)); + this.visitedSessions.add(new VisitedRoom(s)); } } - private boolean isAlreadyVisited(Session s) { - for (VisitedSession vs : this.visitedSessions) { + private boolean isAlreadyVisited(Room s) { + for (VisitedRoom vs : this.visitedSessions) { if (vs.getId().equals(s.getId())) { return true; } @@ -108,12 +110,12 @@ public class LoggedIn implements Entity { } @JsonView(View.Persistence.class) - public List<VisitedSession> getVisitedSessions() { + public List<VisitedRoom> getVisitedSessions() { return visitedSessions; } @JsonView(View.Persistence.class) - public void setVisitedSessions(List<VisitedSession> visitedSessions) { + public void setVisitedSessions(List<VisitedRoom> visitedSessions) { this.visitedSessions = visitedSessions; } diff --git a/src/main/java/de/thm/arsnova/entities/Motd.java b/src/main/java/de/thm/arsnova/model/migration/v2/Motd.java similarity index 89% rename from src/main/java/de/thm/arsnova/entities/Motd.java rename to src/main/java/de/thm/arsnova/model/migration/v2/Motd.java index 7f19e477e8a9083f5b54b3b83a3d3d1f062210b5..440dbc9a1c26bc4d313db53e91bab004f82a0e56 100644 --- a/src/main/java/de/thm/arsnova/entities/Motd.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/Motd.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import java.util.Date; /** - * This class represents a message of the day. + * Represents a Message of the Day. */ -@ApiModel(value = "motd", description = "the message of the day entity") +@ApiModel(value = "Motd", description = "Message of the Day entity") public class Motd implements Entity { private String motdkey; //ID @@ -81,8 +81,8 @@ public class Motd implements Entity { } @JsonView({View.Persistence.class, View.Public.class}) - public void setTitle(final String ttitle) { - title = ttitle; + public void setTitle(final String title) { + this.title = title; } @ApiModelProperty(required = true, value = "text of the message") @@ -103,7 +103,7 @@ public class Motd implements Entity { } @JsonView({View.Persistence.class, View.Public.class}) - public void setAudience(String a) { + public void setAudience(final String a) { audience = a; } @@ -113,7 +113,7 @@ public class Motd implements Entity { } @JsonView({View.Persistence.class, View.Public.class}) - public void setSessionId(String sessionId) { + public void setSessionId(final String sessionId) { this.sessionId = sessionId; } @@ -124,7 +124,7 @@ public class Motd implements Entity { } @JsonView({View.Persistence.class, View.Public.class}) - public void setSessionkey(String a) { + public void setSessionkey(final String a) { sessionkey = a; } @@ -159,7 +159,7 @@ public class Motd implements Entity { } @Override - public boolean equals(Object obj) { + public boolean equals(final Object obj) { if (obj == null || !obj.getClass().equals(this.getClass())) { return false; } diff --git a/src/main/java/de/thm/arsnova/entities/MotdList.java b/src/main/java/de/thm/arsnova/model/migration/v2/MotdList.java similarity index 89% rename from src/main/java/de/thm/arsnova/entities/MotdList.java rename to src/main/java/de/thm/arsnova/model/migration/v2/MotdList.java index 3e346358a40845ab9380c22dc5aa53b0c0e6cbef..8411386c57b89137a027d1dca22cebf07114fc21 100644 --- a/src/main/java/de/thm/arsnova/entities/MotdList.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/MotdList.java @@ -15,17 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; /** - * This class represents a list of motdkeys for a user. + * Contains a list of MotD IDs a user has acknowledged. */ -@ApiModel(value = "motdlist", description = "the motdlist to save the messages a user has confirmed to be read") +@ApiModel(value = "MotdList", description = "Motd List entity - Contains IDs of MotDs a user has acknowledged") public class MotdList implements Entity { private String id; private String rev; diff --git a/src/main/java/de/thm/arsnova/entities/Session.java b/src/main/java/de/thm/arsnova/model/migration/v2/Room.java similarity index 91% rename from src/main/java/de/thm/arsnova/entities/Session.java rename to src/main/java/de/thm/arsnova/model/migration/v2/Room.java index ee09f44be3fc739044edc0e38341fe5a227a7c9c..566bc5a96f7670ff7a402c89c98e05a8cb13b195 100644 --- a/src/main/java/de/thm/arsnova/entities/Session.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/Room.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,19 +15,20 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.ScoreOptions; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; /** - * Represents an ARSnova session. + * Represents a Room (Session). */ -@ApiModel(value = "session", description = "the session entity") -public class Session implements Entity { +@ApiModel(value = "Room", description = "Room (Session) entity") +public class Room implements Entity { private String id; private String rev; private String name; @@ -40,7 +41,7 @@ public class Session implements Entity { private String courseId; private long creationTime; private ScoreOptions learningProgressOptions = new ScoreOptions(); - private SessionFeature features = new SessionFeature(); + private RoomFeature features = new RoomFeature(); private String ppAuthorName; private String ppAuthorMail; @@ -141,7 +142,7 @@ public class Session implements Entity { this.lastOwnerActivity = lastOwnerActivity; } - public boolean isCreator(final User user) { + public boolean isCreator(final ClientAuthentication user) { return user.getUsername().equals(creator); } @@ -196,12 +197,12 @@ public class Session implements Entity { @ApiModelProperty(required = true, value = "the enabled features (e.g. feedback, interposed, learning Progress, lecture)") @JsonView({View.Persistence.class, View.Public.class}) - public SessionFeature getFeatures() { + public RoomFeature getFeatures() { return features; } @JsonView({View.Persistence.class, View.Public.class}) - public void setFeatures(SessionFeature features) { + public void setFeatures(RoomFeature features) { this.features = features; } @@ -337,9 +338,17 @@ public class Session implements Entity { this.flipFlashcards = flip; } + public boolean hasAuthorDetails() { + return ppAuthorName != null && !ppAuthorName.isEmpty() + || ppAuthorMail != null && !ppAuthorMail.isEmpty() + || ppUniversity != null && !ppUniversity.isEmpty() + || ppFaculty != null && !ppFaculty.isEmpty() + || ppLogo != null && !ppLogo.isEmpty(); + } + @Override public String toString() { - return "Session [keyword=" + keyword + ", type=" + getType() + ", creator=" + creator + "]"; + return "Room [keyword=" + keyword + ", type=" + getType() + ", creator=" + creator + "]"; } @Override @@ -356,7 +365,7 @@ public class Session implements Entity { if (obj == null || !obj.getClass().equals(this.getClass())) { return false; } - Session other = (Session) obj; + Room other = (Room) obj; return this.keyword.equals(other.keyword); } diff --git a/src/main/java/de/thm/arsnova/entities/SessionFeature.java b/src/main/java/de/thm/arsnova/model/migration/v2/RoomFeature.java similarity index 90% rename from src/main/java/de/thm/arsnova/entities/SessionFeature.java rename to src/main/java/de/thm/arsnova/model/migration/v2/RoomFeature.java index acecd49fab34a258d7ff9f46057b88e9ea1ba6d4..f8635ac89b339538914c42ad359c06f0d8b3e047 100644 --- a/src/main/java/de/thm/arsnova/entities/SessionFeature.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/RoomFeature.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import java.io.Serializable; /** - * Contains fields that describe which specific feature is activated for a session. + * Contains fields that describe which specific Feature is activated for a Room. */ -@ApiModel(value = "session feature", description = "the session feature entity") -public class SessionFeature implements Serializable { +@ApiModel(value = "RoomFeature", description = "Room (Session) Feature entity - Represents feature/use case settings of a Room") +public class RoomFeature implements Serializable { - private boolean custom = true; + private boolean custom = false; private boolean clicker = false; private boolean peerGrading = false; private boolean twitterWall = false; @@ -40,16 +40,16 @@ public class SessionFeature implements Serializable { private boolean flashcard = false; private boolean total = false; - private boolean jitt = true; - private boolean lecture = true; - private boolean feedback = true; - private boolean interposed = true; - private boolean pi = true; - private boolean learningProgress = true; - private boolean flashcardFeature = true; + private boolean jitt = false; + private boolean lecture = false; + private boolean feedback = false; + private boolean interposed = false; + private boolean pi = false; + private boolean learningProgress = false; + private boolean flashcardFeature = false; private boolean slides = false; - public SessionFeature(SessionFeature features) { + public RoomFeature(RoomFeature features) { this(); if (features != null) { this.custom = features.custom; @@ -72,7 +72,7 @@ public class SessionFeature implements Serializable { } } - public SessionFeature() { } + public RoomFeature() { } @JsonView({View.Persistence.class, View.Public.class}) public boolean isLecture() { diff --git a/src/main/java/de/thm/arsnova/entities/SessionInfo.java b/src/main/java/de/thm/arsnova/model/migration/v2/RoomInfo.java similarity index 84% rename from src/main/java/de/thm/arsnova/entities/SessionInfo.java rename to src/main/java/de/thm/arsnova/model/migration/v2/RoomInfo.java index 0f7c155fe029fb3981c838bd39ece4b25ae5c0bd..599115d89bf6ec41755a58bcda58f4156e151b0e 100644 --- a/src/main/java/de/thm/arsnova/entities/SessionInfo.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/RoomInfo.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -26,11 +26,11 @@ import java.util.ArrayList; import java.util.List; /** - * Summary information of a specific session. For example, this is used to display list entries of 'my sessions' as well - * as 'my visited sessions'. + * Summary information of a specific Room. For example, this is used to display list entries of a user's Rooms as well + * as a user's Room History (Visited Rooms). */ -@ApiModel(value = "session/import", description = "the session info entity") -public class SessionInfo { +@ApiModel(value = "RoomInfo", description = "Room (Session) Info entity") +public class RoomInfo { private String name; private String shortName; @@ -48,24 +48,24 @@ public class SessionInfo { private int numUnreadComments; private int numUnanswered; - public SessionInfo(Session session) { - this.name = session.getName(); - this.shortName = session.getShortName(); - this.keyword = session.getKeyword(); - this.active = session.isActive(); - this.courseType = session.getCourseType(); - this.creationTime = session.getCreationTime(); - this.sessionType = session.getSessionType(); - this.ppLevel = session.getPpLevel(); - this.ppSubject = session.getPpSubject(); + public RoomInfo(Room room) { + this.name = room.getName(); + this.shortName = room.getShortName(); + this.keyword = room.getKeyword(); + this.active = room.isActive(); + this.courseType = room.getCourseType(); + this.creationTime = room.getCreationTime(); + this.sessionType = room.getSessionType(); + this.ppLevel = room.getPpLevel(); + this.ppSubject = room.getPpSubject(); } - public SessionInfo() { } + public RoomInfo() { } - public static List<SessionInfo> fromSessionList(List<Session> sessions) { - List<SessionInfo> infos = new ArrayList<>(); - for (Session s : sessions) { - infos.add(new SessionInfo(s)); + public static List<RoomInfo> fromSessionList(List<Room> sessions) { + List<RoomInfo> infos = new ArrayList<>(); + for (Room s : sessions) { + infos.add(new RoomInfo(s)); } return infos; } @@ -235,7 +235,7 @@ public class SessionInfo { if (getClass() != obj.getClass()) { return false; } - SessionInfo other = (SessionInfo) obj; + RoomInfo other = (RoomInfo) obj; if (keyword == null) { if (other.keyword != null) { return false; diff --git a/src/main/java/de/thm/arsnova/entities/VisitedSession.java b/src/main/java/de/thm/arsnova/model/migration/v2/VisitedRoom.java similarity index 75% rename from src/main/java/de/thm/arsnova/entities/VisitedSession.java rename to src/main/java/de/thm/arsnova/model/migration/v2/VisitedRoom.java index 26bd030bda214dd754b21869539ae19a2a6cde4c..ffa741467165b1e5f5b3bcea06bb2c57a126046d 100644 --- a/src/main/java/de/thm/arsnova/entities/VisitedSession.java +++ b/src/main/java/de/thm/arsnova/model/migration/v2/VisitedRoom.java @@ -15,23 +15,27 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model.migration.v2; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; +import io.swagger.annotations.ApiModel; /** - * A session a user has visited previously. + * A Room (Session) a user has visited previously. */ -public class VisitedSession { +@ApiModel(value = "VisitedRoom", description = "Visited Room (Session) entity - An entry of the Room History for the Logged In entity") +public class VisitedRoom { + @JsonProperty("_id") private String id; private String name; private String keyword; - public VisitedSession() { + public VisitedRoom() { } - public VisitedSession(Session s) { + public VisitedRoom(Room s) { this.id = s.getId(); this.name = s.getName(); this.keyword = s.getKeyword(); @@ -69,7 +73,7 @@ public class VisitedSession { @Override public String toString() { - return "VisitedSession [id=" + id + ", name=" + name + ", keyword=" + return "VisitedRoom [id=" + id + ", name=" + name + ", keyword=" + keyword + "]"; } } diff --git a/src/main/java/de/thm/arsnova/entities/package-info.java b/src/main/java/de/thm/arsnova/model/package-info.java similarity index 63% rename from src/main/java/de/thm/arsnova/entities/package-info.java rename to src/main/java/de/thm/arsnova/model/package-info.java index ed8a4656b1050c9d4e0df681eb85c80d97d4a64e..91ada1f83648ab601e38cb94ddc8115d4de66b33 100644 --- a/src/main/java/de/thm/arsnova/entities/package-info.java +++ b/src/main/java/de/thm/arsnova/model/package-info.java @@ -1,4 +1,4 @@ /** * Classes to translate objects to and from JSON */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; diff --git a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentMixIn.java b/src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentMixIn.java similarity index 94% rename from src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentMixIn.java rename to src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentMixIn.java index cf0c862f792e6133606a89e02a4108d555794860..74a16826ea9aeb38608e45f908bfc6f340d090e1 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentMixIn.java +++ b/src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentMixIn.java @@ -15,13 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.serialization; +package de.thm.arsnova.model.serialization; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import de.thm.arsnova.entities.Entity; +import de.thm.arsnova.model.Entity; @JsonIgnoreProperties(value = {"type"}, allowGetters = true) public abstract class CouchDbDocumentMixIn { diff --git a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentModule.java b/src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentModule.java similarity index 85% rename from src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentModule.java rename to src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentModule.java index 180cb56da61a5d7432b7851face096a808f13262..a297aa28a297bd7cf75a615538fc45b825ba3078 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbDocumentModule.java +++ b/src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentModule.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.serialization; +package de.thm.arsnova.model.serialization; import com.fasterxml.jackson.databind.module.SimpleModule; -import de.thm.arsnova.entities.Entity; +import de.thm.arsnova.model.Entity; public class CouchDbDocumentModule extends SimpleModule { public CouchDbDocumentModule() { @@ -28,5 +28,6 @@ public class CouchDbDocumentModule extends SimpleModule { @Override public void setupModule(SetupContext context) { context.setMixInAnnotations(Entity.class, CouchDbDocumentMixIn.class); + context.setMixInAnnotations(de.thm.arsnova.model.migration.v2.Entity.class, CouchDbDocumentV2MixIn.class); } } diff --git a/src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentV2MixIn.java b/src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentV2MixIn.java new file mode 100644 index 0000000000000000000000000000000000000000..4e06a77e6f3aaa851a5038370f82787cdd879375 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/serialization/CouchDbDocumentV2MixIn.java @@ -0,0 +1,42 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2017 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model.serialization; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import de.thm.arsnova.model.Entity; + +@JsonIgnoreProperties(value = {"type"}, allowGetters = true) +public abstract class CouchDbDocumentV2MixIn { + @JsonProperty("_id") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + abstract String getId(); + + @JsonProperty("_id") abstract void setId(String id); + + @JsonProperty("_rev") + @JsonInclude(JsonInclude.Include.NON_EMPTY) + abstract String getRevision(); + + @JsonProperty("_rev") abstract String setRevision(String rev); + + @JsonSerialize(converter = CouchDbTypeFieldV2Converter.class) + abstract Class<? extends Entity> getType(); +} diff --git a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbObjectMapperFactory.java b/src/main/java/de/thm/arsnova/model/serialization/CouchDbObjectMapperFactory.java similarity index 96% rename from src/main/java/de/thm/arsnova/entities/serialization/CouchDbObjectMapperFactory.java rename to src/main/java/de/thm/arsnova/model/serialization/CouchDbObjectMapperFactory.java index 6fbbc63596e3d21d51e7649d809bee9b0e3362a2..748b65c6a8350c3572a51d7ffd938d9a453eb7ac 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbObjectMapperFactory.java +++ b/src/main/java/de/thm/arsnova/model/serialization/CouchDbObjectMapperFactory.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.serialization; +package de.thm.arsnova.model.serialization; import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/de/thm/arsnova/model/serialization/CouchDbTypeFieldConverter.java b/src/main/java/de/thm/arsnova/model/serialization/CouchDbTypeFieldConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..9918dfc7200a8ea6b3e8e0c29494ce30d5693826 --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/serialization/CouchDbTypeFieldConverter.java @@ -0,0 +1,41 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model.serialization; + +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; +import de.thm.arsnova.model.Entity; + +public class CouchDbTypeFieldConverter implements Converter<Class<? extends Entity>, String> { + + @Override + public String convert(Class<? extends Entity> aClass) { + return aClass.getSimpleName(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructGeneralizedType(typeFactory.constructType(Class.class), Entity.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } +} diff --git a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java b/src/main/java/de/thm/arsnova/model/serialization/CouchDbTypeFieldV2Converter.java similarity index 72% rename from src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java rename to src/main/java/de/thm/arsnova/model/serialization/CouchDbTypeFieldV2Converter.java index 8489552295116e32d911828293c96d2d8d777dc5..41ee2c09954dde67c44dc06720892f4fc268cdcc 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/CouchDbTypeFieldConverter.java +++ b/src/main/java/de/thm/arsnova/model/serialization/CouchDbTypeFieldV2Converter.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2017 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -15,25 +15,25 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.serialization; +package de.thm.arsnova.model.serialization; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.Converter; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.DbUser; -import de.thm.arsnova.entities.Entity; -import de.thm.arsnova.entities.LogEntry; -import de.thm.arsnova.entities.Motd; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.MotdList; -import de.thm.arsnova.entities.Session; +import de.thm.arsnova.model.migration.v2.Answer; +import de.thm.arsnova.model.migration.v2.Comment; +import de.thm.arsnova.model.migration.v2.Content; +import de.thm.arsnova.model.migration.v2.DbUser; +import de.thm.arsnova.model.migration.v2.Entity; +import de.thm.arsnova.model.migration.v2.LogEntry; +import de.thm.arsnova.model.migration.v2.Motd; +import de.thm.arsnova.model.migration.v2.MotdList; +import de.thm.arsnova.model.migration.v2.Room; import java.util.HashMap; import java.util.Map; -public class CouchDbTypeFieldConverter implements Converter<Class<? extends Entity>, String> { +public class CouchDbTypeFieldV2Converter implements Converter<Class<? extends Entity>, String> { private static final Map<Class<? extends Entity>, String> typeMapping = new HashMap<>(); { @@ -41,7 +41,7 @@ public class CouchDbTypeFieldConverter implements Converter<Class<? extends Enti typeMapping.put(DbUser.class, "userdetails"); typeMapping.put(Motd.class, "motd"); typeMapping.put(MotdList.class, "motdlist"); - typeMapping.put(Session.class, "session"); + typeMapping.put(Room.class, "session"); typeMapping.put(Comment.class, "interposed_question"); typeMapping.put(Content.class, "skill_question"); typeMapping.put(Answer.class, "skill_question_answer"); diff --git a/src/main/java/de/thm/arsnova/model/serialization/FormatAnswerTypeIdResolver.java b/src/main/java/de/thm/arsnova/model/serialization/FormatAnswerTypeIdResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..c3d48485a50370ef8ee9789f07b0f506fbd6fd0a --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/serialization/FormatAnswerTypeIdResolver.java @@ -0,0 +1,53 @@ +package de.thm.arsnova.model.serialization; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +import com.fasterxml.jackson.databind.type.TypeFactory; +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.ChoiceAnswer; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.TextAnswer; + +import java.io.IOException; + +public class FormatAnswerTypeIdResolver extends TypeIdResolverBase { + @Override + public String idFromValue(final Object value) { + if (value instanceof Answer) { + return ((Answer) value).getFormat().toString(); + } else { + throw new IllegalArgumentException("Unsupported type."); + } + } + + @Override + public String idFromValueAndType(final Object value, final Class<?> suggestedType) { + return idFromValue(value); + } + + @Override + public JavaType typeFromId(final DatabindContext context, final String id) throws IOException { + Content.Format format = Content.Format.valueOf(id); + switch (format) { + case BINARY: + return TypeFactory.defaultInstance().constructType(ChoiceAnswer.class); + case CHOICE: + return TypeFactory.defaultInstance().constructType(ChoiceAnswer.class); + case NUMBER: + return TypeFactory.defaultInstance().constructType(ChoiceAnswer.class); + case SCALE: + return TypeFactory.defaultInstance().constructType(ChoiceAnswer.class); + case TEXT: + return TypeFactory.defaultInstance().constructType(TextAnswer.class); + default: + throw new IllegalArgumentException("Unsupported type ID."); + } + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } +} diff --git a/src/main/java/de/thm/arsnova/model/serialization/FormatContentTypeIdResolver.java b/src/main/java/de/thm/arsnova/model/serialization/FormatContentTypeIdResolver.java new file mode 100644 index 0000000000000000000000000000000000000000..9936b8e97aa0d72cbad2a4bbdd57a589685e7c6b --- /dev/null +++ b/src/main/java/de/thm/arsnova/model/serialization/FormatContentTypeIdResolver.java @@ -0,0 +1,51 @@ +package de.thm.arsnova.model.serialization; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.DatabindContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.jsontype.impl.TypeIdResolverBase; +import com.fasterxml.jackson.databind.type.TypeFactory; +import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.Content; + +import java.io.IOException; + +public class FormatContentTypeIdResolver extends TypeIdResolverBase { + @Override + public String idFromValue(final Object value) { + if (value instanceof Content) { + return ((Content) value).getFormat().toString(); + } else { + throw new IllegalArgumentException("Unsupported type."); + } + } + + @Override + public String idFromValueAndType(final Object value, final Class<?> suggestedType) { + return idFromValue(value); + } + + @Override + public JavaType typeFromId(final DatabindContext context, final String id) throws IOException { + Content.Format format = Content.Format.valueOf(id); + switch (format) { + case BINARY: + return TypeFactory.defaultInstance().constructType(ChoiceQuestionContent.class); + case CHOICE: + return TypeFactory.defaultInstance().constructType(ChoiceQuestionContent.class); + case NUMBER: + return TypeFactory.defaultInstance().constructType(ChoiceQuestionContent.class); + case SCALE: + return TypeFactory.defaultInstance().constructType(ChoiceQuestionContent.class); + case TEXT: + return TypeFactory.defaultInstance().constructType(Content.class); + default: + throw new IllegalArgumentException("Unsupported type ID."); + } + } + + @Override + public JsonTypeInfo.Id getMechanism() { + return JsonTypeInfo.Id.CUSTOM; + } +} diff --git a/src/main/java/de/thm/arsnova/entities/serialization/View.java b/src/main/java/de/thm/arsnova/model/serialization/View.java similarity index 94% rename from src/main/java/de/thm/arsnova/entities/serialization/View.java rename to src/main/java/de/thm/arsnova/model/serialization/View.java index e57b49f864a45bb66a5349c512e72202cdc9b376..8a68295783a1d0565069f79271c4f7a9990cad30 100644 --- a/src/main/java/de/thm/arsnova/entities/serialization/View.java +++ b/src/main/java/de/thm/arsnova/model/serialization/View.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.serialization; +package de.thm.arsnova.model.serialization; public class View { public interface Public {} diff --git a/src/main/java/de/thm/arsnova/entities/transport/AnswerQueueElement.java b/src/main/java/de/thm/arsnova/model/transport/AnswerQueueElement.java similarity index 72% rename from src/main/java/de/thm/arsnova/entities/transport/AnswerQueueElement.java rename to src/main/java/de/thm/arsnova/model/transport/AnswerQueueElement.java index 6e0346f8f4cc13ca9cd5f036869af1d1cdd3821f..b199acb6eec2c03bf635d7e2fc74e3e6eed67dbd 100644 --- a/src/main/java/de/thm/arsnova/entities/transport/AnswerQueueElement.java +++ b/src/main/java/de/thm/arsnova/model/transport/AnswerQueueElement.java @@ -15,12 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.transport; +package de.thm.arsnova.model.transport; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; /** * An answer that is about to get saved in the database. Answers are not saved immediately, they are instead stored @@ -28,23 +28,23 @@ import de.thm.arsnova.entities.User; */ public class AnswerQueueElement { - private final Session session; + private final Room room; private final Content content; private final Answer answer; - private final User user; + private final ClientAuthentication user; - public AnswerQueueElement(Session session, Content content, Answer answer, User user) { - this.session = session; + public AnswerQueueElement(Room room, Content content, Answer answer, ClientAuthentication user) { + this.room = room; this.content = content; this.answer = answer; this.user = user; } - public Session getSession() { - return session; + public Room getRoom() { + return room; } public Content getQuestion() { @@ -55,7 +55,7 @@ public class AnswerQueueElement { return answer; } - public User getUser() { + public ClientAuthentication getUser() { return user; } } diff --git a/src/main/java/de/thm/arsnova/entities/transport/ImportExportSession.java b/src/main/java/de/thm/arsnova/model/transport/ImportExportContainer.java similarity index 89% rename from src/main/java/de/thm/arsnova/entities/transport/ImportExportSession.java rename to src/main/java/de/thm/arsnova/model/transport/ImportExportContainer.java index fed5f051fcb238fa706fcd301205441a6a46d9c8..ac2814acb1dc3cbe87d1bad8774f6381c77e0609 100644 --- a/src/main/java/de/thm/arsnova/entities/transport/ImportExportSession.java +++ b/src/main/java/de/thm/arsnova/model/transport/ImportExportContainer.java @@ -15,18 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.transport; +package de.thm.arsnova.model.transport; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Motd; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.SessionFeature; -import de.thm.arsnova.entities.SessionInfo; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.migration.v2.Answer; +import de.thm.arsnova.model.migration.v2.Comment; +import de.thm.arsnova.model.migration.v2.Content; +import de.thm.arsnova.model.migration.v2.Motd; +import de.thm.arsnova.model.migration.v2.Room; +import de.thm.arsnova.model.migration.v2.RoomFeature; +import de.thm.arsnova.model.migration.v2.RoomInfo; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; @@ -38,9 +38,9 @@ import java.util.List; * This class is used to allow the import and export of a session. */ @ApiModel(value = "session/import and export", description = "the import export session API") -public class ImportExportSession { +public class ImportExportContainer { - private ImportExportSesssion session; + private ImportExportRoom session; private List<ImportExportContent> questions; @@ -48,11 +48,11 @@ public class ImportExportSession { private List<Motd> motds; - private SessionFeature sessionFeature = new SessionFeature(); + private RoomFeature sessionFeature = new RoomFeature(); - private SessionInfo sessionInfo; + private RoomInfo sessionInfo; - public ImportExportSession() { + public ImportExportContainer() { questions = new ArrayList<>(); feedbackQuestions = new ArrayList<>(); motds = new ArrayList<>(); @@ -61,11 +61,11 @@ public class ImportExportSession { @ApiModelProperty(required = true, value = "used to display session") @JsonView(View.Public.class) - public ImportExportSesssion getSession() { + public ImportExportRoom getSession() { return session; } - public void setSession(ImportExportSesssion session) { + public void setSession(ImportExportRoom session) { this.session = session; } @@ -99,25 +99,25 @@ public class ImportExportSession { } @JsonView(View.Public.class) - public SessionFeature getSessionFeature() { + public RoomFeature getSessionFeature() { return sessionFeature; } - public void setSessionFeature(SessionFeature sF) { + public void setSessionFeature(RoomFeature sF) { sessionFeature = sF; } @JsonView(View.Public.class) - public SessionInfo getSessionInfo() { + public RoomInfo getSessionInfo() { return sessionInfo; } - public void setSessionInfo(SessionInfo si) { + public void setSessionInfo(RoomInfo si) { sessionInfo = si; } - public void setSessionFromSessionObject(Session s) { - ImportExportSesssion iesession = new ImportExportSesssion(); + public void setSessionFromSessionObject(Room s) { + ImportExportRoom iesession = new ImportExportRoom(); iesession.setName(s.getName()); iesession.setShortName(s.getShortName()); iesession.setActive(s.isActive()); @@ -134,8 +134,8 @@ public class ImportExportSession { questions.add(ieq); } - public Session generateSessionEntity(User user) { - final Session s = new Session(); + public Room generateSessionEntity(ClientAuthentication user) { + final Room s = new Room(); // import fields s.setActive(session.isActive()); // overwrite name and shortname @@ -237,7 +237,7 @@ public class ImportExportSession { } } - public static class ImportExportSesssion { + public static class ImportExportRoom { private String name; @@ -249,7 +249,7 @@ public class ImportExportSession { private PublicPool publicPool; - private SessionFeature sessionFeature; + private RoomFeature sessionFeature; @ApiModelProperty(required = true, value = "used to display short name") @JsonView(View.Public.class) @@ -302,11 +302,11 @@ public class ImportExportSession { } @JsonView(View.Public.class) - public SessionFeature getSessionFeature() { + public RoomFeature getSessionFeature() { return this.sessionFeature; } - public void setSessionFeature(SessionFeature sF) { + public void setSessionFeature(RoomFeature sF) { this.sessionFeature = sF; } } @@ -335,7 +335,7 @@ public class ImportExportSession { private String shortName; - public void setPpFromSession(Session s) { + public void setPpFromSession(Room s) { ppAuthorName = s.getPpAuthorName(); ppAuthorMail = s.getPpAuthorMail(); ppUniversity = s.getPpUniversity(); diff --git a/src/main/java/de/thm/arsnova/entities/transport/ScoreStatistics.java b/src/main/java/de/thm/arsnova/model/transport/ScoreStatistics.java similarity index 94% rename from src/main/java/de/thm/arsnova/entities/transport/ScoreStatistics.java rename to src/main/java/de/thm/arsnova/model/transport/ScoreStatistics.java index 09c700ee94d175f0c216b089ffbafc08962c2b8e..0a09a2292f7d297d0397b51d7e152cdf1afd24ba 100644 --- a/src/main/java/de/thm/arsnova/entities/transport/ScoreStatistics.java +++ b/src/main/java/de/thm/arsnova/model/transport/ScoreStatistics.java @@ -15,17 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities.transport; +package de.thm.arsnova.model.transport; import com.fasterxml.jackson.annotation.JsonView; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; /** * The calculated score along with meta-data. */ -@ApiModel(value = "session/{sessionkey}/learningprogress", description = "the score API") +@ApiModel(value = "session/{shortId}/learningprogress", description = "the score API") public class ScoreStatistics { private int courseProgress; diff --git a/src/main/java/de/thm/arsnova/entities/transport/package-info.java b/src/main/java/de/thm/arsnova/model/transport/package-info.java similarity index 86% rename from src/main/java/de/thm/arsnova/entities/transport/package-info.java rename to src/main/java/de/thm/arsnova/model/transport/package-info.java index a6920b7c81f41a1c752a0ea3e72ab880ec59016c..39062caccb756f2cfa9c0f18c4f24ce242e1e4b0 100644 --- a/src/main/java/de/thm/arsnova/entities/transport/package-info.java +++ b/src/main/java/de/thm/arsnova/model/transport/package-info.java @@ -4,4 +4,4 @@ * Since sometimes only a subset of properties are needed, sending the whole entity is a waste of bandwith. Therefore, * entities optimized for data transport are located here. */ -package de.thm.arsnova.entities.transport; +package de.thm.arsnova.model.transport; diff --git a/src/main/java/de/thm/arsnova/persistance/CommentRepository.java b/src/main/java/de/thm/arsnova/persistance/CommentRepository.java deleted file mode 100644 index 6e97fb98a70399eebca6150a28dad0ac21a5289c..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/CommentRepository.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.thm.arsnova.persistance; - -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.CommentReadingCount; -import de.thm.arsnova.entities.User; -import org.springframework.data.repository.CrudRepository; - -import java.util.List; - -public interface CommentRepository extends CrudRepository<Comment, String> { - int countBySessionId(String sessionKey); - CommentReadingCount countReadingBySessionId(String sessionId); - CommentReadingCount countReadingBySessionIdAndUser(String sessionId, User user); - List<Comment> findBySessionId(String sessionId, int start, int limit); - List<Comment> findBySessionIdAndUser(String sessionId, User user, int start, int limit); - Comment findOne(String commentId); - int deleteBySessionId(String sessionId); - int deleteBySessionIdAndUser(String sessionId, User user); -} diff --git a/src/main/java/de/thm/arsnova/persistance/ContentRepository.java b/src/main/java/de/thm/arsnova/persistance/ContentRepository.java deleted file mode 100644 index 9dbef6ce1605be5017c64818e5f2c8cec2771afe..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/ContentRepository.java +++ /dev/null @@ -1,32 +0,0 @@ -package de.thm.arsnova.persistance; - -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.User; -import org.springframework.data.repository.CrudRepository; - -import java.util.List; - -public interface ContentRepository extends CrudRepository<Content, String> { - List<Content> findBySessionIdAndVariantAndActive(Object... keys); - List<Content> findBySessionIdForUsers(String sessionId); - List<Content> findBySessionIdForSpeaker(String sessionId); - int countBySessionId(String sessionId); - List<String> findIdsBySessionId(String sessionId); - List<String> findIdsBySessionIdAndVariant(String sessionId, String variant); - int deleteBySessionId(String sessionId); - List<String> findUnansweredIdsBySessionIdAndUser(String sessionId, User user); - List<Content> findBySessionIdOnlyLectureVariantAndActive(String sessionId); - List<Content> findBySessionIdOnlyLectureVariant(String sessionId); - List<Content> findBySessionIdOnlyFlashcardVariantAndActive(String sessionId); - List<Content> findBySessionIdOnlyFlashcardVariant(String sessionId); - List<Content> findBySessionIdOnlyPreparationVariantAndActive(String sessionId); - List<Content> findBySessionIdOnlyPreparationVariant(String sessionId); - List<Content> findBySessionId(String sessionId); - int countLectureVariantBySessionId(String sessionId); - int countFlashcardVariantBySessionId(String sessionId); - int countPreparationVariantBySessionId(String sessionId); - List<String> findIdsBySessionIdAndVariantAndSubject(String sessionId, String questionVariant, String subject); - List<String> findSubjectsBySessionIdAndVariant(String sessionId, String questionVariant); - List<String> findUnansweredIdsBySessionIdAndUserOnlyLectureVariant(String sessionId, User user); - List<String> findUnansweredIdsBySessionIdAndUserOnlyPreparationVariant(String sessionId, User user); -} diff --git a/src/main/java/de/thm/arsnova/persistance/MotdListRepository.java b/src/main/java/de/thm/arsnova/persistance/MotdListRepository.java deleted file mode 100644 index 5b3a460f43216d8e8f70e9ac31160c59d41d3905..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/MotdListRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.thm.arsnova.persistance; - -import de.thm.arsnova.entities.MotdList; - -public interface MotdListRepository { - MotdList findByUsername(String username); - MotdList save(MotdList motdlist); -} diff --git a/src/main/java/de/thm/arsnova/persistance/SessionRepository.java b/src/main/java/de/thm/arsnova/persistance/SessionRepository.java deleted file mode 100644 index 6d2b31f633dfee6564f4ad9a86632aa9de1a6ca5..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/SessionRepository.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.persistance; - -import de.thm.arsnova.connector.model.Course; -import de.thm.arsnova.entities.LoggedIn; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.SessionInfo; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ImportExportSession; -import org.springframework.data.repository.CrudRepository; - -import java.util.List; - -public interface SessionRepository extends CrudRepository<Session, String> { - Session findByKeyword(String keyword); - List<Session> findInactiveGuestSessionsMetadata(long lastActivityBefore); - List<Session> findByUser(User user, int start, int limit); - List<Session> findByUsername(String username, int start, int limit); - List<Session> findAllForPublicPool(); - List<Session> findForPublicPoolByUser(User user); - List<Session> findVisitedByUsername(String username, int start, int limit); - List<SessionInfo> getMySessionsInfo(User user, int start, int limit); - List<SessionInfo> findInfosForPublicPool(); - List<SessionInfo> findInfosForPublicPoolByUser(User user); - List<SessionInfo> findInfoForVisitedByUser(User currentUser, int start, int limit); - List<Session> findSessionsByCourses(List<Course> courses); - SessionInfo importSession(User user, ImportExportSession importSession); - ImportExportSession exportSession(String sessionkey, Boolean withAnswer, Boolean withFeedbackQuestions); - LoggedIn registerAsOnlineUser(User user, Session session); -} diff --git a/src/main/java/de/thm/arsnova/persistance/SessionStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/SessionStatisticsRepository.java deleted file mode 100644 index 5791280415b3c7379780d03c7799eb5c2892b6ad..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/SessionStatisticsRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package de.thm.arsnova.persistance; - -import de.thm.arsnova.services.score.Score; -import de.thm.arsnova.entities.Session; - -public interface SessionStatisticsRepository { - Score getLearningProgress(Session session); -} diff --git a/src/main/java/de/thm/arsnova/persistance/StatisticsRepository.java b/src/main/java/de/thm/arsnova/persistance/StatisticsRepository.java deleted file mode 100644 index ea334f0d6932f2770a65c3f24aa71c7418a73847..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/StatisticsRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package de.thm.arsnova.persistance; - -import de.thm.arsnova.entities.Statistics; - -public interface StatisticsRepository { - Statistics getStatistics(); -} diff --git a/src/main/java/de/thm/arsnova/persistance/VisitedSessionRepository.java b/src/main/java/de/thm/arsnova/persistance/VisitedSessionRepository.java deleted file mode 100644 index ce0b2358155f8b539ecaec84f976c32b02577a84..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/VisitedSessionRepository.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.thm.arsnova.persistance; - -public interface VisitedSessionRepository { - int deleteInactiveGuestVisitedSessionLists(long lastActivityBefore); -} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java deleted file mode 100644 index ab9934fd1574e49daaf5b5c389eb3692c05c66ff..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbContentRepository.java +++ /dev/null @@ -1,282 +0,0 @@ -package de.thm.arsnova.persistance.couchdb; - -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.persistance.ContentRepository; -import de.thm.arsnova.persistance.LogEntryRepository; -import org.ektorp.BulkDeleteDocument; -import org.ektorp.ComplexKey; -import org.ektorp.CouchDbConnector; -import org.ektorp.DocumentOperationResult; -import org.ektorp.ViewResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -public class CouchDbContentRepository extends CouchDbCrudRepository<Content> implements ContentRepository { - private static final Logger logger = LoggerFactory.getLogger(CouchDbContentRepository.class); - - @Autowired - private LogEntryRepository dbLogger; - - public CouchDbContentRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(Content.class, db, "by_sessionid", createIfNotExists); - } - - @Override - public List<Content> findBySessionIdForUsers(final String sessionId) { - final List<Content> contents = new ArrayList<>(); - final List<Content> questions1 = findBySessionIdAndVariantAndActive(sessionId, "lecture", true); - final List<Content> questions2 = findBySessionIdAndVariantAndActive(sessionId, "preparation", true); - final List<Content> questions3 = findBySessionIdAndVariantAndActive(sessionId, "flashcard", true); - contents.addAll(questions1); - contents.addAll(questions2); - contents.addAll(questions3); - - return contents; - } - - @Override - public List<Content> findBySessionIdForSpeaker(final String sessionId) { - return findBySessionIdAndVariantAndActive(new Object[] {sessionId}, sessionId); - } - - @Override - public int countBySessionId(final String sessionId) { - final ViewResult result = db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId)) - .endKey(ComplexKey.of(sessionId, ComplexKey.emptyObject()))); - - return result.getSize(); - } - - @Override - public List<String> findIdsBySessionId(final String sessionId) { - return collectQuestionIds(db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId)) - .endKey(ComplexKey.of(sessionId, ComplexKey.emptyObject())))); - } - - @Override - public List<String> findIdsBySessionIdAndVariant(final String sessionId, final String variant) { - return collectQuestionIds(db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId, variant)) - .endKey(ComplexKey.of(sessionId, variant, ComplexKey.emptyObject())))); - } - - @Override - public int deleteBySessionId(final String sessionId) { - final ViewResult result = db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId)) - .endKey(ComplexKey.of(sessionId, ComplexKey.emptyObject())) - .reduce(false)); - - final List<BulkDeleteDocument> deleteDocs = new ArrayList<>(); - for (final ViewResult.Row a : result.getRows()) { - final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText()); - deleteDocs.add(d); - } - List<DocumentOperationResult> errors = db.executeBulk(deleteDocs); - - return deleteDocs.size() - errors.size(); - } - - @Override - public List<String> findUnansweredIdsBySessionIdAndUser(final String sessionId, final User user) { - final ViewResult result = db.queryView(createQuery("questionid_by_user_sessionid_variant") - .designDocId("_design/Answer") - .startKey(ComplexKey.of(user.getUsername(), sessionId)) - .endKey(ComplexKey.of(user.getUsername(), sessionId, ComplexKey.emptyObject()))); - final List<String> answeredIds = new ArrayList<>(); - for (final ViewResult.Row row : result.getRows()) { - answeredIds.add(row.getId()); - } - return collectUnansweredQuestionIds(findIdsBySessionId(sessionId), answeredIds); - } - - @Override - public List<String> findUnansweredIdsBySessionIdAndUserOnlyLectureVariant(final String sessionId, final User user) { - final ViewResult result = db.queryView(createQuery("questionid_piround_by_user_sessionid_variant") - .designDocId("_design/Answer") - .key(ComplexKey.of(user.getUsername(), sessionId, "lecture"))); - final Map<String, Integer> answeredQuestions = new HashMap<>(); - for (final ViewResult.Row row : result.getRows()) { - answeredQuestions.put(row.getId(), row.getKeyAsNode().get(2).asInt()); - } - - return collectUnansweredQuestionIdsByPiRound(findBySessionIdOnlyLectureVariantAndActive(sessionId), answeredQuestions); - } - - @Override - public List<String> findUnansweredIdsBySessionIdAndUserOnlyPreparationVariant(final String sessionId, final User user) { - final ViewResult result = db.queryView(createQuery("questionid_piround_by_user_sessionid_variant") - .designDocId("_design/Answer") - .key(ComplexKey.of(user.getUsername(), sessionId, "preparation"))); - final Map<String, Integer> answeredQuestions = new HashMap<>(); - for (final ViewResult.Row row : result.getRows()) { - answeredQuestions.put(row.getId(), row.getKeyAsNode().get(2).asInt()); - } - - return collectUnansweredQuestionIdsByPiRound(findBySessionIdOnlyPreparationVariantAndActive(sessionId), answeredQuestions); - } - - @Override - public List<Content> findBySessionIdOnlyLectureVariantAndActive(final String sessionId) { - return findBySessionIdAndVariantAndActive(sessionId, "lecture", true); - } - - @Override - public List<Content> findBySessionIdOnlyLectureVariant(final String sessionId) { - return findBySessionIdAndVariantAndActive(sessionId, "lecture"); - } - - @Override - public List<Content> findBySessionIdOnlyFlashcardVariantAndActive(final String sessionId) { - return findBySessionIdAndVariantAndActive(sessionId, "flashcard", true); - } - - @Override - public List<Content> findBySessionIdOnlyFlashcardVariant(final String sessionId) { - return findBySessionIdAndVariantAndActive(sessionId, "flashcard"); - } - - @Override - public List<Content> findBySessionIdOnlyPreparationVariantAndActive(final String sessionId) { - return findBySessionIdAndVariantAndActive(sessionId, "preparation", true); - } - - @Override - public List<Content> findBySessionIdOnlyPreparationVariant(final String sessionId) { - return findBySessionIdAndVariantAndActive(sessionId, "preparation"); - } - - @Override - public List<Content> findBySessionId(final String sessionId) { - return findBySessionIdAndVariantAndActive(sessionId); - } - - @Override - public List<Content> findBySessionIdAndVariantAndActive(final Object... keys) { - final Object[] endKeys = Arrays.copyOf(keys, keys.length + 1); - endKeys[keys.length] = ComplexKey.emptyObject(); - final List<Content> contents = db.queryView(createQuery("by_sessionid_variant_active") - .includeDocs(true) - .reduce(false) - .startKey(ComplexKey.of(keys)) - .endKey(ComplexKey.of(endKeys)), - Content.class); - for (final Content content : contents) { - content.updateRoundManagementState(); - //content.setSessionKeyword(session.getKeyword()); - } - - return contents; - } - - @Override - public int countLectureVariantBySessionId(final String sessionId) { - /* TODO: reduce code duplication */ - final ViewResult result = db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId, "lecture")) - .endKey(ComplexKey.of(sessionId, "lecture", ComplexKey.emptyObject()))); - - return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); - } - - @Override - public int countFlashcardVariantBySessionId(final String sessionId) { - /* TODO: reduce code duplication */ - final ViewResult result = db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId, "flashcard")) - .endKey(ComplexKey.of(sessionId, "flashcard", ComplexKey.emptyObject()))); - - return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); - } - - @Override - public int countPreparationVariantBySessionId(final String sessionId) { - /* TODO: reduce code duplication */ - final ViewResult result = db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId, "preparation")) - .endKey(ComplexKey.of(sessionId, "preparation", ComplexKey.emptyObject()))); - - return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); - } - - private List<String> collectUnansweredQuestionIds( - final List<String> contentIds, - final List<String> answeredContentIds - ) { - final List<String> unanswered = new ArrayList<>(); - for (final String contentId : contentIds) { - if (!answeredContentIds.contains(contentId)) { - unanswered.add(contentId); - } - } - return unanswered; - } - - private List<String> collectUnansweredQuestionIdsByPiRound( - final List<Content> contents, - final Map<String, Integer> answeredQuestions - ) { - final List<String> unanswered = new ArrayList<>(); - - for (final Content content : contents) { - if (!"slide".equals(content.getQuestionType()) && (!answeredQuestions.containsKey(content.getId()) - || (answeredQuestions.containsKey(content.getId()) && answeredQuestions.get(content.getId()) != content.getPiRound()))) { - unanswered.add(content.getId()); - } - } - - return unanswered; - } - - private List<String> collectQuestionIds(final ViewResult viewResult) { - final List<String> ids = new ArrayList<>(); - for (final ViewResult.Row row : viewResult.getRows()) { - ids.add(row.getId()); - } - return ids; - } - - /* TODO: remove if this method is no longer used */ - @Override - public List<String> findIdsBySessionIdAndVariantAndSubject(final String sessionId, final String questionVariant, final String subject) { - final ViewResult result = db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId, questionVariant, 1, subject)) - .endKey(ComplexKey.of(sessionId, questionVariant, 1, subject, ComplexKey.emptyObject()))); - - final List<String> qids = new ArrayList<>(); - - for (final ViewResult.Row row : result.getRows()) { - final String s = row.getId(); - qids.add(s); - } - - return qids; - } - - @Override - public List<String> findSubjectsBySessionIdAndVariant(final String sessionId, final String questionVariant) { - final ViewResult result = db.queryView(createQuery("by_sessionid_variant_active") - .startKey(ComplexKey.of(sessionId, questionVariant)) - .endKey(ComplexKey.of(sessionId, questionVariant, ComplexKey.emptyObject()))); - - final Set<String> uniqueSubjects = new HashSet<>(); - - for (final ViewResult.Row row : result.getRows()) { - uniqueSubjects.add(row.getKeyAsNode().get(3).asText()); - } - - return new ArrayList<>(uniqueSubjects); - } -} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java deleted file mode 100644 index 499013a874871e16905241e0a9a296f3be4145c0..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdListRepository.java +++ /dev/null @@ -1,43 +0,0 @@ -package de.thm.arsnova.persistance.couchdb; - -import de.thm.arsnova.entities.MotdList; -import de.thm.arsnova.persistance.MotdListRepository; -import org.ektorp.CouchDbConnector; -import org.ektorp.DbAccessException; -import org.ektorp.support.CouchDbRepositorySupport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; - -public class CouchDbMotdListRepository extends CouchDbRepositorySupport<MotdList> implements MotdListRepository { - private static final Logger logger = LoggerFactory.getLogger(CouchDbMotdListRepository.class); - - public CouchDbMotdListRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(MotdList.class, db, createIfNotExists); - } - - @Override - public MotdList findByUsername(final String username) { - final List<MotdList> motdListList = queryView("by_username", username); - return motdListList.isEmpty() ? new MotdList() : motdListList.get(0); - } - - /* TODO: Move to service layer. */ - @Override - public MotdList save(final MotdList motdlist) { - try { - if (motdlist.getId() != null) { - update(motdlist); - } else { - db.create(motdlist); - } - - return motdlist; - } catch (final DbAccessException e) { - logger.error("Could not save MotD list {}.", motdlist, e); - } - - return null; - } -} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java deleted file mode 100644 index c5f6d05283a35a9a5c05adf197f400594a604d2c..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionRepository.java +++ /dev/null @@ -1,572 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.persistance.couchdb; - -import de.thm.arsnova.connector.model.Course; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.LoggedIn; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.SessionInfo; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.VisitedSession; -import de.thm.arsnova.entities.transport.ImportExportSession; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.persistance.LogEntryRepository; -import de.thm.arsnova.persistance.MotdRepository; -import de.thm.arsnova.persistance.SessionRepository; -import org.ektorp.ComplexKey; -import org.ektorp.CouchDbConnector; -import org.ektorp.DocumentNotFoundException; -import org.ektorp.UpdateConflictException; -import org.ektorp.ViewQuery; -import org.ektorp.ViewResult; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cache.annotation.Cacheable; - -import java.io.IOException; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -public class CouchDbSessionRepository extends CouchDbCrudRepository<Session> implements SessionRepository { - private static final Logger logger = LoggerFactory.getLogger(CouchDbSessionRepository.class); - - @Autowired - private LogEntryRepository dbLogger; - - @Autowired - private MotdRepository motdRepository; - - public CouchDbSessionRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(Session.class, db, "by_keyword", createIfNotExists); - } - - @Override - @Cacheable("sessions") - public Session findByKeyword(final String keyword) { - final List<Session> session = queryView("by_keyword", keyword); - - return !session.isEmpty() ? session.get(0) : null; - } - - /* TODO: Move to service layer. */ - private String getSessionKeyword(final String internalSessionId) throws IOException { - final Session session = get(internalSessionId); - if (session == null) { - logger.error("No session found for internal id {}.", internalSessionId); - - return null; - } - - return session.getKeyword(); - } - - @Override - public List<Session> findVisitedByUsername(final String username, final int start, final int limit) { - final int qSkip = start > 0 ? start : -1; - final int qLimit = limit > 0 ? limit : -1; - - try { - final ViewResult visitedSessionResult = db.queryView(createQuery("visited_sessions_by_user") - .designDocId("_design/LoggedIn").key(username)); - final List<Session> visitedSessions = visitedSessionResult.getRows().stream().map(vs -> { - final Session s = new Session(); - s.setId(vs.getValueAsNode().get("_id").asText()); - s.setKeyword(vs.getValueAsNode().get("keyword").asText()); - s.setName(vs.getValueAsNode().get("name").asText()); - - return s; - }).collect(Collectors.toList()); - - if (visitedSessions.isEmpty()) { - return new ArrayList<>(); - } - - // Filter sessions that don't exist anymore, also filter my own sessions - final List<Session> result = new ArrayList<>(); - final List<Session> filteredSessions = new ArrayList<>(); - for (final Session s : visitedSessions) { - try { - /* FIXME: caching (getSessionFromKeyword) */ - final Session session = findByKeyword(s.getKeyword()); - if (session != null && !(session.getCreator().equals(username))) { - result.add(session); - } else { - filteredSessions.add(s); - } - } catch (final NotFoundException e) { - filteredSessions.add(s); - } - } - if (filteredSessions.isEmpty()) { - return result; - } - // Update document to remove sessions that don't exist anymore - final List<VisitedSession> newVisitedSessions = new ArrayList<>(); - for (final Session s : result) { - newVisitedSessions.add(new VisitedSession(s)); - } - - try { - final LoggedIn loggedIn = db.get(LoggedIn.class, visitedSessionResult.getRows().get(0).getId()); - loggedIn.setVisitedSessions(newVisitedSessions); - db.update(loggedIn); - } catch (UpdateConflictException e) { - logger.error("Could not clean up LoggedIn document of {}.", username, e); - } - - return result; - } catch (final DocumentNotFoundException e) { - return new ArrayList<>(); - } - } - - @Override - public List<SessionInfo> findInfoForVisitedByUser(final User user, final int start, final int limit) { - final List<Session> sessions = findVisitedByUsername(user.getUsername(), start, limit); - if (sessions.isEmpty()) { - return new ArrayList<>(); - } - return this.getInfosForVisitedSessions(sessions, user); - } - - @Override - public List<Session> findSessionsByCourses(final List<Course> courses) { - return queryView("by_courseid", - ComplexKey.of(courses.stream().map(Course::getId).collect(Collectors.toList()))); - } - - @Override - public List<Session> findInactiveGuestSessionsMetadata(final long lastActivityBefore) { - final ViewResult result = db.queryView( - createQuery("by_lastactivity_for_guests").endKey(lastActivityBefore)); - final int[] count = new int[3]; - - List<Session> sessions = new ArrayList<>(); - for (final ViewResult.Row row : result.getRows()) { - final Session s = new Session(); - s.setId(row.getId()); - s.setRevision(row.getValueAsNode().get("_rev").asText()); - sessions.add(s); - } - - return sessions; - } - - /* TODO: Move to service layer. */ - @Override - public SessionInfo importSession(final User user, final ImportExportSession importSession) { - /* FIXME: not yet migrated - move to service layer */ - throw new UnsupportedOperationException(); -// final Session session = this.saveSession(user, importSession.generateSessionEntity(user)); -// final List<Document> questions = new ArrayList<>(); -// // We need to remember which answers belong to which question. -// // The answers need a questionId, so we first store the questions to get the IDs. -// // Then we update the answer objects and store them as well. -// final Map<Document, ImportExportSession.ImportExportContent> mapping = new HashMap<>(); -// // Later, generate all answer documents -// List<Document> answers = new ArrayList<>(); -// // We can then push answers together with comments in one large bulk request -// List<Document> interposedQuestions = new ArrayList<>(); -// // Motds shouldn't be forgotten, too -// List<Document> motds = new ArrayList<>(); -// try { -// // add session id to all questions and generate documents -// for (final ImportExportSession.ImportExportContent question : importSession.getQuestions()) { -// final Document doc = toQuestionDocument(session, question); -// question.setSessionId(session.getId()); -// questions.add(doc); -// mapping.put(doc, question); -// } -// database.bulkSaveDocuments(questions.toArray(new Document[questions.size()])); -// -// // bulk import answers together with interposed questions -// for (Map.Entry<Document, ImportExportSession.ImportExportContent> entry : mapping.entrySet()) { -// final Document doc = entry.getKey(); -// final ImportExportSession.ImportExportContent question = entry.getValue(); -// question.setId(doc.getId()); -// question.setRevision(doc.getRev()); -// for (final de.thm.arsnova.entities.transport.Answer answer : question.getAnswers()) { -// final Answer a = answer.generateAnswerEntity(user, question); -// final Document answerDoc = new Document(); -// answerDoc.put("type", "skill_question_answer"); -// answerDoc.put("sessionId", a.getSessionId()); -// answerDoc.put("questionId", a.getQuestionId()); -// answerDoc.put("answerSubject", a.getAnswerSubject()); -// answerDoc.put("questionVariant", a.getQuestionVariant()); -// answerDoc.put("questionValue", a.getQuestionValue()); -// answerDoc.put("answerText", a.getAnswerText()); -// answerDoc.put("answerTextRaw", a.getAnswerTextRaw()); -// answerDoc.put("timestamp", a.getTimestamp()); -// answerDoc.put("piRound", a.getPiRound()); -// answerDoc.put("abstention", a.isAbstention()); -// answerDoc.put("successfulFreeTextAnswer", a.isSuccessfulFreeTextAnswer()); -// // we do not store the user's name -// answerDoc.put("user", ""); -// answers.add(answerDoc); -// } -// } -// for (final de.thm.arsnova.entities.transport.Comment i : importSession.getFeedbackQuestions()) { -// final Document q = new Document(); -// q.put("type", "interposed_question"); -// q.put("sessionId", session.getId()); -// q.put("subject", i.getSubject()); -// q.put("text", i.getText()); -// q.put("timestamp", i.getTimestamp()); -// q.put("read", i.isRead()); -// // we do not store the creator's name -// q.put("creator", ""); -// interposedQuestions.add(q); -// } -// for (final Motd m : importSession.getMotds()) { -// final Document d = new Document(); -// d.put("type", "motd"); -// d.put("motdkey", m.getMotdkey()); -// d.put("title", m.getTitle()); -// d.put("text", m.getText()); -// d.put("audience", m.getAudience()); -// d.put("sessionkey", session.getKeyword()); -// d.put("startdate", String.valueOf(m.getStartdate().getTime())); -// d.put("enddate", String.valueOf(m.getEnddate().getTime())); -// motds.add(d); -// } -// final List<Document> documents = new ArrayList<>(answers); -// database.bulkSaveDocuments(interposedQuestions.toArray(new Document[interposedQuestions.size()])); -// database.bulkSaveDocuments(motds.toArray(new Document[motds.size()])); -// database.bulkSaveDocuments(documents.toArray(new Document[documents.size()])); -// } catch (final IOException e) { -// logger.error("Could not import session.", e); -// // Something went wrong, delete this session since we do not want a partial import -// this.deleteSession(session); -// return null; -// } -// return this.calculateSessionInfo(importSession, session); - } - - /* TODO: Move to service layer. */ - @Override - public ImportExportSession exportSession( - final String sessionkey, - final Boolean withAnswers, - final Boolean withFeedbackQuestions) { - /* FIXME: not yet migrated - move to service layer */ - throw new UnsupportedOperationException(); -// final ImportExportSession importExportSession = new ImportExportSession(); -// final Session session = getDatabaseDao().getSessionFromKeyword(sessionkey); -// importExportSession.setSessionFromSessionObject(session); -// final List<Content> questionList = getDatabaseDao().getAllSkillQuestions(session); -// for (final Content question : questionList) { -// final List<de.thm.arsnova.entities.transport.Answer> answerList = new ArrayList<>(); -// if (withAnswers) { -// for (final Answer a : this.getDatabaseDao().getAllAnswers(question)) { -// final de.thm.arsnova.entities.transport.Answer transportAnswer = new de.thm.arsnova.entities.transport.Answer(a); -// answerList.add(transportAnswer); -// } -// // getAllAnswers does not grep for whole answer object so i need to add empty entries for abstentions -// int i = this.getDatabaseDao().getAbstentionAnswerCount(question.getId()); -// for (int b = 0; b < i; b++) { -// final de.thm.arsnova.entities.transport.Answer ans = new de.thm.arsnova.entities.transport.Answer(); -// ans.setAnswerSubject(""); -// ans.setAnswerImage(""); -// ans.setAnswerText(""); -// ans.setAbstention(true); -// answerList.add(ans); -// } -// } -// importExportSession.addQuestionWithAnswers(question, answerList); -// } -// if (withFeedbackQuestions) { -// final List<de.thm.arsnova.entities.transport.Comment> interposedQuestionList = new ArrayList<>(); -// for (final Comment i : getDatabaseDao().getInterposedQuestions(session, 0, 0)) { -// de.thm.arsnova.entities.transport.Comment transportInterposedQuestion = new de.thm.arsnova.entities.transport.Comment(i); -// interposedQuestionList.add(transportInterposedQuestion); -// } -// importExportSession.setFeedbackQuestions(interposedQuestionList); -// } -// if (withAnswers) { -// importExportSession.setSessionInfo(this.calculateSessionInfo(importExportSession, session)); -// } -// importExportSession.setMotds(motdRepository.getMotdsForSession(session.getKeyword())); -// return importExportSession; - } - - /* TODO: Move to service layer. */ - private SessionInfo calculateSessionInfo(final ImportExportSession importExportSession, final Session session) { - int unreadComments = 0; - int numUnanswered = 0; - int numAnswers = 0; - for (Comment i : importExportSession.getFeedbackQuestions()) { - if (!i.isRead()) { - unreadComments++; - } - } - for (ImportExportSession.ImportExportContent question : importExportSession.getQuestions()) { - numAnswers += question.getAnswers().size(); - if (question.getAnswers().isEmpty()) { - numUnanswered++; - } - } - final SessionInfo info = new SessionInfo(session); - info.setNumQuestions(importExportSession.getQuestions().size()); - info.setNumUnanswered(numUnanswered); - info.setNumAnswers(numAnswers); - info.setNumInterposed(importExportSession.getFeedbackQuestions().size()); - info.setNumUnredInterposed(unreadComments); - return info; - } - - @Override - public List<Session> findByUser(final User user, final int start, final int limit) { - return findByUsername(user.getUsername(), start, limit); - } - - @Override - public List<Session> findByUsername(final String username, final int start, final int limit) { - final int qSkip = start > 0 ? start : -1; - final int qLimit = limit > 0 ? limit : -1; - - /* TODO: Only load IDs and check against cache for data. */ - return db.queryView( - createQuery("partial_by_sessiontype_creator_name") - .skip(qSkip) - .limit(qLimit) - .startKey(ComplexKey.of(null, username)) - .endKey(ComplexKey.of(null, username, ComplexKey.emptyObject())) - .includeDocs(true), - Session.class); - } - - @Override - public List<Session> findAllForPublicPool() { - // TODO replace with new view - return queryView("partial_by_ppsubject_name_for_publicpool"); - } - - @Override - public List<SessionInfo> findInfosForPublicPool() { - final List<Session> sessions = this.findAllForPublicPool(); - return getInfosForSessions(sessions); - } - - @Override - public List<Session> findForPublicPoolByUser(final User user) { - /* TODO: Only load IDs and check against cache for data. */ - return db.queryView( - createQuery("partial_by_sessiontype_creator_name") - .startKey(ComplexKey.of("public_pool", user.getUsername())) - .endKey(ComplexKey.of("public_pool", user.getUsername(), ComplexKey.emptyObject())) - .includeDocs(true), - Session.class); - } - - /* TODO: Move to service layer. */ - @Override - public List<SessionInfo> findInfosForPublicPoolByUser(final User user) { - final List<Session> sessions = this.findForPublicPoolByUser(user); - if (sessions.isEmpty()) { - return new ArrayList<>(); - } - return getInfosForSessions(sessions); - } - - /* TODO: Move to service layer. */ - @Override - public List<SessionInfo> getMySessionsInfo(final User user, final int start, final int limit) { - final List<Session> sessions = this.findByUser(user, start, limit); - if (sessions.isEmpty()) { - return new ArrayList<>(); - } - return getInfosForSessions(sessions); - } - - /* TODO: Move to service layer. */ - private List<SessionInfo> getInfosForSessions(final List<Session> sessions) { - final List<String> sessionIds = sessions.stream().map(Session::getId).collect(Collectors.toList()); - final ViewQuery questionCountView = createQuery("by_sessionid").designDocId("_design/Content") - .group(true).keys(sessionIds); - final ViewQuery answerCountView = createQuery("by_sessionid").designDocId("_design/Answer") - .group(true).keys(sessionIds); - final ViewQuery commentCountView = createQuery("by_sessionid").designDocId("_design/Comment") - .group(true).keys(sessionIds); - final ViewQuery unreadCommentCountView = createQuery("by_sessionid_read").designDocId("_design/Comment") - .group(true).keys(sessions.stream().map(session -> ComplexKey.of(session.getId(), false)).collect(Collectors.toList())); - - return getSessionInfoData(sessions, questionCountView, answerCountView, commentCountView, unreadCommentCountView); - } - - /* TODO: Move to service layer. */ - private List<SessionInfo> getInfosForVisitedSessions(final List<Session> sessions, final User user) { - final ViewQuery answeredQuestionsView = createQuery("by_user_sessionid").designDocId("_design/Answer") - .keys(sessions.stream().map(session -> ComplexKey.of(user.getUsername(), session.getId())).collect(Collectors.toList())); - final ViewQuery contentIdsView = createQuery("by_sessionid").designDocId("_design/Content") - .keys(sessions.stream().map(Session::getId).collect(Collectors.toList())); - - return getVisitedSessionInfoData(sessions, answeredQuestionsView, contentIdsView); - } - - /* TODO: Move to service layer. */ - private List<SessionInfo> getVisitedSessionInfoData( - final List<Session> sessions, - final ViewQuery answeredQuestionsView, - final ViewQuery contentIdsView) { - final Map<String, Set<String>> answeredQuestionsMap = new HashMap<>(); - final Map<String, Set<String>> contentIdMap = new HashMap<>(); - - // Maps a session ID to a set of question IDs of answered questions of that session - for (final ViewResult.Row row : db.queryView(answeredQuestionsView).getRows()) { - final String sessionId = row.getKey(); - final String contentId = row.getValue(); - Set<String> contentIdsInSession = answeredQuestionsMap.get(sessionId); - if (contentIdsInSession == null) { - contentIdsInSession = new HashSet<>(); - } - contentIdsInSession.add(contentId); - answeredQuestionsMap.put(sessionId, contentIdsInSession); - } - - // Maps a session ID to a set of question IDs of that session - for (final ViewResult.Row row : db.queryView(contentIdsView).getRows()) { - final String sessionId = row.getKey(); - final String contentId = row.getId(); - Set<String> contentIdsInSession = contentIdMap.get(sessionId); - if (contentIdsInSession == null) { - contentIdsInSession = new HashSet<>(); - } - contentIdsInSession.add(contentId); - contentIdMap.put(sessionId, contentIdsInSession); - } - - // For each session, count the question IDs that are not yet answered - final Map<String, Integer> unansweredQuestionsCountMap = new HashMap<>(); - for (final Session s : sessions) { - if (!contentIdMap.containsKey(s.getId())) { - continue; - } - // Note: create a copy of the first set so that we don't modify the contents in the original set - final Set<String> contentIdsInSession = new HashSet<>(contentIdMap.get(s.getId())); - Set<String> answeredContentIdsInSession = answeredQuestionsMap.get(s.getId()); - if (answeredContentIdsInSession == null) { - answeredContentIdsInSession = new HashSet<>(); - } - contentIdsInSession.removeAll(answeredContentIdsInSession); - unansweredQuestionsCountMap.put(s.getId(), contentIdsInSession.size()); - } - - final List<SessionInfo> sessionInfos = new ArrayList<>(); - for (final Session session : sessions) { - int numUnanswered = 0; - - if (unansweredQuestionsCountMap.containsKey(session.getId())) { - numUnanswered = unansweredQuestionsCountMap.get(session.getId()); - } - final SessionInfo info = new SessionInfo(session); - info.setNumUnanswered(numUnanswered); - sessionInfos.add(info); - } - return sessionInfos; - } - - /* TODO: Move to service layer. */ - private List<SessionInfo> getSessionInfoData( - final List<Session> sessions, - final ViewQuery questionCountView, - final ViewQuery answerCountView, - final ViewQuery commentCountView, - final ViewQuery unreadCommentCountView) { - final Map<String, Integer> questionCountMap = db.queryView(questionCountView).getRows() - .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - final Map<String, Integer> answerCountMap = db.queryView(answerCountView).getRows() - .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - final Map<String, Integer> commentCountMap = db.queryView(commentCountView).getRows() - .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - final Map<String, Integer> unreadCommentCountMap = db.queryView(unreadCommentCountView).getRows() - .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - final List<SessionInfo> sessionInfos = new ArrayList<>(); - for (final Session session : sessions) { - int numQuestions = 0; - int numAnswers = 0; - int numComments = 0; - int numUnreadComments = 0; - if (questionCountMap.containsKey(session.getId())) { - numQuestions = questionCountMap.get(session.getId()); - } - if (answerCountMap.containsKey(session.getId())) { - numAnswers = answerCountMap.get(session.getId()); - } - if (commentCountMap.containsKey(session.getId())) { - numComments = commentCountMap.get(session.getId()); - } - if (unreadCommentCountMap.containsKey(session.getId())) { - numUnreadComments = unreadCommentCountMap.get(session.getId()); - } - - final SessionInfo info = new SessionInfo(session); - info.setNumQuestions(numQuestions); - info.setNumAnswers(numAnswers); - info.setNumInterposed(numComments); - info.setNumUnredInterposed(numUnreadComments); - sessionInfos.add(info); - } - return sessionInfos; - } - - /* TODO: Move to service layer. */ - @Override - public LoggedIn registerAsOnlineUser(final User user, final Session session) { - LoggedIn loggedIn = new LoggedIn(); - try { - final List<LoggedIn> loggedInList = db.queryView(createQuery("all").designDocId("_design/LoggedIn").key(user.getUsername()), LoggedIn.class); - - if (!loggedInList.isEmpty()) { - loggedIn = loggedInList.get(0); - - /* Do not clutter CouchDB. Only update once every 3 hours per session. */ - if (loggedIn.getSessionId().equals(session.getId()) && loggedIn.getTimestamp() > System.currentTimeMillis() - 3 * 3600000) { - return loggedIn; - } - } - - loggedIn.setUser(user.getUsername()); - loggedIn.setSessionId(session.getId()); - loggedIn.addVisitedSession(session); - loggedIn.updateTimestamp(); - - if (loggedIn.getId() == null) { - db.create(loggedIn); - } else { - db.update(loggedIn); - } - } catch (final UpdateConflictException e) { - logger.error("Could not save LoggedIn document of {}.", user.getUsername(), e); - } - - return loggedIn; - } -} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbVisitedSessionRepository.java b/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbVisitedSessionRepository.java deleted file mode 100644 index a9bacfe27eb4f366cfdb5567981c5d841af7a195..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbVisitedSessionRepository.java +++ /dev/null @@ -1,70 +0,0 @@ -package de.thm.arsnova.persistance.couchdb; - -import com.google.common.collect.Lists; -import de.thm.arsnova.entities.VisitedSession; -import de.thm.arsnova.persistance.LogEntryRepository; -import de.thm.arsnova.persistance.VisitedSessionRepository; -import org.ektorp.BulkDeleteDocument; -import org.ektorp.CouchDbConnector; -import org.ektorp.DbAccessException; -import org.ektorp.DocumentOperationResult; -import org.ektorp.ViewResult; -import org.ektorp.support.CouchDbRepositorySupport; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.ArrayList; -import java.util.List; - -public class CouchDbVisitedSessionRepository extends CouchDbRepositorySupport<VisitedSession> implements VisitedSessionRepository { - private static final int BULK_PARTITION_SIZE = 500; - - private static final Logger logger = LoggerFactory.getLogger(CouchDbVisitedSessionRepository.class); - - @Autowired - private LogEntryRepository dbLogger; - - public CouchDbVisitedSessionRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(VisitedSession.class, db, createIfNotExists); - } - - @Override - public int deleteInactiveGuestVisitedSessionLists(final long lastActivityBefore) { - try { - final ViewResult result = db.queryView(createQuery("by_last_activity_for_guests").endKey(lastActivityBefore)); - - int count = 0; - final List<List<ViewResult.Row>> partitions = Lists.partition(result.getRows(), BULK_PARTITION_SIZE); - for (final List<ViewResult.Row> partition: partitions) { - final List<BulkDeleteDocument> newDocs = new ArrayList<>(); - for (final ViewResult.Row oldDoc : partition) { - final BulkDeleteDocument newDoc = new BulkDeleteDocument(oldDoc.getId(), oldDoc.getValueAsNode().get("_rev").asText()); - newDocs.add(newDoc); - logger.debug("Marked logged_in document {} for deletion.", oldDoc.getId()); - /* Use log type 'user' since effectively the user is deleted in case of guests */ - dbLogger.log("delete", "type", "user", "id", oldDoc.getId()); - } - - if (!newDocs.isEmpty()) { - final List<DocumentOperationResult> results = db.executeBulk(newDocs); - count += newDocs.size() - results.size(); - if (!results.isEmpty()) { - logger.error("Could not bulk delete some visited session lists."); - } - } - } - - if (count > 0) { - logger.info("Deleted {} visited session lists of inactive users.", count); - dbLogger.log("cleanup", "type", "visitedsessions", "count", count); - } - - return count; - } catch (final DbAccessException e) { - logger.error("Could not delete visited session lists of inactive users.", e); - } - - return 0; - } -} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java b/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java deleted file mode 100644 index fbe08d2dc2d1ee264158ec609715eace84cbdbe2..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/InitializingCouchDbConnector.java +++ /dev/null @@ -1,86 +0,0 @@ -package de.thm.arsnova.persistance.couchdb; - -import com.fasterxml.jackson.core.JsonProcessingException; -import de.thm.arsnova.persistance.couchdb.support.MangoCouchDbConnector; -import org.ektorp.CouchDbInstance; -import org.ektorp.DocumentNotFoundException; -import org.ektorp.impl.ObjectMapperFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ResourceLoaderAware; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.core.io.ResourceLoader; -import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.util.FileCopyUtils; - -import javax.script.Bindings; -import javax.script.ScriptEngine; -import javax.script.ScriptEngineManager; -import javax.script.ScriptException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.List; - -public class InitializingCouchDbConnector extends MangoCouchDbConnector implements InitializingBean, ResourceLoaderAware { - private static final Logger logger = LoggerFactory.getLogger(InitializingCouchDbConnector.class); - private final List<Bindings> docs = new ArrayList<>(); - - private ResourceLoader resourceLoader; - - public InitializingCouchDbConnector(final String databaseName, final CouchDbInstance dbInstance) { - super(databaseName, dbInstance); - } - - public InitializingCouchDbConnector(final String databaseName, final CouchDbInstance dbi, final ObjectMapperFactory om) { - super(databaseName, dbi, om); - } - - protected void loadDesignDocFiles() throws IOException, ScriptException { - final ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("application/javascript"); - engine.eval(new InputStreamReader(new ClassPathResource("couchdb/jsToJson.js").getInputStream())); - - final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); - final Resource[] resources = resolver.getResources("classpath:couchdb/*.design.js"); - for (Resource resource : resources) { - logger.debug("Loading CouchDB design doc: {}", resource.getFilename()); - final String js = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); - /* Reset designDoc before parsing a new one. */ - engine.eval("var designDoc = null;" + js); - final Bindings jsonObject = (Bindings) engine.eval("jsToJson(designDoc)"); - docs.add(jsonObject); - } - } - - protected void createDesignDocs() { - docs.forEach(doc -> { - if (logger.isDebugEnabled()) { - try { - logger.debug("Creating design doc:\n{}", objectMapper.writeValueAsString(doc)); - } catch (JsonProcessingException e) { - logger.warn("Failed to serialize design doc.", e); - } - } - try { - final String rev = getCurrentRevision((String) doc.get("_id")); - doc.put("_rev", rev); - update(doc); - } catch (final DocumentNotFoundException e) { - create(doc); - } - }); - } - - @Override - public void afterPropertiesSet() throws Exception { - loadDesignDocFiles(); - createDesignDocs(); - } - - @Override - public void setResourceLoader(final ResourceLoader resourceLoader) { - this.resourceLoader = resourceLoader; - } -} diff --git a/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java b/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java similarity index 60% rename from src/main/java/de/thm/arsnova/persistance/AnswerRepository.java rename to src/main/java/de/thm/arsnova/persistence/AnswerRepository.java index 67db63626e13b5619a25e9de8a157ce3680462e1..c07995b557722bf78420a8f18077dcea71d90e8e 100644 --- a/src/main/java/de/thm/arsnova/persistance/AnswerRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/AnswerRepository.java @@ -15,26 +15,25 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.persistance; +package de.thm.arsnova.persistence; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.User; -import org.springframework.data.repository.CrudRepository; +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.AnswerStatistics; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; import java.util.List; public interface AnswerRepository extends CrudRepository<Answer, String> { - Answer findByQuestionIdUserPiRound(String questionId, User user, int piRound); - List<Answer> findByContentIdPiRound(String contentId, int piRound); - List<Answer> findByContentId(String contentId); + <T extends Answer> T findByContentIdUserPiRound(String contentId, Class<T> type, ClientAuthentication user, int piRound); + AnswerStatistics findByContentIdRound(String contentId, int round, final int optionCount); int countByContentIdRound(String contentId, int round); int countByContentId(String contentId); - List<Answer> findByContentId(String contentId, int start, int limit); - List<Answer> findByUserSessionId(User user, String sessionId); - int countBySessionKey(String sessionKey); + <T extends Answer> List<T> findByContentId(String contentId, Class<T> type, int start, int limit); + List<Answer> findByUserRoomId(ClientAuthentication user, String roomId); + int countByRoomId(String roomId); int deleteByContentId(String contentId); - int countBySessionIdLectureVariant(String sessionId); - int countBySessionIdPreparationVariant(String sessionId); + int countByRoomIdOnlyLectureVariant(String roomId); + int countByRoomIdOnlyPreparationVariant(String roomId); int deleteAllAnswersForQuestions(List<String> contentIds); int deleteByContentIds(List<String> contentIds); } diff --git a/src/main/java/de/thm/arsnova/persistence/AttachmentRepository.java b/src/main/java/de/thm/arsnova/persistence/AttachmentRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..fb6bc64e1494ed09dc566db37cf2c6afdb62132f --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/AttachmentRepository.java @@ -0,0 +1,6 @@ +package de.thm.arsnova.persistence; + +import de.thm.arsnova.model.Attachment; + +public interface AttachmentRepository extends CrudRepository<Attachment, String> { +} diff --git a/src/main/java/de/thm/arsnova/persistence/CommentRepository.java b/src/main/java/de/thm/arsnova/persistence/CommentRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..afa7bf82e6884600184fd7b7c12f0b6db190edc6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/CommentRepository.java @@ -0,0 +1,18 @@ +package de.thm.arsnova.persistence; + +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.migration.v2.CommentReadingCount; + +import java.util.List; + +public interface CommentRepository extends CrudRepository<Comment, String> { + int countByRoomId(String roomId); + CommentReadingCount countReadingByRoomId(String roomId); + CommentReadingCount countReadingByRoomIdAndUser(String roomId, ClientAuthentication user); + List<Comment> findByRoomId(String roomId, int start, int limit); + List<Comment> findByRoomIdAndUser(String roomId, ClientAuthentication user, int start, int limit); + Comment findOne(String commentId); + int deleteByRoomId(String roomId); + int deleteByRoomIdAndUser(String roomId, ClientAuthentication user); +} diff --git a/src/main/java/de/thm/arsnova/persistence/ContentRepository.java b/src/main/java/de/thm/arsnova/persistence/ContentRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..89be0738817873d57dc8cb1212e6b7ba0e70d44b --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/ContentRepository.java @@ -0,0 +1,31 @@ +package de.thm.arsnova.persistence; + +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; + +import java.util.List; + +public interface ContentRepository extends CrudRepository<Content, String> { + List<Content> findByRoomIdAndVariantAndActive(Object... keys); + List<Content> findByRoomIdForUsers(String roomId); + List<Content> findByRoomIdForSpeaker(String roomId); + int countByRoomId(String roomId); + List<String> findIdsByRoomId(String roomId); + List<String> findIdsByRoomIdAndVariant(String roomId, String variant); + int deleteByRoomId(String roomId); + List<String> findUnansweredIdsByRoomIdAndUser(String roomId, ClientAuthentication user); + List<Content> findByRoomIdOnlyLectureVariantAndActive(String roomId); + List<Content> findByRoomIdOnlyLectureVariant(String roomId); + List<Content> findByRoomIdOnlyFlashcardVariantAndActive(String roomId); + List<Content> findByRoomIdOnlyFlashcardVariant(String roomId); + List<Content> findByRoomIdOnlyPreparationVariantAndActive(String roomId); + List<Content> findByRoomIdOnlyPreparationVariant(String roomId); + List<Content> findByRoomId(String roomId); + int countLectureVariantByRoomId(String roomId); + int countFlashcardVariantRoomId(String roomId); + int countPreparationVariantByRoomId(String roomId); + List<String> findIdsByRoomIdAndVariantAndSubject(String roomId, String questionVariant, String subject); + List<String> findSubjectsByRoomIdAndVariant(String roomId, String questionVariant); + List<String> findUnansweredIdsByRoomIdAndUserOnlyLectureVariant(String roomId, ClientAuthentication user); + List<String> findUnansweredIdsByRoomIdAndUserOnlyPreparationVariant(String roomId, ClientAuthentication user); +} diff --git a/src/main/java/de/thm/arsnova/persistence/CrudRepository.java b/src/main/java/de/thm/arsnova/persistence/CrudRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..98390d49fa3e5a585438b88cd1d7940a7eefca2d --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/CrudRepository.java @@ -0,0 +1,37 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.persistence; + +/** + * This is a temporary extension to {@link org.springframework.data.repository.CrudRepository} which simplifies the + * migration to Spring Data 2.0. + * + * {@inheritDoc} + * + * @author Daniel Gerhardt + */ +public interface CrudRepository<T, ID> extends org.springframework.data.repository.CrudRepository<T, ID> { + /** + * + * @param id The entity's Id + * @return The retrieved entity or null + * @deprecated Use {@link #findById(Object)} instead. + */ + @Deprecated + T findOne(final ID id); +} diff --git a/src/main/java/de/thm/arsnova/persistance/LogEntryRepository.java b/src/main/java/de/thm/arsnova/persistence/LogEntryRepository.java similarity index 97% rename from src/main/java/de/thm/arsnova/persistance/LogEntryRepository.java rename to src/main/java/de/thm/arsnova/persistence/LogEntryRepository.java index 1515e340c2b53530848f9a3b2d6ba9f3e311a61b..49d91f5113e6e1ffd4ce75f5f97ea1033a1db241 100644 --- a/src/main/java/de/thm/arsnova/persistance/LogEntryRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/LogEntryRepository.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.persistance; +package de.thm.arsnova.persistence; -import de.thm.arsnova.entities.LogEntry; +import de.thm.arsnova.model.migration.v2.LogEntry; import java.util.HashMap; import java.util.Map; diff --git a/src/main/java/de/thm/arsnova/persistance/MotdRepository.java b/src/main/java/de/thm/arsnova/persistence/MotdRepository.java similarity index 83% rename from src/main/java/de/thm/arsnova/persistance/MotdRepository.java rename to src/main/java/de/thm/arsnova/persistence/MotdRepository.java index f403ccb271ee40c73f8685f4ee7551c097bc571e..f320bbe66d3e6c15e51803f72792649c8706f5b9 100644 --- a/src/main/java/de/thm/arsnova/persistance/MotdRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/MotdRepository.java @@ -15,10 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.persistance; +package de.thm.arsnova.persistence; -import de.thm.arsnova.entities.Motd; -import org.springframework.data.repository.CrudRepository; +import de.thm.arsnova.model.Motd; import java.util.List; @@ -28,6 +27,5 @@ public interface MotdRepository extends CrudRepository<Motd, String> { List<Motd> findGlobalForLoggedIn(); List<Motd> findGlobalForTutors(); List<Motd> findForStudents(); - List<Motd> findBySessionKey(String sessionkey); - Motd findByKey(String key); + List<Motd> findByRoomId(String roomId); } diff --git a/src/main/java/de/thm/arsnova/persistence/RoomRepository.java b/src/main/java/de/thm/arsnova/persistence/RoomRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..9e811203ca4b6e2691dff15ebb9364ac92c21f0d --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/RoomRepository.java @@ -0,0 +1,42 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.persistence; + +import de.thm.arsnova.connector.model.Course; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ImportExportContainer; + +import java.util.List; + +public interface RoomRepository extends CrudRepository<Room, String> { + Room findByShortId(String shortId); + List<Room> findInactiveGuestRoomsMetadata(long lastActivityBefore); + List<Room> findByOwner(ClientAuthentication owner, int start, int limit); + List<Room> findByOwnerId(String ownerId, int start, int limit); + List<String> findIdsByOwnerId(String ownerId); + List<Room> findAllForPublicPool(); + List<Room> findForPublicPoolByOwner(ClientAuthentication owner); + List<Room> getRoomsWithStatsForOwner(ClientAuthentication owner, int start, int limit); + List<Room> getRoomHistoryWithStatsForUser(List<Room> rooms, ClientAuthentication owner); + List<Room> findInfosForPublicPool(); + List<Room> findInfosForPublicPoolByOwner(ClientAuthentication owner); + List<Room> findRoomsByCourses(List<Course> courses); + Room importRoom(ClientAuthentication user, ImportExportContainer importRoom); + ImportExportContainer exportRoom(String id, Boolean withAnswer, Boolean withFeedbackQuestions); +} diff --git a/src/main/java/de/thm/arsnova/persistence/SessionStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistence/SessionStatisticsRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..7812c1469c5aa3afe9d4f71a5821d0bd21ee775a --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/SessionStatisticsRepository.java @@ -0,0 +1,8 @@ +package de.thm.arsnova.persistence; + +import de.thm.arsnova.model.Room; +import de.thm.arsnova.service.score.Score; + +public interface SessionStatisticsRepository { + Score getLearningProgress(Room room); +} diff --git a/src/main/java/de/thm/arsnova/persistence/StatisticsRepository.java b/src/main/java/de/thm/arsnova/persistence/StatisticsRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..03e7be384ea81fdfc757eea7e71d89e33a0ef976 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/StatisticsRepository.java @@ -0,0 +1,7 @@ +package de.thm.arsnova.persistence; + +import de.thm.arsnova.model.Statistics; + +public interface StatisticsRepository { + Statistics getStatistics(); +} diff --git a/src/test/java/de/thm/arsnova/entities/TestUser.java b/src/main/java/de/thm/arsnova/persistence/UserRepository.java similarity index 67% rename from src/test/java/de/thm/arsnova/entities/TestUser.java rename to src/main/java/de/thm/arsnova/persistence/UserRepository.java index cba4b3e07e0abad7816ff4e49a582c56ba5cc012..61ab8b2589d247bbfebbb1dd98eab888cb6193d3 100644 --- a/src/test/java/de/thm/arsnova/entities/TestUser.java +++ b/src/main/java/de/thm/arsnova/persistence/UserRepository.java @@ -15,14 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.persistence; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import de.thm.arsnova.model.UserProfile; -public class TestUser extends User { - private static final long serialVersionUID = 1L; +import java.util.List; - public TestUser(String username) { - super( new UsernamePasswordAuthenticationToken(username, "secret") ); - } +public interface UserRepository extends CrudRepository<UserProfile, String> { + UserProfile findByAuthProviderAndLoginId(UserProfile.AuthProvider authProvider, String loginId); + List<UserProfile> findByLoginId(String loginId); + int deleteInactiveUsers(long lastActivityBefore); } diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java similarity index 52% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java index 96f8e43ed62a0c7fd7d60cf18e3b9355d7ea386f..026246bb3b9b3cb7bdd8cf3bd548fd579c592b5b 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbAnswerRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAnswerRepository.java @@ -1,17 +1,17 @@ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; import com.fasterxml.jackson.databind.JsonNode; import com.google.common.collect.Lists; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.persistance.AnswerRepository; -import de.thm.arsnova.persistance.LogEntryRepository; +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.AnswerStatistics; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.persistence.AnswerRepository; +import de.thm.arsnova.persistence.LogEntryRepository; import org.ektorp.BulkDeleteDocument; import org.ektorp.ComplexKey; import org.ektorp.CouchDbConnector; import org.ektorp.DbAccessException; import org.ektorp.DocumentOperationResult; -import org.ektorp.UpdateConflictException; import org.ektorp.ViewResult; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,7 +20,11 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> implements AnswerRepository, ApplicationEventPublisherAware { private static final int BULK_PARTITION_SIZE = 500; @@ -32,7 +36,7 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple private ApplicationEventPublisher publisher; public CouchDbAnswerRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(Answer.class, db, "by_sessionid", createIfNotExists); + super(Answer.class, db, "by_id", createIfNotExists); } @Override @@ -43,7 +47,7 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple @Override public int deleteByContentId(final String contentId) { try { - final ViewResult result = db.queryView(createQuery("by_questionid") + final ViewResult result = db.queryView(createQuery("by_contentid") .key(contentId)); final List<List<ViewResult.Row>> partitions = Lists.partition(result.getRows(), BULK_PARTITION_SIZE); @@ -71,65 +75,60 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple } @Override - public Answer findByQuestionIdUserPiRound(final String contentId, final User user, final int piRound) { - final List<Answer> answerList = queryView("by_questionid_user_piround", - ComplexKey.of(contentId, user.getUsername(), piRound)); + public <T extends Answer> T findByContentIdUserPiRound(final String contentId, final Class<T> type, final ClientAuthentication user, final int piRound) { + final List<T> answerList = db.queryView(createQuery("by_contentid_creatorid_round") + .key(ComplexKey.of(contentId, user.getUsername(), piRound)), type); return answerList.isEmpty() ? null : answerList.get(0); } @Override - public List<Answer> findByContentIdPiRound(final String contentId, final int piRound) { - final String questionId = contentId; - final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + public AnswerStatistics findByContentIdRound(final String contentId, final int round, final int optionCount) { + final ViewResult result = db.queryView(createQuery("by_contentid_round_selectedchoiceindexes") .group(true) - .startKey(ComplexKey.of(questionId, piRound)) - .endKey(ComplexKey.of(questionId, piRound, ComplexKey.emptyObject()))); - final int abstentionCount = countByContentId(questionId); - - final List<Answer> answers = new ArrayList<>(); + .startKey(ComplexKey.of(contentId, round)) + .endKey(ComplexKey.of(contentId, round, ComplexKey.emptyObject()))); + final AnswerStatistics stats = new AnswerStatistics(); + stats.setContentId(contentId); + final AnswerStatistics.RoundStatistics roundStats = new AnswerStatistics.RoundStatistics(); + roundStats.setRound(round); + roundStats.setAbstentionCount(0); + final List<Integer> independentCounts = new ArrayList(Collections.nCopies(optionCount, 0)); + final Map<List<Integer>, AnswerStatistics.RoundStatistics.Combination> combinations = new HashMap(); for (final ViewResult.Row d : result) { - final Answer a = new Answer(); - a.setAnswerCount(d.getValueAsInt()); - a.setAbstentionCount(abstentionCount); - a.setQuestionId(d.getKeyAsNode().get(0).asText()); - a.setPiRound(piRound); - final JsonNode answerTextNode = d.getKeyAsNode().get(3); - a.setAnswerText(answerTextNode.isNull() ? null : answerTextNode.asText()); - answers.add(a); - } - - return answers; - } - - @Override - public List<Answer> findByContentId(final String contentId) { - final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") - .group(true) - .startKey(ComplexKey.of(contentId)) - .endKey(ComplexKey.of(contentId, ComplexKey.emptyObject()))); - final int abstentionCount = countByContentId(contentId); - - final List<Answer> answers = new ArrayList<>(); - for (final ViewResult.Row d : result.getRows()) { - final Answer a = new Answer(); - a.setAnswerCount(d.getValueAsInt()); - a.setAbstentionCount(abstentionCount); - a.setQuestionId(d.getKeyAsNode().get(0).asText()); - final JsonNode answerTextNode = d.getKeyAsNode().get(3); - final JsonNode answerSubjectNode = d.getKeyAsNode().get(4); - final boolean successfulFreeTextAnswer = d.getKeyAsNode().get(5).asBoolean(); - a.setAnswerText(answerTextNode.isNull() ? null : answerTextNode.asText()); - a.setAnswerSubject(answerSubjectNode.isNull() ? null : answerSubjectNode.asText()); - a.setSuccessfulFreeTextAnswer(successfulFreeTextAnswer); - answers.add(a); + if (d.getKeyAsNode().get(2).size() == 0) { + /* Abstentions */ + roundStats.setAbstentionCount(d.getValueAsInt()); + } else { + /* Answers: + * Extract selected indexes from key[2] and count from value */ + final JsonNode jsonIndexes = d.getKeyAsNode().get(2); + Integer[] indexes = new Integer[jsonIndexes.size()]; + /* Count independently */ + for (int i = 0; i < jsonIndexes.size(); i++) { + indexes[i] = jsonIndexes.get(i).asInt(); + independentCounts.set(indexes[i], independentCounts.get(indexes[i]) + d.getValueAsInt()); + } + /* Count option combinations */ + AnswerStatistics.RoundStatistics.Combination combination = + combinations.getOrDefault(Arrays.asList(indexes), + new AnswerStatistics.RoundStatistics.Combination( + Arrays.asList(indexes), d.getValueAsInt())); + combinations.put(Arrays.asList(indexes), combination); + roundStats.setCombinatedCounts(combinations.values()); + } } + roundStats.setIndependentCounts(independentCounts); + /* TODO: Review - might lead easily to IndexOutOfBoundsExceptions - use a Map instead? */ + List<AnswerStatistics.RoundStatistics> roundStatisticsList = new ArrayList(Collections.nCopies(round, null)); + roundStatisticsList.set(round - 1, roundStats); + stats.setRoundStatistics(roundStatisticsList); - return answers; + return stats; } @Override public int countByContentId(final String contentId) { - final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + final ViewResult result = db.queryView(createQuery("by_contentid_round_body_subject") .reduce(true) .startKey(ComplexKey.of(contentId)) .endKey(ComplexKey.of(contentId, ComplexKey.emptyObject()))); @@ -139,7 +138,7 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple @Override public int countByContentIdRound(final String contentId, final int round) { - final ViewResult result = db.queryView(createQuery("by_questionid_piround_text_subject") + final ViewResult result = db.queryView(createQuery("by_contentid_round_body_subject") .reduce(true) .startKey(ComplexKey.of(contentId, round)) .endKey(ComplexKey.of(contentId, round, ComplexKey.emptyObject()))); @@ -148,46 +147,48 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple } @Override - public List<Answer> findByContentId(final String contentId, final int start, final int limit) { + public <T extends Answer> List<T> findByContentId(final String contentId, final Class<T> type, final int start, final int limit) { final int qSkip = start > 0 ? start : -1; final int qLimit = limit > 0 ? limit : -1; - final List<Answer> answers = db.queryView(createQuery("by_questionid_timestamp") + final List<T> answers = db.queryView(createQuery("by_contentid_creationtimestamp") .skip(qSkip) .limit(qLimit) - //.includeDocs(true) - .startKey(ComplexKey.of(contentId)) - .endKey(ComplexKey.of(contentId, ComplexKey.emptyObject())) + .includeDocs(true) + .startKey(ComplexKey.of(contentId, ComplexKey.emptyObject())) + .endKey(ComplexKey.of(contentId)) .descending(true), - Answer.class); + type); return answers; } @Override - public List<Answer> findByUserSessionId(final User user, final String sessionId) { - return queryView("by_user_sessionid", ComplexKey.of(user.getUsername(), sessionId)); + public List<Answer> findByUserRoomId(final ClientAuthentication user, final String roomId) { + return queryView("by_creatorid_roomid", ComplexKey.of(user.getId(), roomId)); } @Override - public int countBySessionKey(final String sessionKey) { - final ViewResult result = db.queryView(createQuery("by_sessionid_variant").key(sessionKey)); + public int countByRoomId(final String roomId) { + final ViewResult result = db.queryView(createQuery("by_roomid") + .key(roomId) + .reduce(true)); return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); } @Override - public int countBySessionIdLectureVariant(final String sessionId) { - return countBySessionIdVariant(sessionId, "lecture"); + public int countByRoomIdOnlyLectureVariant(final String roomId) { + return countBySessionIdVariant(roomId, "lecture"); } @Override - public int countBySessionIdPreparationVariant(final String sessionId) { - return countBySessionIdVariant(sessionId, "preparation"); + public int countByRoomIdOnlyPreparationVariant(final String roomId) { + return countBySessionIdVariant(roomId, "preparation"); } private int countBySessionIdVariant(final String sessionId, final String variant) { - final ViewResult result = db.queryView(createQuery("by_sessionid_variant") + final ViewResult result = db.queryView(createQuery("by_roomid_variant") .key(ComplexKey.of(sessionId, variant))); return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); @@ -195,7 +196,7 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple @Override public int deleteAllAnswersForQuestions(final List<String> contentIds) { - final ViewResult result = db.queryView(createQuery("by_questionid") + final ViewResult result = db.queryView(createQuery("by_contentid") .keys(contentIds)); final List<BulkDeleteDocument> allAnswers = new ArrayList<>(); for (final ViewResult.Row a : result.getRows()) { @@ -215,7 +216,7 @@ public class CouchDbAnswerRepository extends CouchDbCrudRepository<Answer> imple @Override public int deleteByContentIds(final List<String> contentIds) { - final ViewResult result = db.queryView(createQuery("by_questionid") + final ViewResult result = db.queryView(createQuery("by_contentid") .keys(contentIds)); final List<BulkDeleteDocument> deleteDocs = new ArrayList<>(); for (final ViewResult.Row a : result.getRows()) { diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAttachmentRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAttachmentRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..409b9170da01da87752f9f3019fb626e3eb0385b --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbAttachmentRepository.java @@ -0,0 +1,11 @@ +package de.thm.arsnova.persistence.couchdb; + +import de.thm.arsnova.model.Attachment; +import de.thm.arsnova.persistence.AttachmentRepository; +import org.ektorp.CouchDbConnector; + +public class CouchDbAttachmentRepository extends CouchDbCrudRepository<Attachment> implements AttachmentRepository { + public CouchDbAttachmentRepository(final CouchDbConnector db, final boolean createIfNotExists) { + super(Attachment.class, db, "by_id", createIfNotExists); + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCommentRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCommentRepository.java similarity index 61% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCommentRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCommentRepository.java index 6276c44fbed535d262a3080bc229c45a4be69444..8b353d242c545e91e57b40a09d5539293048070c 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCommentRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCommentRepository.java @@ -1,11 +1,11 @@ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; import com.fasterxml.jackson.databind.JsonNode; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.CommentReadingCount; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.persistance.CommentRepository; -import de.thm.arsnova.persistance.LogEntryRepository; +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.migration.v2.CommentReadingCount; +import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.LogEntryRepository; import org.ektorp.ComplexKey; import org.ektorp.CouchDbConnector; import org.ektorp.UpdateConflictException; @@ -23,12 +23,12 @@ public class CouchDbCommentRepository extends CouchDbCrudRepository<Comment> imp private LogEntryRepository dbLogger; public CouchDbCommentRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(Comment.class, db, "by_sessionid", createIfNotExists); + super(Comment.class, db, "by_id", createIfNotExists); } @Override - public int countBySessionId(final String sessionId) { - final ViewResult result = db.queryView(createQuery("by_sessionid") + public int countByRoomId(final String sessionId) { + final ViewResult result = db.queryView(createQuery("by_roomid") .key(sessionId) .reduce(true) .group(true)); @@ -40,20 +40,20 @@ public class CouchDbCommentRepository extends CouchDbCrudRepository<Comment> imp } @Override - public CommentReadingCount countReadingBySessionId(final String sessionId) { - final ViewResult result = db.queryView(createQuery("by_sessionid_read") - .startKey(ComplexKey.of(sessionId)) - .endKey(ComplexKey.of(sessionId, ComplexKey.emptyObject())) + public CommentReadingCount countReadingByRoomId(final String roomId) { + final ViewResult result = db.queryView(createQuery("by_roomid_read") + .startKey(ComplexKey.of(roomId)) + .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) .reduce(true) .group(true)); return calculateReadingCount(result); } @Override - public CommentReadingCount countReadingBySessionIdAndUser(final String sessionId, final User user) { - final ViewResult result = db.queryView(createQuery("by_sessionid_creator_read") - .startKey(ComplexKey.of(sessionId, user.getUsername())) - .endKey(ComplexKey.of(sessionId, user.getUsername(), ComplexKey.emptyObject())) + public CommentReadingCount countReadingByRoomIdAndUser(final String roomId, final ClientAuthentication user) { + final ViewResult result = db.queryView(createQuery("by_roomid_creatorid_read") + .startKey(ComplexKey.of(roomId, user.getId())) + .endKey(ComplexKey.of(roomId, user.getId(), ComplexKey.emptyObject())) .reduce(true) .group(true)); return calculateReadingCount(result); @@ -103,57 +103,57 @@ public class CouchDbCommentRepository extends CouchDbCrudRepository<Comment> imp } @Override - public List<Comment> findBySessionId(final String sessionId, final int start, final int limit) { + public List<Comment> findByRoomId(final String roomId, final int start, final int limit) { final int qSkip = start > 0 ? start : -1; final int qLimit = limit > 0 ? limit : -1; - final List<Comment> comments = db.queryView(createQuery("by_sessionid_timestamp") + final List<Comment> comments = db.queryView(createQuery("by_roomid_creationtimestamp") .skip(qSkip) .limit(qLimit) .descending(true) - .startKey(ComplexKey.of(sessionId, ComplexKey.emptyObject())) - .endKey(ComplexKey.of(sessionId)) + .startKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) + .endKey(ComplexKey.of(roomId)) .includeDocs(true), Comment.class); // for (Comment comment : comments) { -// comment.setSessionId(session.getKeyword()); +// comment.setRoomId(session.getKeyword()); // } return comments; } @Override - public List<Comment> findBySessionIdAndUser(final String sessionId, final User user, final int start, final int limit) { + public List<Comment> findByRoomIdAndUser(final String roomId, final ClientAuthentication user, final int start, final int limit) { final int qSkip = start > 0 ? start : -1; final int qLimit = limit > 0 ? limit : -1; - final List<Comment> comments = db.queryView(createQuery("by_sessionid_creator_timestamp") + final List<Comment> comments = db.queryView(createQuery("by_roomid_creatorid_creationtimestamp") .skip(qSkip) .limit(qLimit) .descending(true) - .startKey(ComplexKey.of(sessionId, user.getUsername(), ComplexKey.emptyObject())) - .endKey(ComplexKey.of(sessionId, user.getUsername())) + .startKey(ComplexKey.of(roomId, user.getUsername(), ComplexKey.emptyObject())) + .endKey(ComplexKey.of(roomId, user.getUsername())) .includeDocs(true), Comment.class); // for (Comment comment : comments) { -// comment.setSessionId(session.getKeyword()); +// comment.setRoomId(session.getKeyword()); // } return comments; } @Override - public int deleteBySessionId(final String sessionId) { - final ViewResult result = db.queryView(createQuery("by_sessionid").key(sessionId)); + public int deleteByRoomId(final String roomId) { + final ViewResult result = db.queryView(createQuery("by_roomid").key(roomId)); return delete(result); } @Override - public int deleteBySessionIdAndUser(final String sessionId, final User user) { - final ViewResult result = db.queryView(createQuery("by_sessionid_creator_read") - .startKey(ComplexKey.of(sessionId, user.getUsername())) - .endKey(ComplexKey.of(sessionId, user.getUsername(), ComplexKey.emptyObject()))); + public int deleteByRoomIdAndUser(final String roomId, final ClientAuthentication user) { + final ViewResult result = db.queryView(createQuery("by_roomid_creatorid_read") + .startKey(ComplexKey.of(roomId, user.getUsername())) + .endKey(ComplexKey.of(roomId, user.getUsername(), ComplexKey.emptyObject()))); return delete(result); } diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..5defab3aff88efcdcae75519b36795bb9d7931d6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbContentRepository.java @@ -0,0 +1,280 @@ +package de.thm.arsnova.persistence.couchdb; + +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.persistence.ContentRepository; +import de.thm.arsnova.persistence.LogEntryRepository; +import org.ektorp.BulkDeleteDocument; +import org.ektorp.ComplexKey; +import org.ektorp.CouchDbConnector; +import org.ektorp.DocumentOperationResult; +import org.ektorp.ViewResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class CouchDbContentRepository extends CouchDbCrudRepository<Content> implements ContentRepository { + private static final Logger logger = LoggerFactory.getLogger(CouchDbContentRepository.class); + + @Autowired + private LogEntryRepository dbLogger; + + public CouchDbContentRepository(final CouchDbConnector db, final boolean createIfNotExists) { + super(Content.class, db, "by_id", createIfNotExists); + } + + @Override + public List<Content> findByRoomIdForUsers(final String roomId) { + final List<Content> contents = new ArrayList<>(); + final List<Content> questions1 = findByRoomIdAndVariantAndActive(roomId, "lecture", true); + final List<Content> questions2 = findByRoomIdAndVariantAndActive(roomId, "preparation", true); + final List<Content> questions3 = findByRoomIdAndVariantAndActive(roomId, "flashcard", true); + contents.addAll(questions1); + contents.addAll(questions2); + contents.addAll(questions3); + + return contents; + } + + @Override + public List<Content> findByRoomIdForSpeaker(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId); + } + + @Override + public int countByRoomId(final String roomId) { + final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId)) + .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) + .reduce(true)); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + @Override + public List<String> findIdsByRoomId(final String roomId) { + return collectQuestionIds(db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId)) + .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) + .reduce(false))); + } + + @Override + public List<String> findIdsByRoomIdAndVariant(final String roomId, final String variant) { + return collectQuestionIds(db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId, variant)) + .endKey(ComplexKey.of(roomId, variant, ComplexKey.emptyObject())) + .reduce(false))); + } + + @Override + public int deleteByRoomId(final String roomId) { + final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId)) + .endKey(ComplexKey.of(roomId, ComplexKey.emptyObject())) + .reduce(false)); + + final List<BulkDeleteDocument> deleteDocs = new ArrayList<>(); + for (final ViewResult.Row a : result.getRows()) { + final BulkDeleteDocument d = new BulkDeleteDocument(a.getId(), a.getValueAsNode().get("_rev").asText()); + deleteDocs.add(d); + } + List<DocumentOperationResult> errors = db.executeBulk(deleteDocs); + + return deleteDocs.size() - errors.size(); + } + + @Override + public List<String> findUnansweredIdsByRoomIdAndUser(final String roomId, final ClientAuthentication user) { + final ViewResult result = db.queryView(createQuery("contentid_by_creatorid_roomid_variant") + .designDocId("_design/Answer") + .startKey(ComplexKey.of(user.getId(), roomId)) + .endKey(ComplexKey.of(user.getUsername(), roomId, ComplexKey.emptyObject()))); + final List<String> answeredIds = new ArrayList<>(); + for (final ViewResult.Row row : result.getRows()) { + answeredIds.add(row.getId()); + } + return collectUnansweredQuestionIds(findIdsByRoomId(roomId), answeredIds); + } + + @Override + public List<String> findUnansweredIdsByRoomIdAndUserOnlyLectureVariant(final String roomId, final ClientAuthentication user) { + final ViewResult result = db.queryView(createQuery("contentid_round_by_creatorid_roomid_variant") + .designDocId("_design/Answer") + .key(ComplexKey.of(user.getId(), roomId, "lecture"))); + final Map<String, Integer> answeredQuestions = new HashMap<>(); + for (final ViewResult.Row row : result.getRows()) { + answeredQuestions.put(row.getId(), row.getKeyAsNode().get(2).asInt()); + } + + return collectUnansweredQuestionIdsByPiRound(findByRoomIdOnlyLectureVariantAndActive(roomId), answeredQuestions); + } + + @Override + public List<String> findUnansweredIdsByRoomIdAndUserOnlyPreparationVariant(final String roomId, final ClientAuthentication user) { + final ViewResult result = db.queryView(createQuery("contentid_round_by_creatorid_roomid_variant") + .designDocId("_design/Answer") + .key(ComplexKey.of(user.getId(), roomId, "preparation"))); + final Map<String, Integer> answeredQuestions = new HashMap<>(); + for (final ViewResult.Row row : result.getRows()) { + answeredQuestions.put(row.getId(), row.getKeyAsNode().get(2).asInt()); + } + + return collectUnansweredQuestionIdsByPiRound(findByRoomIdOnlyPreparationVariantAndActive(roomId), answeredQuestions); + } + + @Override + public List<Content> findByRoomIdOnlyLectureVariantAndActive(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId, "lecture", true); + } + + @Override + public List<Content> findByRoomIdOnlyLectureVariant(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId, "lecture"); + } + + @Override + public List<Content> findByRoomIdOnlyFlashcardVariantAndActive(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId, "flashcard", true); + } + + @Override + public List<Content> findByRoomIdOnlyFlashcardVariant(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId, "flashcard"); + } + + @Override + public List<Content> findByRoomIdOnlyPreparationVariantAndActive(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId, "preparation", true); + } + + @Override + public List<Content> findByRoomIdOnlyPreparationVariant(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId, "preparation"); + } + + @Override + public List<Content> findByRoomId(final String roomId) { + return findByRoomIdAndVariantAndActive(roomId); + } + + @Override + public List<Content> findByRoomIdAndVariantAndActive(final Object... keys) { + final Object[] endKeys = Arrays.copyOf(keys, keys.length + 1); + endKeys[keys.length] = ComplexKey.emptyObject(); + + return db.queryView(createQuery("by_roomid_group_locked") + .includeDocs(true) + .reduce(false) + .startKey(ComplexKey.of(keys)) + .endKey(ComplexKey.of(endKeys)), + Content.class); + } + + @Override + public int countLectureVariantByRoomId(final String roomId) { + /* TODO: reduce code duplication */ + final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId, "lecture")) + .endKey(ComplexKey.of(roomId, "lecture", ComplexKey.emptyObject()))); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + @Override + public int countFlashcardVariantRoomId(final String roomId) { + /* TODO: reduce code duplication */ + final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId, "flashcard")) + .endKey(ComplexKey.of(roomId, "flashcard", ComplexKey.emptyObject()))); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + @Override + public int countPreparationVariantByRoomId(final String roomId) { + /* TODO: reduce code duplication */ + final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId, "preparation")) + .endKey(ComplexKey.of(roomId, "preparation", ComplexKey.emptyObject()))); + + return result.isEmpty() ? 0 : result.getRows().get(0).getValueAsInt(); + } + + private List<String> collectUnansweredQuestionIds( + final List<String> contentIds, + final List<String> answeredContentIds + ) { + final List<String> unanswered = new ArrayList<>(); + for (final String contentId : contentIds) { + if (!answeredContentIds.contains(contentId)) { + unanswered.add(contentId); + } + } + return unanswered; + } + + private List<String> collectUnansweredQuestionIdsByPiRound( + final List<Content> contents, + final Map<String, Integer> answeredQuestions + ) { + final List<String> unanswered = new ArrayList<>(); + + for (final Content content : contents) { + if (!"slide".equals(content.getFormat()) && (!answeredQuestions.containsKey(content.getId()) + || (answeredQuestions.containsKey(content.getId()) && answeredQuestions.get(content.getId()) != content.getState().getRound()))) { + unanswered.add(content.getId()); + } + } + + return unanswered; + } + + private List<String> collectQuestionIds(final ViewResult viewResult) { + final List<String> ids = new ArrayList<>(); + for (final ViewResult.Row row : viewResult.getRows()) { + ids.add(row.getId()); + } + return ids; + } + + /* TODO: remove if this method is no longer used */ + @Override + public List<String> findIdsByRoomIdAndVariantAndSubject(final String roomId, final String questionVariant, final String subject) { + final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId, questionVariant, false, subject)) + .endKey(ComplexKey.of(roomId, questionVariant, false, subject, ComplexKey.emptyObject()))); + + final List<String> qids = new ArrayList<>(); + + for (final ViewResult.Row row : result.getRows()) { + final String s = row.getId(); + qids.add(s); + } + + return qids; + } + + @Override + public List<String> findSubjectsByRoomIdAndVariant(final String roomId, final String questionVariant) { + final ViewResult result = db.queryView(createQuery("by_roomid_group_locked") + .startKey(ComplexKey.of(roomId, questionVariant)) + .endKey(ComplexKey.of(roomId, questionVariant, ComplexKey.emptyObject()))); + + final Set<String> uniqueSubjects = new HashSet<>(); + + for (final ViewResult.Row row : result.getRows()) { + uniqueSubjects.add(row.getKeyAsNode().get(3).asText()); + } + + return new ArrayList<>(uniqueSubjects); + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCrudRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCrudRepository.java similarity index 84% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCrudRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCrudRepository.java index 8c745c65654d9e4bb4d43ee935e4d31c32338b90..39285cd0dc1749133f9cc0694618349f31b9ab4b 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbCrudRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbCrudRepository.java @@ -1,15 +1,16 @@ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; -import de.thm.arsnova.entities.Entity; +import de.thm.arsnova.model.Entity; +import de.thm.arsnova.persistence.CrudRepository; import org.ektorp.BulkDeleteDocument; import org.ektorp.CouchDbConnector; import org.ektorp.support.CouchDbRepositorySupport; -import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.NoRepositoryBean; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.stream.Collectors; @NoRepositoryBean @@ -52,7 +53,7 @@ abstract class CouchDbCrudRepository<T extends Entity> extends CouchDbRepository } @Override - public <S extends T> Iterable<S> save(final Iterable<S> entities) { + public <S extends T> Iterable<S> saveAll(final Iterable<S> entities) { if (!(entities instanceof Collection)) { throw new IllegalArgumentException("Implementation only supports Collections."); } @@ -61,13 +62,18 @@ abstract class CouchDbCrudRepository<T extends Entity> extends CouchDbRepository return entities; } + @Override + public Optional<T> findById(final String id) { + return Optional.ofNullable(get(id)); + } + @Override public T findOne(final String id) { return get(id); } @Override - public boolean exists(final String id) { + public boolean existsById(final String id) { return contains(id); } @@ -77,7 +83,7 @@ abstract class CouchDbCrudRepository<T extends Entity> extends CouchDbRepository } @Override - public Iterable<T> findAll(final Iterable<String> strings) { + public Iterable<T> findAllById(final Iterable<String> strings) { if (!(strings instanceof Collection)) { throw new IllegalArgumentException("Implementation only supports Collections."); } @@ -97,7 +103,7 @@ abstract class CouchDbCrudRepository<T extends Entity> extends CouchDbRepository } @Override - public void delete(final String id) { + public void deleteById(final String id) { T entity = get(id); db.delete(id, entity.getRevision()); } @@ -108,7 +114,7 @@ abstract class CouchDbCrudRepository<T extends Entity> extends CouchDbRepository } @Override - public void delete(final Iterable<? extends T> entities) { + public void deleteAll(final Iterable<? extends T> entities) { if (!(entities instanceof Collection)) { throw new IllegalArgumentException("Implementation only supports Collections."); } diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbInitializer.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbInitializer.java new file mode 100644 index 0000000000000000000000000000000000000000..697ffc06d95fcda5e40c9e37b94221adb01009b5 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbInitializer.java @@ -0,0 +1,158 @@ +package de.thm.arsnova.persistence.couchdb; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.thm.arsnova.model.MigrationState; +import de.thm.arsnova.persistence.couchdb.migrations.MigrationExecutor; +import de.thm.arsnova.service.StatusService; +import org.ektorp.CouchDbConnector; +import org.ektorp.DbAccessException; +import org.ektorp.DocumentNotFoundException; +import org.ektorp.impl.ObjectMapperFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.stereotype.Component; +import org.springframework.util.FileCopyUtils; + +import javax.annotation.PostConstruct; +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Component +public class CouchDbInitializer implements ResourceLoaderAware { + private static final Logger logger = LoggerFactory.getLogger(CouchDbInitializer.class); + private final List<Bindings> docs = new ArrayList<>(); + + private ResourceLoader resourceLoader; + private MigrationExecutor migrationExecutor; + private CouchDbConnector connector; + private ObjectMapper objectMapper; + private StatusService statusService; + private boolean migrationStarted = false; + + public CouchDbInitializer(final CouchDbConnector couchDbConnector, final ObjectMapperFactory objectMapperFactory, + final StatusService statusService) { + connector = couchDbConnector; + objectMapper = objectMapperFactory.createObjectMapper(couchDbConnector); + this.statusService = statusService; + } + + protected void loadDesignDocFiles() throws IOException, ScriptException { + final ScriptEngine engine = new ScriptEngineManager().getEngineByMimeType("application/javascript"); + engine.eval(new InputStreamReader(new ClassPathResource("couchdb/jsToJson.js").getInputStream())); + + final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); + final Resource[] resources = resolver.getResources("classpath:couchdb/*.design.js"); + for (Resource resource : resources) { + logger.debug("Loading CouchDB design doc: {}", resource.getFilename()); + final String js = FileCopyUtils.copyToString(new InputStreamReader(resource.getInputStream())); + /* Reset designDoc before parsing a new one. */ + engine.eval("var designDoc = null;" + js); + final Bindings jsonObject = (Bindings) engine.eval("jsToJson(designDoc)"); + docs.add(jsonObject); + } + } + + protected void createDesignDocs() { + connector.executeBulk(docs.stream().filter(doc -> { + try { + if (logger.isDebugEnabled()) { + logger.debug("Checking design doc {}:\n{}", doc.get("_id"), objectMapper.writeValueAsString(doc)); + } + final Map<String, Object> existingDoc = connector.get(HashMap.class, doc.get("_id").toString()); + final String existingViews = objectMapper.writeValueAsString(existingDoc.get("views")); + final String currentViews = objectMapper.writeValueAsString(doc.get("views")); + if (existingViews.equals(currentViews)) { + logger.debug("Design doc {} already exists.", doc.get("_id")); + return false; + } else { + logger.debug("Design doc {} will be updated.", doc.get("_id")); + doc.put("_rev", existingDoc.get("_rev")); + return true; + } + } catch (final DocumentNotFoundException e) { + logger.debug("Design doc {} will be created.", doc.get("_id")); + return true; + } catch (JsonProcessingException e) { + logger.warn("Failed to serialize design doc {}.", doc.get("_id"), e); + return false; + } + }).collect(Collectors.toList())); + } + + private MigrationState checkMigrationState() { + MigrationState state; + try { + state = connector.get(MigrationState.class, MigrationState.ID); + } catch (DocumentNotFoundException e) { + logger.debug("No migration state found in database.", e); + if (connector.getDbInfo().getDocCount() > 0) { + /* TODO: use a custom exception */ + throw new DbAccessException("Database is not empty."); + } + state = new MigrationState(); + connector.create(state); + } + + return state; + } + + protected void migrate(final MigrationState state) { + if (migrationExecutor != null && migrationExecutor.runMigrations(state)) { + connector.update(state); + } + } + + @PostConstruct + private void init() { + statusService.putMaintenanceReason(this.getClass(), "Database not initialized"); + } + + @EventListener + private void onApplicationEvent(ContextRefreshedEvent event) throws IOException, ScriptException { + /* Event is triggered more than once */ + if (migrationStarted) { + return; + } + migrationStarted = true; + + try { + final MigrationState state = checkMigrationState(); + statusService.putMaintenanceReason(this.getClass(), "Data migration active"); + loadDesignDocFiles(); + createDesignDocs(); + migrate(state); + statusService.removeMaintenanceReason(this.getClass()); + } catch (DbAccessException e) { + logger.error("Database is invalid.", e); + statusService.putMaintenanceReason(this.getClass(), "Invalid database"); + } + } + + @Override + public void setResourceLoader(final ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Autowired + public void setMigrationExecutor(final MigrationExecutor migrationExecutor) { + this.migrationExecutor = migrationExecutor; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbLogEntryRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbLogEntryRepository.java similarity index 91% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbLogEntryRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbLogEntryRepository.java index 33abf56ec809e85c48c5b05e0104a7bfa5a7b813..1af116fa21434b770b276c1c8fbb495f2c817436 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbLogEntryRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbLogEntryRepository.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; -import de.thm.arsnova.entities.LogEntry; -import de.thm.arsnova.persistance.LogEntryRepository; +import de.thm.arsnova.model.migration.v2.LogEntry; +import de.thm.arsnova.persistence.LogEntryRepository; import org.ektorp.CouchDbConnector; import org.ektorp.support.CouchDbRepositorySupport; import org.slf4j.Logger; diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbMotdRepository.java similarity index 67% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbMotdRepository.java index d7c5317b9d981df8dbcae5a6684cdfe504fd9979..6d1e01fa896362793b4a26b0658455cece64ed09 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbMotdRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbMotdRepository.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; -import de.thm.arsnova.entities.Motd; -import de.thm.arsnova.persistance.MotdRepository; +import de.thm.arsnova.model.Motd; +import de.thm.arsnova.persistence.MotdRepository; import org.ektorp.CouchDbConnector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,29 +30,29 @@ public class CouchDbMotdRepository extends CouchDbCrudRepository<Motd> implement private static final Logger logger = LoggerFactory.getLogger(CouchDbMotdRepository.class); public CouchDbMotdRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(Motd.class, db, "by_sessionkey", createIfNotExists); + super(Motd.class, db, "by_id", createIfNotExists); } @Override public List<Motd> findGlobalForAdmin() { - return find("by_audience_for_global", null); + return find(null); } @Override public List<Motd> findGlobalForAll() { - return find("by_audience_for_global", "all"); + return find(Motd.Audience.ALL); } @Override public List<Motd> findGlobalForLoggedIn() { - return find("by_audience_for_global", "loggedIn"); + return find(Motd.Audience.AUTHENTICATED); } @Override public List<Motd> findGlobalForTutors() { final List<Motd> union = new ArrayList<>(); - union.addAll(find("by_audience_for_global", "loggedIn")); - union.addAll(find("by_audience_for_global", "tutors")); + union.addAll(find(Motd.Audience.AUTHENTICATED)); + union.addAll(find(Motd.Audience.AUTHORS)); return union; } @@ -60,25 +60,22 @@ public class CouchDbMotdRepository extends CouchDbCrudRepository<Motd> implement @Override public List<Motd> findForStudents() { final List<Motd> union = new ArrayList<>(); - union.addAll(find("by_audience_for_global", "loggedIn")); - union.addAll(find("by_audience_for_global", "students")); + union.addAll(find(Motd.Audience.AUTHENTICATED)); + union.addAll(find(Motd.Audience.PARTICIPANTS)); return union; } @Override - public List<Motd> findBySessionKey(final String sessionkey) { - return find("by_sessionkey", sessionkey); + public List<Motd> findByRoomId(final String roomId) { + return find("by_roomid", roomId); } - private List<Motd> find(final String viewName, final String key) { - return queryView(viewName, key); + private List<Motd> find(final Motd.Audience audience) { + return queryView("by_audience_for_global", audience.toString()); } - @Override - public Motd findByKey(final String key) { - final List<Motd> motd = queryView("by_motdkey", key); - - return motd.get(0); + private List<Motd> find(final String viewName, final String key) { + return queryView(viewName, key); } } diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbRoomRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbRoomRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..6a7c4e32ad95e32d12a41d0ad021b0b3004b2929 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbRoomRepository.java @@ -0,0 +1,475 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.persistence.couchdb; + +import de.thm.arsnova.connector.model.Course; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.RoomStatistics; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ImportExportContainer; +import de.thm.arsnova.persistence.LogEntryRepository; +import de.thm.arsnova.persistence.MotdRepository; +import de.thm.arsnova.persistence.RoomRepository; +import org.ektorp.ComplexKey; +import org.ektorp.CouchDbConnector; +import org.ektorp.ViewQuery; +import org.ektorp.ViewResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import java.io.IOException; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class CouchDbRoomRepository extends CouchDbCrudRepository<Room> implements RoomRepository { + private static final Logger logger = LoggerFactory.getLogger(CouchDbRoomRepository.class); + + @Autowired + private LogEntryRepository dbLogger; + + @Autowired + private MotdRepository motdRepository; + + public CouchDbRoomRepository(final CouchDbConnector db, final boolean createIfNotExists) { + super(Room.class, db, "by_id", createIfNotExists); + } + + @Override + public Room findByShortId(final String shortId) { + if (shortId == null) { + return null; + } + final List<Room> roomList = queryView("by_shortid", shortId); + + return !roomList.isEmpty() ? roomList.get(0) : null; + } + + /* TODO: Move to service layer. */ + private String getShortId(final String id) throws IOException { + final Room room = get(id); + if (room == null) { + logger.error("No room found for id {}.", id); + + return null; + } + + return room.getShortId(); + } + + @Override + public List<Room> findRoomsByCourses(final List<Course> courses) { + return queryView("by_courseid", + ComplexKey.of(courses.stream().map(Course::getId).collect(Collectors.toList()))); + } + + @Override + public List<Room> findInactiveGuestRoomsMetadata(final long lastActivityBefore) { + final ViewResult result = db.queryView( + createQuery("by_lastactivity_for_guests").endKey(lastActivityBefore)); + final int[] count = new int[3]; + + List<Room> rooms = new ArrayList<>(); + for (final ViewResult.Row row : result.getRows()) { + final Room s = new Room(); + s.setId(row.getId()); + s.setRevision(row.getValueAsNode().get("_rev").asText()); + rooms.add(s); + } + + return rooms; + } + + /* TODO: Move to service layer. */ + @Override + public Room importRoom(final ClientAuthentication user, final ImportExportContainer importRoom) { + /* FIXME: not yet migrated - move to service layer */ + throw new UnsupportedOperationException(); +// final Room session = this.saveSession(user, importRoom.generateSessionEntity(user)); +// final List<Document> questions = new ArrayList<>(); +// // We need to remember which answers belong to which question. +// // The answers need a questionId, so we first store the questions to get the IDs. +// // Then we update the answer objects and store them as well. +// final Map<Document, ImportExportContainer.ImportExportContent> mapping = new HashMap<>(); +// // Later, generate all answer documents +// List<Document> answers = new ArrayList<>(); +// // We can then push answers together with comments in one large bulk request +// List<Document> interposedQuestions = new ArrayList<>(); +// // Motds shouldn't be forgotten, too +// List<Document> motds = new ArrayList<>(); +// try { +// // add session id to all questions and generate documents +// for (final ImportExportContainer.ImportExportContent question : importRoom.getQuestions()) { +// final Document doc = toQuestionDocument(session, question); +// question.setRoomId(session.getId()); +// questions.add(doc); +// mapping.put(doc, question); +// } +// database.bulkSaveDocuments(questions.toArray(new Document[questions.size()])); +// +// // bulk import answers together with interposed questions +// for (Map.Entry<Document, ImportExportContainer.ImportExportContent> entry : mapping.entrySet()) { +// final Document doc = entry.getKey(); +// final ImportExportContainer.ImportExportContent question = entry.getValue(); +// question.setId(doc.getId()); +// question.setRevision(doc.getRev()); +// for (final de.thm.arsnova.entities.transport.Answer answer : question.getAnswers()) { +// final Answer a = answer.generateAnswerEntity(user, question); +// final Document answerDoc = new Document(); +// answerDoc.put("type", "skill_question_answer"); +// answerDoc.put("sessionId", a.getRoomId()); +// answerDoc.put("questionId", a.getContentId()); +// answerDoc.put("answerSubject", a.getAnswerSubject()); +// answerDoc.put("questionVariant", a.getGroups()); +// answerDoc.put("questionValue", a.getQuestionValue()); +// answerDoc.put("answerText", a.getAnswerText()); +// answerDoc.put("answerTextRaw", a.getAnswerTextRaw()); +// answerDoc.put("timestamp", a.getTimestamp()); +// answerDoc.put("piRound", a.getPiRound()); +// answerDoc.put("abstention", a.isAbstention()); +// answerDoc.put("successfulFreeTextAnswer", a.isSuccessfulFreeTextAnswer()); +// // we do not store the user's name +// answerDoc.put("user", ""); +// answers.add(answerDoc); +// } +// } +// for (final de.thm.arsnova.entities.transport.Comment i : importRoom.getFeedbackQuestions()) { +// final Document q = new Document(); +// q.put("type", "interposed_question"); +// q.put("sessionId", session.getId()); +// q.put("subject", i.getSubject()); +// q.put("text", i.getText()); +// q.put("timestamp", i.getTimestamp()); +// q.put("read", i.isRead()); +// // we do not store the creator's name +// q.put("creator", ""); +// interposedQuestions.add(q); +// } +// for (final Motd m : importRoom.getMotds()) { +// final Document d = new Document(); +// d.put("type", "motd"); +// d.put("motdkey", m.getMotdkey()); +// d.put("title", m.getTitle()); +// d.put("text", m.getText()); +// d.put("audience", m.getAudience()); +// d.put("sessionkey", session.getKeyword()); +// d.put("startdate", String.valueOf(m.getStartDate().getTime())); +// d.put("enddate", String.valueOf(m.getEndDate().getTime())); +// motds.add(d); +// } +// final List<Document> documents = new ArrayList<>(answers); +// database.bulkSaveDocuments(interposedQuestions.toArray(new Document[interposedQuestions.size()])); +// database.bulkSaveDocuments(motds.toArray(new Document[motds.size()])); +// database.bulkSaveDocuments(documents.toArray(new Document[documents.size()])); +// } catch (final IOException e) { +// logger.error("Could not import session.", e); +// // Something went wrong, delete this session since we do not want a partial import +// this.delete(session); +// return null; +// } +// return this.calculateSessionInfo(importRoom, session); + } + + /* TODO: Move to service layer. */ + @Override + public ImportExportContainer exportRoom( + final String id, + final Boolean withAnswers, + final Boolean withFeedbackQuestions) { + /* FIXME: not yet migrated - move to service layer */ + throw new UnsupportedOperationException(); +// final ImportExportContainer importExportSession = new ImportExportContainer(); +// final Room session = getDatabaseDao().getSessionFromKeyword(sessionkey); +// importExportSession.setSessionFromSessionObject(session); +// final List<Content> questionList = getDatabaseDao().getAllSkillQuestions(session); +// for (final Content question : questionList) { +// final List<de.thm.arsnova.entities.transport.Answer> answerList = new ArrayList<>(); +// if (withAnswers) { +// for (final Answer a : this.getDatabaseDao().getAllAnswers(question)) { +// final de.thm.arsnova.entities.transport.Answer transportAnswer = new de.thm.arsnova.entities.transport.Answer(a); +// answerList.add(transportAnswer); +// } +// // getAllAnswers does not grep for whole answer object so i need to add empty entries for abstentions +// int i = this.getDatabaseDao().getAbstentionAnswerCount(question.getId()); +// for (int b = 0; b < i; b++) { +// final de.thm.arsnova.entities.transport.Answer ans = new de.thm.arsnova.entities.transport.Answer(); +// ans.setAnswerSubject(""); +// ans.setAnswerImage(""); +// ans.setAnswerText(""); +// ans.setAbstention(true); +// answerList.add(ans); +// } +// } +// importExportSession.addQuestionWithAnswers(question, answerList); +// } +// if (withFeedbackQuestions) { +// final List<de.thm.arsnova.entities.transport.Comment> interposedQuestionList = new ArrayList<>(); +// for (final Comment i : getDatabaseDao().getInterposedQuestions(session, 0, 0)) { +// de.thm.arsnova.entities.transport.Comment transportInterposedQuestion = new de.thm.arsnova.entities.transport.Comment(i); +// interposedQuestionList.add(transportInterposedQuestion); +// } +// importExportSession.setFeedbackQuestions(interposedQuestionList); +// } +// if (withAnswers) { +// importExportSession.setSessionInfo(this.calculateSessionInfo(importExportSession, session)); +// } +// importExportSession.setMotds(motdRepository.getMotdsForSession(session.getKeyword())); +// return importExportSession; + } + + /* TODO: Move to service layer. */ + private Room calculateSessionInfo(final ImportExportContainer importExportSession, final Room room) { + /* FIXME: not yet migrated - move to service layer */ + throw new UnsupportedOperationException(); +// int unreadComments = 0; +// int numUnanswered = 0; +// int numAnswers = 0; +// for (Comment i : importExportSession.getFeedbackQuestions()) { +// if (!i.isRead()) { +// unreadComments++; +// } +// } +// for (ImportExportContainer.ImportExportContent question : importExportSession.getQuestions()) { +// numAnswers += question.getAnswers().size(); +// if (question.getAnswers().isEmpty()) { +// numUnanswered++; +// } +// } +// RoomStatistics stats = new RoomStatistics(); +// stats.setContentCount(importExportSession.getQuestions().size()); +// stats.setAnswerCount(numAnswers); +// stats.setUnreadAnswerCount(numUnanswered); +// stats.setCommentCount(importExportSession.getFeedbackQuestions().size()); +// stats.setUnreadCommentCount(unreadComments); +// +// return room; + } + + @Override + public List<Room> findByOwner(final ClientAuthentication owner, final int start, final int limit) { + return findByOwnerId(owner.getId(), start, limit); + } + + @Override + public List<Room> findByOwnerId(final String ownerId, final int start, final int limit) { + final int qSkip = start > 0 ? start : -1; + final int qLimit = limit > 0 ? limit : -1; + + /* TODO: Only load IDs and check against cache for data. */ + return db.queryView( + createQuery("partial_by_pool_ownerid_name") + .skip(qSkip) + .limit(qLimit) + .startKey(ComplexKey.of(false, ownerId)) + .endKey(ComplexKey.of(false, ownerId, ComplexKey.emptyObject())) + .includeDocs(true), + Room.class); + } + + @Override + public List<String> findIdsByOwnerId(final String ownerId) { + ViewResult result = db.queryView(createQuery("by_ownerid") + .key(ownerId) + .includeDocs(false)); + + return result.getRows().stream().map(ViewResult.Row::getId).collect(Collectors.toList()); + } + + @Override + public List<Room> findAllForPublicPool() { + // TODO replace with new view + return queryView("partial_by_category_name_for_pool"); + } + + @Override + public List<Room> findInfosForPublicPool() { + final List<Room> rooms = this.findAllForPublicPool(); + return attachStatsForRooms(rooms); + } + + @Override + public List<Room> findForPublicPoolByOwner(final ClientAuthentication owner) { + /* TODO: Only load IDs and check against cache for data. */ + return db.queryView( + createQuery("partial_by_pool_ownerid_name") + .startKey(ComplexKey.of(true, owner.getId())) + .endKey(ComplexKey.of(true, owner.getId(), ComplexKey.emptyObject())) + .includeDocs(true), + Room.class); + } + + /* TODO: Move to service layer. */ + @Override + public List<Room> findInfosForPublicPoolByOwner(final ClientAuthentication owner) { + final List<Room> rooms = this.findForPublicPoolByOwner(owner); + if (rooms.isEmpty()) { + return new ArrayList<>(); + } + return attachStatsForRooms(rooms); + } + + /* TODO: Move to service layer. */ + @Override + public List<Room> getRoomsWithStatsForOwner(final ClientAuthentication owner, final int start, final int limit) { + final List<Room> rooms = this.findByOwner(owner, start, limit); + if (rooms.isEmpty()) { + return new ArrayList<>(); + } + return attachStatsForRooms(rooms); + } + + /* TODO: Move to service layer. */ + private List<Room> attachStatsForRooms(final List<Room> rooms) { + final List<String> roomIds = rooms.stream().map(Room::getId).collect(Collectors.toList()); + final ViewQuery questionCountView = createQuery("by_roomid").designDocId("_design/Content") + .group(true).keys(roomIds); + final ViewQuery answerCountView = createQuery("by_roomid").designDocId("_design/Answer") + .group(true).keys(roomIds); + final ViewQuery commentCountView = createQuery("by_roomid").designDocId("_design/Comment") + .group(true).keys(roomIds); + final ViewQuery unreadCommentCountView = createQuery("by_roomid_read").designDocId("_design/Comment") + .group(true).keys(rooms.stream().map(session -> ComplexKey.of(session.getId(), false)).collect(Collectors.toList())); + + return attachStats(rooms, questionCountView, answerCountView, commentCountView, unreadCommentCountView); + } + + /* TODO: Move to service layer. */ + public List<Room> getRoomHistoryWithStatsForUser(final List<Room> rooms, final ClientAuthentication user) { + final ViewQuery answeredQuestionsView = createQuery("by_creatorid_roomid").designDocId("_design/Answer") + .reduce(false).keys(rooms.stream().map(room -> ComplexKey.of(user.getId(), room.getId())).collect(Collectors.toList())); + final ViewQuery contentIdsView = createQuery("by_roomid").designDocId("_design/Content") + .reduce(false).keys(rooms.stream().map(Room::getId).collect(Collectors.toList())); + + return attachRoomHistoryStats(rooms, answeredQuestionsView, contentIdsView); + } + + /* TODO: Move to service layer. */ + private List<Room> attachRoomHistoryStats( + final List<Room> rooms, + final ViewQuery answeredQuestionsView, + final ViewQuery contentIdsView) { + final Map<String, Set<String>> answeredQuestionsMap = new HashMap<>(); + final Map<String, Set<String>> contentIdMap = new HashMap<>(); + + // Maps a room ID to a set of question IDs of answered questions of that room + for (final ViewResult.Row row : db.queryView(answeredQuestionsView).getRows()) { + final String roomId = row.getKey(); + final String contentId = row.getValue(); + Set<String> contentIdsInRoom = answeredQuestionsMap.get(roomId); + if (contentIdsInRoom == null) { + contentIdsInRoom = new HashSet<>(); + } + contentIdsInRoom.add(contentId); + answeredQuestionsMap.put(roomId, contentIdsInRoom); + } + + // Maps a room ID to a set of question IDs of that room + for (final ViewResult.Row row : db.queryView(contentIdsView).getRows()) { + final String roomId = row.getKey(); + final String contentId = row.getId(); + Set<String> contentIdsInRoom = contentIdMap.get(roomId); + if (contentIdsInRoom == null) { + contentIdsInRoom = new HashSet<>(); + } + contentIdsInRoom.add(contentId); + contentIdMap.put(roomId, contentIdsInRoom); + } + + // For each room, count the question IDs that are not yet answered + final Map<String, Integer> unansweredQuestionsCountMap = new HashMap<>(); + for (final Room s : rooms) { + if (!contentIdMap.containsKey(s.getId())) { + continue; + } + // Note: create a copy of the first set so that we don't modify the contents in the original set + final Set<String> contentIdsInRoom = new HashSet<>(contentIdMap.get(s.getId())); + Set<String> answeredContentIdsInRoom = answeredQuestionsMap.get(s.getId()); + if (answeredContentIdsInRoom == null) { + answeredContentIdsInRoom = new HashSet<>(); + } + contentIdsInRoom.removeAll(answeredContentIdsInRoom); + unansweredQuestionsCountMap.put(s.getId(), contentIdsInRoom.size()); + } + + for (final Room room : rooms) { + int numUnanswered = 0; + + if (unansweredQuestionsCountMap.containsKey(room.getId())) { + numUnanswered = unansweredQuestionsCountMap.get(room.getId()); + } + RoomStatistics stats = new RoomStatistics(); + room.setStatistics(stats); + stats.setUnansweredContentCount(numUnanswered); + } + return rooms; + } + + /* TODO: Move to service layer. */ + private List<Room> attachStats( + final List<Room> rooms, + final ViewQuery questionCountView, + final ViewQuery answerCountView, + final ViewQuery commentCountView, + final ViewQuery unreadCommentCountView) { + final Map<String, Integer> questionCountMap = db.queryView(questionCountView).getRows() + .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + final Map<String, Integer> answerCountMap = db.queryView(answerCountView).getRows() + .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + final Map<String, Integer> commentCountMap = db.queryView(commentCountView).getRows() + .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + final Map<String, Integer> unreadCommentCountMap = db.queryView(unreadCommentCountView).getRows() + .stream().map(row -> new AbstractMap.SimpleImmutableEntry<>(row.getKey(), row.getValueAsInt())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + for (final Room room : rooms) { + int numQuestions = 0; + int numAnswers = 0; + int numComments = 0; + int numUnreadComments = 0; + if (questionCountMap.containsKey(room.getId())) { + numQuestions = questionCountMap.get(room.getId()); + } + if (answerCountMap.containsKey(room.getId())) { + numAnswers = answerCountMap.get(room.getId()); + } + if (commentCountMap.containsKey(room.getId())) { + numComments = commentCountMap.get(room.getId()); + } + if (unreadCommentCountMap.containsKey(room.getId())) { + numUnreadComments = unreadCommentCountMap.get(room.getId()); + } + + final RoomStatistics stats = new RoomStatistics(); + room.setStatistics(stats); + stats.setContentCount(numQuestions); + stats.setAnswerCount(numAnswers); + stats.setCommentCount(numComments); + stats.setUnreadCommentCount(numUnreadComments); + } + return rooms; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbSessionStatisticsRepository.java similarity index 78% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbSessionStatisticsRepository.java index e0c5f4927da1ad8b9ca33d3aaf6646411935698d..4a1b49c7e936debbc1ee2dfb28acad6710e778a9 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbSessionStatisticsRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbSessionStatisticsRepository.java @@ -1,9 +1,9 @@ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; import com.fasterxml.jackson.databind.JsonNode; -import de.thm.arsnova.services.score.Score; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.persistance.SessionStatisticsRepository; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.persistence.SessionStatisticsRepository; +import de.thm.arsnova.service.score.Score; import org.ektorp.ComplexKey; import org.ektorp.CouchDbConnector; import org.ektorp.ViewResult; @@ -15,13 +15,13 @@ public class CouchDbSessionStatisticsRepository extends CouchDbRepositorySupport } @Override - public Score getLearningProgress(final Session session) { + public Score getLearningProgress(final Room room) { final ViewResult maximumValueResult = db.queryView(createQuery("maximum_value_of_question") - .startKey(ComplexKey.of(session.getId())) - .endKey(ComplexKey.of(session.getId(), ComplexKey.emptyObject()))); + .startKey(ComplexKey.of(room.getId())) + .endKey(ComplexKey.of(room.getId(), ComplexKey.emptyObject()))); final ViewResult answerSumResult = db.queryView(createQuery("question_value_achieved_for_user") - .startKey(ComplexKey.of(session.getId())) - .endKey(ComplexKey.of(session.getId(), ComplexKey.emptyObject()))); + .startKey(ComplexKey.of(room.getId())) + .endKey(ComplexKey.of(room.getId(), ComplexKey.emptyObject()))); final Score courseScore = new Score(); // no results found diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbStatisticsRepository.java similarity index 95% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbStatisticsRepository.java index 3506f7da41bc4033de687df6536a96da4fb0187a..e45c3dc7e83f5bc47bd27ee1f6a510de566d995f 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbStatisticsRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbStatisticsRepository.java @@ -1,7 +1,7 @@ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; -import de.thm.arsnova.entities.Statistics; -import de.thm.arsnova.persistance.StatisticsRepository; +import de.thm.arsnova.model.Statistics; +import de.thm.arsnova.persistence.StatisticsRepository; import org.ektorp.CouchDbConnector; import org.ektorp.DbAccessException; import org.ektorp.ViewResult; diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbUserRepository.java b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbUserRepository.java similarity index 77% rename from src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbUserRepository.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbUserRepository.java index 414bebd9abc44140063180cecc1632f9b46960f2..f9cf3bcb74e3c97b58d390560c31a53edfa34863 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/CouchDbUserRepository.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/CouchDbUserRepository.java @@ -15,12 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.persistance.couchdb; +package de.thm.arsnova.persistence.couchdb; import com.google.common.collect.Lists; -import de.thm.arsnova.entities.DbUser; -import de.thm.arsnova.persistance.UserRepository; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.persistence.UserRepository; import org.ektorp.BulkDeleteDocument; +import org.ektorp.ComplexKey; import org.ektorp.CouchDbConnector; import org.ektorp.DbAccessException; import org.ektorp.DocumentOperationResult; @@ -32,13 +33,13 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; -public class CouchDbUserRepository extends CouchDbCrudRepository<DbUser> implements UserRepository { +public class CouchDbUserRepository extends CouchDbCrudRepository<UserProfile> implements UserRepository { private static final int BULK_PARTITION_SIZE = 500; private static final Logger logger = LoggerFactory.getLogger(CouchDbUserRepository.class); public CouchDbUserRepository(final CouchDbConnector db, final boolean createIfNotExists) { - super(DbUser.class, db, "by_username", createIfNotExists); + super(UserProfile.class, db, "by_id", createIfNotExists); } private void log(Object... strings) { @@ -46,14 +47,22 @@ public class CouchDbUserRepository extends CouchDbCrudRepository<DbUser> impleme } @Override - public DbUser findByUsername(final String username) { - final List<DbUser> users = queryView("by_username", username); + public UserProfile findByAuthProviderAndLoginId(final UserProfile.AuthProvider authProvider, final String loginId) { + final List<UserProfile> users = queryView("by_authprovider_loginid", + ComplexKey.of(authProvider.toString(), loginId)); return !users.isEmpty() ? users.get(0) : null; } @Override - public void delete(final DbUser user) { + public List<UserProfile> findByLoginId(final String loginId) { + final List<UserProfile> users = queryView("by_loginid", loginId); + + return users; + } + + @Override + public void delete(final UserProfile user) { if (db.delete(user) != null) { log("delete", "type", "user", "id", user.getId()); } else { @@ -64,7 +73,7 @@ public class CouchDbUserRepository extends CouchDbCrudRepository<DbUser> impleme @Override public int deleteInactiveUsers(final long lastActivityBefore) { - final ViewQuery q = createQuery("by_creation_for_inactive").endKey(lastActivityBefore); + final ViewQuery q = createQuery("by_creationtimestamp_for_inactive").endKey(lastActivityBefore); final List<ViewResult.Row> rows = db.queryView(q).getRows(); int count = 0; diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/MigrateFromLegacyCondition.java b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/MigrateFromLegacyCondition.java new file mode 100644 index 0000000000000000000000000000000000000000..b81b9369af53afc9065b117a2cdc126011df19c6 --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/MigrateFromLegacyCondition.java @@ -0,0 +1,15 @@ +package de.thm.arsnova.persistence.couchdb.migrations; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.stereotype.Component; + +@Component +public class MigrateFromLegacyCondition implements Condition { + @Override + public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) { + final String migrateFrom = context.getEnvironment().getProperty("couchdb.migrate-from"); + return migrateFrom != null && !migrateFrom.isEmpty(); + } +} diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/Migration.java b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/Migration.java new file mode 100644 index 0000000000000000000000000000000000000000..e2d43a08a89f080e63df63571fad495be53d15ca --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/Migration.java @@ -0,0 +1,23 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.persistence.couchdb.migrations; + +public interface Migration { + String getId(); + void migrate(); +} diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/MigrationExecutor.java b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/MigrationExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..ff74e2356d6f237ecaf2202c573e31d95381556e --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/MigrationExecutor.java @@ -0,0 +1,69 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.persistence.couchdb.migrations; + +import de.thm.arsnova.model.MigrationState; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * Run necessary migrations based on the {@link MigrationState} to adjust data to the current entity models. + * + * @author Daniel Gerhardt + */ +@Service +public class MigrationExecutor { + private static final Logger logger = LoggerFactory.getLogger(MigrationExecutor.class); + + private List<Migration> migrations = Collections.EMPTY_LIST; + + public MigrationExecutor(final Optional<List<Migration>> migrations) { + migrations.map(m -> this.migrations = m.stream() + .sorted(Comparator.comparing(Migration::getId)).collect(Collectors.toList())); + logger.debug("Initialized {} migration(s).", this.migrations.size()); + } + + public boolean runMigrations(@NonNull final MigrationState migrationState) { + List<Migration> pendingMigrations = migrations.stream() + .filter(m -> !migrationState.getCompleted().contains(m.getId())).collect(Collectors.toList()); + boolean stateChange = false; + if (migrationState.getActive() != null) { + throw new IllegalStateException("An migration is already active: " + migrationState.getActive()); + } + logger.debug("Pending migrations: " + pendingMigrations.stream() + .map(Migration::getId).collect(Collectors.joining())); + for (Migration migration : pendingMigrations) { + stateChange = true; + migrationState.setActive(migration.getId(), new Date()); + migration.migrate(); + migrationState.getCompleted().add(migration.getId()); + migrationState.setActive(null); + } + + return stateChange; + } +} diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/V2ToV3Migration.java b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/V2ToV3Migration.java new file mode 100644 index 0000000000000000000000000000000000000000..37b57cedfd8b361b963e7a1c227e6b0a133c328e --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/migrations/V2ToV3Migration.java @@ -0,0 +1,504 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.persistence.couchdb.migrations; + +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Motd; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.model.migration.FromV2Migrator; +import de.thm.arsnova.model.migration.v2.DbUser; +import de.thm.arsnova.model.migration.v2.LoggedIn; +import de.thm.arsnova.model.migration.v2.MotdList; +import de.thm.arsnova.persistence.ContentRepository; +import de.thm.arsnova.persistence.RoomRepository; +import de.thm.arsnova.persistence.UserRepository; +import de.thm.arsnova.persistence.couchdb.support.MangoCouchDbConnector; +import de.thm.arsnova.persistence.couchdb.support.PagedMangoResponse; +import org.ektorp.DbAccessException; +import org.ektorp.DocumentNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Conditional; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Performs the data migration from version 2 to version 3. + * + * @author Daniel Gerhardt + */ +@Conditional(MigrateFromLegacyCondition.class) +@Service +public class V2ToV3Migration implements Migration { + private static final String ID = "20170914131300"; + private static final int LIMIT = 200; + private static final long OUTDATED_AFTER = 1000L * 3600 * 24 * 30 * 6; + private static final String FULL_INDEX_BY_TYPE = "full-index-by-type"; + private static final String USER_INDEX = "user-index"; + private static final String LOGGEDIN_INDEX = "loggedin-index"; + private static final String SESSION_INDEX = "session-index"; + private static final String MOTD_INDEX = "motd-index"; + private static final String MOTDLIST_INDEX = "motdlist-index"; + + private static final Logger logger = LoggerFactory.getLogger(V2ToV3Migration.class); + + private FromV2Migrator migrator; + private MangoCouchDbConnector toConnector; + private MangoCouchDbConnector fromConnector; + private UserRepository userRepository; + private RoomRepository roomRepository; + private ContentRepository contentRepository; + private long referenceTimestamp = System.currentTimeMillis(); + + public V2ToV3Migration( + final FromV2Migrator migrator, + final MangoCouchDbConnector toConnector, + @Qualifier("couchDbMigrationConnector") final MangoCouchDbConnector fromConnector, + final UserRepository userRepository, + final RoomRepository roomRepository, + final ContentRepository contentRepository) { + this.migrator = migrator; + this.toConnector = toConnector; + this.fromConnector = fromConnector; + this.userRepository = userRepository; + this.roomRepository = roomRepository; + this.contentRepository = contentRepository; + } + + public String getId() { + return ID; + } + + public void migrate() { + createV2Index(); + migrator.setIgnoreRevision(true); + try { + migrateUsers(); + migrateUnregisteredUsers(); + migrateRooms(); + migrateMotds(); + migrateComments(); + migrateContents(); + migrateAnswers(); + } catch (InterruptedException e) { + throw new DbAccessException(e); + } + migrator.setIgnoreRevision(false); + } + + private void createV2Index() { + List<MangoCouchDbConnector.MangoQuery.Sort> fields; + Map<String, Object> filterSelector; + Map<String, Object> subFilterSelector; + + fields = new ArrayList<>(); + fields.add(new MangoCouchDbConnector.MangoQuery.Sort("type", false)); + fromConnector.createJsonIndex(FULL_INDEX_BY_TYPE, fields); + + filterSelector = new HashMap<>(); + filterSelector.put("type", "userdetails"); + Map<String, String> lockedFilter = new HashMap<>(); + subFilterSelector = new HashMap<>(); + subFilterSelector.put("$exists", false); + filterSelector.put("locked", subFilterSelector); + fromConnector.createPartialJsonIndex(USER_INDEX, new ArrayList<>(), filterSelector); + fields = new ArrayList<>(); + fields.add(new MangoCouchDbConnector.MangoQuery.Sort("username", false)); + fromConnector.createPartialJsonIndex(USER_INDEX, fields, filterSelector); + + filterSelector = new HashMap<>(); + filterSelector.put("type", "logged_in"); + fromConnector.createPartialJsonIndex(LOGGEDIN_INDEX, new ArrayList<>(), filterSelector); + fields = new ArrayList<>(); + fields.add(new MangoCouchDbConnector.MangoQuery.Sort("user", false)); + fromConnector.createPartialJsonIndex(LOGGEDIN_INDEX, fields, filterSelector); + + filterSelector = new HashMap<>(); + filterSelector.put("type", "session"); + fromConnector.createPartialJsonIndex(SESSION_INDEX, new ArrayList<>(), filterSelector); + fields = new ArrayList<>(); + fields.add(new MangoCouchDbConnector.MangoQuery.Sort("keyword", false)); + fromConnector.createPartialJsonIndex(SESSION_INDEX, fields, filterSelector); + + filterSelector = new HashMap<>(); + filterSelector.put("type", "motd"); + fromConnector.createPartialJsonIndex(MOTD_INDEX, new ArrayList<>(), filterSelector); + fields = new ArrayList<>(); + fields.add(new MangoCouchDbConnector.MangoQuery.Sort("motdkey", false)); + fromConnector.createPartialJsonIndex(MOTD_INDEX, fields, filterSelector); + + filterSelector = new HashMap<>(); + filterSelector.put("type", "motdlist"); + fromConnector.createPartialJsonIndex(MOTDLIST_INDEX, new ArrayList<>(), filterSelector); + fields = new ArrayList<>(); + fields.add(new MangoCouchDbConnector.MangoQuery.Sort("username", false)); + fromConnector.createPartialJsonIndex(MOTDLIST_INDEX, fields, filterSelector); + } + + private void waitForV2Index(final String name) throws InterruptedException { + for (int i = 0; i < 10; i++) { + if (fromConnector.initializeIndex(name)) { + return; + } + Thread.sleep(10000 * Math.round(1.0 + 0.5 * i)); + } + } + + private void migrateUsers() throws InterruptedException { + waitForV2Index(USER_INDEX); + waitForV2Index(LOGGEDIN_INDEX); + Map<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "userdetails"); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(USER_INDEX); + query.setLimit(LIMIT); + String bookmark = null; + + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}, bookmark: {}", skip, bookmark); + query.setBookmark(bookmark); + List<UserProfile> profilesV3 = new ArrayList<>(); + PagedMangoResponse<de.thm.arsnova.model.migration.v2.DbUser> response = + fromConnector.queryForPage(query, de.thm.arsnova.model.migration.v2.DbUser.class); + List<de.thm.arsnova.model.migration.v2.DbUser> dbUsersV2 = response.getEntities(); + bookmark = response.getBookmark(); + if (dbUsersV2.size() == 0) { + break; + } + + for (DbUser userV2 : dbUsersV2) { + HashMap<String, Object> loggedInQueryOptions = new HashMap<>(); + loggedInQueryOptions.put("type", "logged_in"); + loggedInQueryOptions.put("user", userV2.getUsername()); + MangoCouchDbConnector.MangoQuery loggedInQuery = new MangoCouchDbConnector.MangoQuery(loggedInQueryOptions); + loggedInQuery.setIndexDocument(LOGGEDIN_INDEX); + List<LoggedIn> loggedInList = fromConnector.query(loggedInQuery, LoggedIn.class); + LoggedIn loggedIn = loggedInList.size() > 0 ? loggedInList.get(0) : null; + + UserProfile profileV3 = migrator.migrate(userV2, loggedIn, loadMotdList(userV2.getUsername())); + profileV3.setAcknowledgedMotds(migrateMotdIds(profileV3.getAcknowledgedMotds())); + profilesV3.add(profileV3); + } + + toConnector.executeBulk(profilesV3); + } + } + + private void migrateUnregisteredUsers() throws InterruptedException { + waitForV2Index(USER_INDEX); + waitForV2Index(LOGGEDIN_INDEX); + /* Load registered usernames to exclude them later */ + Map<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "userdetails"); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(USER_INDEX); + query.setLimit(LIMIT); + Set<String> usernames = new HashSet<>(); + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}", skip); + query.setSkip(skip); + List<String> result = fromConnector.query(query, "username", String.class); + if (result.isEmpty()) { + break; + } + usernames.addAll(result); + } + + queryOptions = new HashMap<>(); + queryOptions.put("type", "logged_in"); + query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(LOGGEDIN_INDEX); + query.setLimit(LIMIT); + String bookmark = null; + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}, bookmark: {}", skip, bookmark); + query.setBookmark(bookmark); + List<UserProfile> profilesV3 = new ArrayList<>(); + PagedMangoResponse<de.thm.arsnova.model.migration.v2.LoggedIn> response = + fromConnector.queryForPage(query, de.thm.arsnova.model.migration.v2.LoggedIn.class); + List<de.thm.arsnova.model.migration.v2.LoggedIn> loggedInsV2 = response.getEntities(); + bookmark = response.getBookmark(); + if (loggedInsV2.isEmpty()) { + break; + } + for (LoggedIn loggedInV2 : loggedInsV2) { + if (usernames.contains(loggedInV2.getUser())) { + continue; + } + /* There might be rare cases of duplicate LoggedIn records for a user so add them to the filter list */ + usernames.add(loggedInV2.getUser()); + UserProfile profileV3 = migrator.migrate(null, loggedInV2, loadMotdList(loggedInV2.getUser())); + profileV3.setAcknowledgedMotds(migrateMotdIds(profileV3.getAcknowledgedMotds())); + profilesV3.add(profileV3); + } + toConnector.executeBulk(profilesV3); + } + } + + private void migrateRooms() throws InterruptedException { + waitForV2Index(SESSION_INDEX); + Map<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "session"); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(SESSION_INDEX); + query.setLimit(LIMIT); + String bookmark = null; + + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}, bookmark: {}", skip, bookmark); + query.setBookmark(bookmark); + List<Room> roomsV3 = new ArrayList<>(); + PagedMangoResponse<de.thm.arsnova.model.migration.v2.Room> response = + fromConnector.queryForPage(query, de.thm.arsnova.model.migration.v2.Room.class); + List<de.thm.arsnova.model.migration.v2.Room> roomsV2 = response.getEntities(); + bookmark = response.getBookmark(); + if (roomsV2.size() == 0) { + break; + } + + for (de.thm.arsnova.model.migration.v2.Room roomV2 : roomsV2) { + List<UserProfile> profiles = userRepository.findByLoginId(roomV2.getCreator()); + if (profiles.size() == 0) { + logger.warn("Skipping migration of Room {}. Creator {} does not exist.", + roomV2.getId(), roomV2.getCreator()); + continue; + } + roomsV3.add(migrator.migrate(roomV2, Optional.of(profiles.get(0)))); + } + + toConnector.executeBulk(roomsV3); + } + } + + private void migrateMotds() throws InterruptedException { + waitForV2Index(MOTD_INDEX); + Map<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "motd"); + /* Exclude outdated MotDs */ + HashMap<String, String> subQuery = new HashMap<>(); + subQuery.put("$gt", String.valueOf(referenceTimestamp - OUTDATED_AFTER)); + queryOptions.put("enddate", subQuery); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(MOTD_INDEX); + query.setLimit(LIMIT); + String bookmark = null; + + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}, bookmark: {}", skip, bookmark); + query.setBookmark(bookmark); + List<Motd> motdsV3 = new ArrayList<>(); + PagedMangoResponse<de.thm.arsnova.model.migration.v2.Motd> response = + fromConnector.queryForPage(query, de.thm.arsnova.model.migration.v2.Motd.class); + List<de.thm.arsnova.model.migration.v2.Motd> motdsV2 = response.getEntities(); + bookmark = response.getBookmark(); + if (motdsV2.size() == 0) { + break; + } + + for (de.thm.arsnova.model.migration.v2.Motd motdV2 : motdsV2) { + if (motdV2.getAudience().equals("session")) { + Room room = roomRepository.findByShortId(motdV2.getSessionkey()); + /* sessionId has not been set for some old MotDs */ + if (room == null) { + logger.warn("Skipping migration of Motd {}. Room {} does not exist.", + motdV2.getId(), motdV2.getSessionId()); + continue; + } + motdV2.setSessionId(room.getId()); + } + motdsV3.add(migrator.migrate(motdV2)); + } + + toConnector.executeBulk(motdsV3); + } + } + + private void migrateComments() throws InterruptedException { + waitForV2Index(FULL_INDEX_BY_TYPE); + Map<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "interposed_question"); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(FULL_INDEX_BY_TYPE); + query.setLimit(LIMIT); + String bookmark = null; + + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}, bookmark: {}", skip, bookmark); + query.setBookmark(bookmark); + List<Comment> commentsV3 = new ArrayList<>(); + PagedMangoResponse<de.thm.arsnova.model.migration.v2.Comment> response = + fromConnector.queryForPage(query, de.thm.arsnova.model.migration.v2.Comment.class); + List<de.thm.arsnova.model.migration.v2.Comment> commentsV2 = response.getEntities(); + bookmark = response.getBookmark(); + if (commentsV2.size() == 0) { + break; + } + + for (de.thm.arsnova.model.migration.v2.Comment commentV2 : commentsV2) { + try { + Room roomV3 = roomRepository.findOne(commentV2.getSessionId()); + List<UserProfile> profiles = Collections.EMPTY_LIST; + if (commentV2.getCreator() != null && !commentV2.getCreator().equals("")) { + profiles = userRepository.findByLoginId(commentV2.getCreator()); + } + if (profiles.size() == 0) { + /* No creator is set or creator does not exist -> fallback: creator = Room owner */ + commentV2.setCreator(null); + Comment commentV3 = migrator.migrate(commentV2); + commentV3.setCreatorId(roomV3.getOwnerId()); + commentsV3.add(commentV3); + } else { + commentsV3.add(migrator.migrate(commentV2, profiles.get(0))); + } + } catch (DocumentNotFoundException e) { + logger.warn("Skipping migration of Comment {}. Room {} does not exist.", + commentV2.getId(), commentV2.getSessionId()); + continue; + } + } + + toConnector.executeBulk(commentsV3); + } + } + + private void migrateContents() throws InterruptedException { + waitForV2Index(FULL_INDEX_BY_TYPE); + Map<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "skill_question"); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(FULL_INDEX_BY_TYPE); + query.setLimit(LIMIT); + String bookmark = null; + + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}, bookmark: {}", skip, bookmark); + query.setBookmark(bookmark); + List<Content> contentsV3 = new ArrayList<>(); + PagedMangoResponse<de.thm.arsnova.model.migration.v2.Content> response = + fromConnector.queryForPage(query, de.thm.arsnova.model.migration.v2.Content.class); + List<de.thm.arsnova.model.migration.v2.Content> contentsV2 = response.getEntities(); + bookmark = response.getBookmark(); + if (contentsV2.size() == 0) { + break; + } + + for (de.thm.arsnova.model.migration.v2.Content contentV2 : contentsV2) { + if (roomRepository.existsById(contentV2.getSessionId())) { + try { + contentsV3.add(migrator.migrate(contentV2)); + } catch (IllegalArgumentException e) { + logger.warn("Skipping migration of Content {}.", contentV2.getId(), e); + } + } else { + logger.warn("Skipping migration of Content {}. Room {} does not exist.", + contentV2.getId(), contentV2.getSessionId()); + } + } + + toConnector.executeBulk(contentsV3); + } + } + + private void migrateAnswers() throws InterruptedException { + waitForV2Index(FULL_INDEX_BY_TYPE); + Map<String, Object> queryOptions = new HashMap<>(); + queryOptions.put("type", "skill_question_answer"); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(FULL_INDEX_BY_TYPE); + query.setLimit(LIMIT); + String bookmark = null; + + for (int skip = 0;; skip += LIMIT) { + logger.debug("Migration progress: {}, bookmark: {}", skip, bookmark); + query.setBookmark(bookmark); + List<Answer> answersV3 = new ArrayList<>(); + PagedMangoResponse<de.thm.arsnova.model.migration.v2.Answer> response = + fromConnector.queryForPage(query, de.thm.arsnova.model.migration.v2.Answer.class); + List<de.thm.arsnova.model.migration.v2.Answer> answersV2 = response.getEntities(); + bookmark = response.getBookmark(); + if (answersV2.size() == 0) { + break; + } + + for (de.thm.arsnova.model.migration.v2.Answer answerV2 : answersV2) { + if (!roomRepository.existsById(answerV2.getSessionId())) { + logger.warn("Skipping migration of Answer {}. Room {} does not exist.", + answerV2.getId(), answerV2.getQuestionId()); + continue; + } + try { + Content contentV3 = contentRepository.findOne(answerV2.getQuestionId()); + answersV3.add(migrator.migrate(answerV2, contentV3)); + } catch (DocumentNotFoundException e) { + logger.warn("Skipping migration of Answer {}. Content {} does not exist.", + answerV2.getId(), answerV2.getQuestionId()); + continue; + } catch (IndexOutOfBoundsException e) { + logger.warn("Skipping migration of Answer {}. Data inconsistency detected.", answerV2.getId()); + } + } + + toConnector.executeBulk(answersV3); + } + } + + private HashSet<String> migrateMotdIds(final Set<String> oldIds) throws InterruptedException { + if (oldIds.isEmpty()) { + return new HashSet<>(); + } + waitForV2Index(MOTD_INDEX); + Map<String, Object> queryOptions = new HashMap<>(); + Map<String, Set<String>> subQuery1 = new HashMap<>(); + subQuery1.put("$in", oldIds); + queryOptions.put("type", "motd"); + queryOptions.put("motdkey", subQuery1); + /* Exclude outdated MotDs */ + HashMap<String, String> subQuery2 = new HashMap<>(); + subQuery2.put("$gt", String.valueOf(referenceTimestamp - OUTDATED_AFTER)); + queryOptions.put("enddate", subQuery2); + MangoCouchDbConnector.MangoQuery query = new MangoCouchDbConnector.MangoQuery(queryOptions); + query.setIndexDocument(MOTD_INDEX); + query.setLimit(LIMIT); + + return new HashSet<>(fromConnector.query(query, "_id", String.class)); + } + + private MotdList loadMotdList(final String username) throws InterruptedException { + waitForV2Index(MOTDLIST_INDEX); + HashMap<String, Object> motdListQueryOptions = new HashMap<>(); + motdListQueryOptions.put("type", "motdlist"); + motdListQueryOptions.put("username", username); + MangoCouchDbConnector.MangoQuery motdListQuery = new MangoCouchDbConnector.MangoQuery(motdListQueryOptions); + motdListQuery.setIndexDocument(MOTDLIST_INDEX); + List<MotdList> motdListList = fromConnector.query(motdListQuery, MotdList.class); + + return motdListList.size() > 0 ? motdListList.get(0) : null; + } +} diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java b/src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoCouchDbConnector.java similarity index 55% rename from src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoCouchDbConnector.java index 07d0776673d525925eb8626192de82bd089a30f7..b8f61d21feb9955069481d14c30225fa6632ca26 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoCouchDbConnector.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoCouchDbConnector.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.persistance.couchdb.support; +package de.thm.arsnova.persistence.couchdb.support; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -8,9 +8,10 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.databind.util.Converter; -import de.thm.arsnova.entities.serialization.View; +import de.thm.arsnova.model.serialization.View; import org.ektorp.CouchDbInstance; import org.ektorp.DbAccessException; +import org.ektorp.http.HttpResponse; import org.ektorp.impl.ObjectMapperFactory; import org.ektorp.impl.StdCouchDbConnector; import org.slf4j.Logger; @@ -18,6 +19,7 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,13 +33,16 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { * Represents a <code>_find</code> query for CouchDB's Mango API. * See http://docs.couchdb.org/en/stable/api/database/find.html#db-find. */ - public class MangoQuery { - @JsonSerialize(converter = Sort.ToListConverter.class) - public class Sort { - public class ToListConverter implements Converter<Sort, List<String>> { + public static class MangoQuery { + @JsonSerialize(converter = Sort.ToMapConverter.class) + public static class Sort { + public static class ToMapConverter implements Converter<Sort, Map<String, String>> { @Override - public List<String> convert(Sort value) { - return Arrays.asList(value.field, value.descending ? "desc" : "asc"); + public Map<String, String> convert(Sort value) { + Map<String, String> map = new HashMap<>(); + map.put(value.field, value.descending ? "desc" : "asc"); + + return map; } @Override @@ -47,7 +52,7 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { @Override public JavaType getOutputType(TypeFactory typeFactory) { - return typeFactory.constructGeneralizedType(typeFactory.constructType(List.class), String.class); + return typeFactory.constructMapType(Map.class, String.class, String.class); } } @@ -85,6 +90,7 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { private String indexName; private boolean update = true; private boolean stable = false; + private String bookmark; public MangoQuery() { this.selector = new HashMap<>(); @@ -97,6 +103,7 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { this.selector = selector; } + @JsonInclude(JsonInclude.Include.ALWAYS) @JsonView(View.Persistence.class) public Map<String, ?> getSelector() { return selector; @@ -184,6 +191,15 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { public void setStable(boolean stable) { this.stable = stable; } + + @JsonView(View.Persistence.class) + public String getBookmark() { + return bookmark; + } + + public void setBookmark(final String bookmark) { + this.bookmark = bookmark; + } } private static final Logger logger = LoggerFactory.getLogger(MangoCouchDbConnector.class); @@ -199,11 +215,10 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { /** * * @param query The query sent to CouchDB's Mango API - * @param type Type for deserialization of retrieved entities + * @param rh Handler for the response to the query * @return List of retrieved entities */ - public <T> List<T> query(final MangoQuery query, final Class<T> type) { - MangoResponseHandler<T> rh = new MangoResponseHandler<T>(type, objectMapper, true); + public <T> List<T> query(final MangoQuery query, final MangoResponseHandler<T> rh) { String queryString; try { queryString = objectMapper.writeValueAsString(query); @@ -211,9 +226,90 @@ public class MangoCouchDbConnector extends StdCouchDbConnector { } catch (JsonProcessingException e) { throw new DbAccessException("Could not serialize Mango query."); } - List<T> result = restTemplate.post(dbURI.append("_find").toString(), queryString, rh); + List<T> result = restTemplate.postUncached(dbURI.append("_find").toString(), queryString, rh); + //List<T> result = restTemplate.post(dbURI.append("_find").toString(), new JacksonableEntity(query, objectMapper), rh); + logger.debug("Answer from CouchDB Mango query: {}", result); return result; } + + /** + * + * @param query The query sent to CouchDB's Mango API + * @param type Type for deserialization of retrieved entities + * @return List of retrieved entities + */ + public <T> List<T> query(final MangoQuery query, final Class<T> type) { + MangoResponseHandler<T> rh = new MangoResponseHandler<>(type, objectMapper, true); + return query(query, rh); + } + + /** + * + * @param query The query sent to CouchDB's Mango API + * @param propertyName Name of the entity's property to be parsed + * @param type Type for deserialization of retrieved entities + * @return List of retrieved entities + */ + public <T> List<T> query(final MangoQuery query, final String propertyName, final Class<T> type) { + query.setFields(Arrays.asList(new String[] {propertyName})); + MangoResponseHandler<T> rh = new MangoResponseHandler<>(propertyName, type, objectMapper); + + return query(query, rh); + } + + /** + * + * @param query The query sent to CouchDB's Mango API + * @param type Type for deserialization of retrieved entities + * @return List of retrieved entities + */ + public <T> PagedMangoResponse<T> queryForPage(final MangoQuery query, final Class<T> type) { + MangoResponseHandler<T> rh = new MangoResponseHandler<>(type, objectMapper, true); + return new PagedMangoResponse<T>(query(query, rh), rh.getBookmark()); + } + + public void createPartialJsonIndex(final String name, final List<MangoQuery.Sort> fields, final Map<String, Object> filterSelector) { + Map<String, Object> query = new HashMap<>(); + Map<String, Object> index = new HashMap<>(); + query.put("ddoc", name); + query.put("type", "json"); + query.put("index", index); + index.put("fields", fields); + if (filterSelector != null) { + index.put("partial_filter_selector", filterSelector); + } + String queryString; + try { + queryString = objectMapper.writeValueAsString(query); + logger.debug("Creating CouchDB index using Mango API: {}", queryString); + } catch (JsonProcessingException e) { + throw new DbAccessException(e); + } + restTemplate.postUncached(dbURI.append("_index").toString(), queryString); + } + + public void createJsonIndex(final String name, final List<MangoQuery.Sort> fields) { + createPartialJsonIndex(name, fields, null); + } + + public boolean initializeIndex(final String name) { + MangoQuery query = new MangoQuery(Collections.EMPTY_MAP); + query.setIndexDocument(name); + query.setLimit(0); + try { + String queryString = objectMapper.writeValueAsString(query); + logger.debug("Using Mango API query to initialize CouchDB index: {}", queryString); + HttpResponse response = restTemplate.postUncached(dbURI.append("_find").toString(), queryString); + response.releaseConnection(); + } catch (JsonProcessingException e) { + throw new DbAccessException("Could not serialize Mango query."); + } catch (DbAccessException e) { + logger.debug("CouchDB index is not ready yet: {}", name, e); + return false; + } + + return true; + } } diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoQueryResultParser.java b/src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoQueryResultParser.java similarity index 69% rename from src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoQueryResultParser.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoQueryResultParser.java index 7367787f539098ce75769c789eb988152976bf06..c8fd9289ec5512d9f440f1d0e5cfe499205d895b 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoQueryResultParser.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoQueryResultParser.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.persistance.couchdb.support; +package de.thm.arsnova.persistence.couchdb.support; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; @@ -14,6 +14,7 @@ import java.util.List; public class MangoQueryResultParser<T> { private static final String DOCS_FIELD_NAME = "docs"; + private static final String BOOKMARK_FIELD_NAME = "bookmark"; private static final String WARNING_FIELD_NAME = "warning"; private static final String ERROR_FIELD_NAME = "error"; private static final String REASON_FIELD_NAME = "reason"; @@ -23,12 +24,20 @@ public class MangoQueryResultParser<T> { private Class<T> type; private ObjectMapper objectMapper; private List<T> docs; + private String bookmark; + private String propertyName = null; public MangoQueryResultParser(Class<T> type, ObjectMapper objectMapper) { this.type = type; this.objectMapper = objectMapper; } + public MangoQueryResultParser(String propertyName, Class<T> type, ObjectMapper objectMapper) { + this.propertyName = propertyName; + this.type = type; + this.objectMapper = objectMapper; + } + public void parseResult(InputStream json) throws IOException { JsonParser jp = objectMapper.getFactory().createParser(json); @@ -53,6 +62,8 @@ public class MangoQueryResultParser<T> { if (DOCS_FIELD_NAME.equals(currentName)) { docs = new ArrayList<T>(); parseDocs(jp); + } else if (BOOKMARK_FIELD_NAME.equals(currentName)) { + bookmark = jp.getText(); } else if (WARNING_FIELD_NAME.equals(currentName)) { logger.warn("Warning for CouchDB Mango query: {}", jp.getText()); } else if (ERROR_FIELD_NAME.equals(currentName)) { @@ -74,8 +85,26 @@ public class MangoQueryResultParser<T> { } while (jp.nextToken() == JsonToken.START_OBJECT) { - T doc = jp.readValueAs(type); - docs.add(doc); + T doc = null; + if (propertyName == null) { + doc = jp.readValueAs(type); + docs.add(doc); + } else { + while (jp.nextToken() == JsonToken.FIELD_NAME) { + String fieldName = jp.getText(); + jp.nextToken(); + if (fieldName.equals(propertyName)) { + doc = jp.readValueAs(type); + docs.add(doc); + } + } + if (doc == null) { + throw new DbAccessException("Cannot parse response from CouchDB. Property is missing."); + } + if (jp.currentToken() != JsonToken.END_OBJECT) { + throw new DbAccessException("Cannot parse response from CouchDB. Unexpected data."); + } + } } if (jp.currentToken() != JsonToken.END_ARRAY) { @@ -86,4 +115,8 @@ public class MangoQueryResultParser<T> { public List<T> getDocs() { return docs; } + + public String getBookmark() { + return bookmark; + } } diff --git a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoResponseHandler.java b/src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoResponseHandler.java similarity index 64% rename from src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoResponseHandler.java rename to src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoResponseHandler.java index 462555e7abd12682836bfcc1d223a5b68ab9970e..84ae2275c7781d5330d61954e23a39523e0db5f5 100644 --- a/src/main/java/de/thm/arsnova/persistance/couchdb/support/MangoResponseHandler.java +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/support/MangoResponseHandler.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.persistance.couchdb.support; +package de.thm.arsnova.persistence.couchdb.support; import com.fasterxml.jackson.databind.ObjectMapper; import org.ektorp.http.HttpResponse; @@ -10,6 +10,7 @@ import java.util.List; public class MangoResponseHandler<T> extends StdResponseHandler<List<T>> { private MangoQueryResultParser<T> parser; + private String bookmark; public MangoResponseHandler(Class<T> docType, ObjectMapper om) { Assert.notNull(om, "ObjectMapper may not be null"); @@ -24,9 +25,22 @@ public class MangoResponseHandler<T> extends StdResponseHandler<List<T>> { parser = new MangoQueryResultParser<T>(docType, om); } + public MangoResponseHandler(String propertyName, Class<T> propertyType, ObjectMapper om) { + Assert.notNull(om, "ObjectMapper may not be null"); + Assert.notNull(propertyType, "propertyType may not be null"); + Assert.notNull(propertyName, "propertyName may not be null"); + parser = new MangoQueryResultParser<T>(propertyName, propertyType, om); + } + @Override public List<T> success(HttpResponse hr) throws Exception { parser.parseResult(hr.getContent()); + bookmark = parser.getBookmark(); + return parser.getDocs(); } + + public String getBookmark() { + return bookmark; + } } diff --git a/src/main/java/de/thm/arsnova/persistence/couchdb/support/PagedMangoResponse.java b/src/main/java/de/thm/arsnova/persistence/couchdb/support/PagedMangoResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..a8f4b3793a4c902b78d3a5f17cf904f0c2e670ee --- /dev/null +++ b/src/main/java/de/thm/arsnova/persistence/couchdb/support/PagedMangoResponse.java @@ -0,0 +1,27 @@ +package de.thm.arsnova.persistence.couchdb.support; + +import java.util.List; + +/** + * Contains the entities of the response and the bookmark to query the next page. + * + * @param <T> + * @author Daniel Gerhardt + */ +public class PagedMangoResponse<T> { + private List<T> entities; + private String bookmark; + + public PagedMangoResponse(final List<T> entities, final String bookmark) { + this.entities = entities; + this.bookmark = bookmark; + } + + public List<T> getEntities() { + return entities; + } + + public String getBookmark() { + return bookmark; + } +} diff --git a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java index b20639d8632fa9725f55cfe06578af4cb97e4453..13ab5f6c949152710bd8d6f23d063f45a46cdf61 100644 --- a/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java +++ b/src/main/java/de/thm/arsnova/security/ApplicationPermissionEvaluator.java @@ -17,22 +17,23 @@ */ package de.thm.arsnova.security; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.persistance.CommentRepository; -import de.thm.arsnova.persistance.ContentRepository; -import de.thm.arsnova.persistance.SessionRepository; -import org.pac4j.oauth.profile.facebook.FacebookProfile; -import org.pac4j.oauth.profile.google2.Google2Profile; -import org.pac4j.oauth.profile.twitter.TwitterProfile; -import org.pac4j.springframework.security.authentication.Pac4jAuthenticationToken; +import de.thm.arsnova.model.Motd; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentRepository; +import de.thm.arsnova.persistence.MotdRepository; +import de.thm.arsnova.persistence.RoomRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Arrays; @@ -40,13 +41,15 @@ import java.util.Arrays; /** * Provides access control methods that can be used in annotations. */ +@Component public class ApplicationPermissionEvaluator implements PermissionEvaluator { + private static final Logger logger = LoggerFactory.getLogger(ApplicationPermissionEvaluator.class); @Value("${security.admin-accounts}") private String[] adminAccounts; @Autowired - private SessionRepository sessionRepository; + private RoomRepository roomRepository; @Autowired private CommentRepository commentRepository; @@ -54,24 +57,32 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { @Autowired private ContentRepository contentRepository; + @Autowired + private MotdRepository motdRepository; + @Override public boolean hasPermission( final Authentication authentication, final Object targetDomainObject, final Object permission) { + logger.debug("Evaluating permission: hasPermission({}, {}. {})", authentication, targetDomainObject, permission); if (authentication == null || targetDomainObject == null || !(permission instanceof String)) { return false; } - final String username = getUsername(authentication); + final String userId = getUserId(authentication); - return hasAdminRole(username) - || (targetDomainObject instanceof Session - && hasSessionPermission(username, ((Session) targetDomainObject), permission.toString())) + return hasAdminRole(userId) + || (targetDomainObject instanceof UserProfile + && hasUserProfilePermission(userId, ((UserProfile) targetDomainObject), permission.toString())) + || (targetDomainObject instanceof Room + && hasRoomPermission(userId, ((Room) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Content - && hasContentPermission(username, ((Content) targetDomainObject), permission.toString())) + && hasContentPermission(userId, ((Content) targetDomainObject), permission.toString())) || (targetDomainObject instanceof Comment - && hasCommentPermission(username, ((Comment) targetDomainObject), permission.toString())); + && hasCommentPermission(userId, ((Comment) targetDomainObject), permission.toString())) + || (targetDomainObject instanceof Motd + && hasMotdPermission(userId, ((Motd) targetDomainObject), permission.toString())); } @Override @@ -80,86 +91,143 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { final Serializable targetId, final String targetType, final Object permission) { + logger.debug("Evaluating permission: hasPermission({}, {}, {}, {})", authentication, targetId, targetType, permission); if (authentication == null || targetId == null || targetType == null || !(permission instanceof String)) { return false; } - final String username = getUsername(authentication); - if (hasAdminRole(username)) { + final String userId = getUserId(authentication); + if (hasAdminRole(userId)) { return true; } switch (targetType) { - case "session": - final Session targetSession = sessionRepository.findByKeyword(targetId.toString()); - return targetSession != null && hasSessionPermission(username, targetSession, permission.toString()); + case "userprofile": + final UserProfile targetUserProfile = new UserProfile(); + targetUserProfile.setId(targetId.toString()); + return hasUserProfilePermission(userId, targetUserProfile, permission.toString()); + case "room": + final Room targetRoom = roomRepository.findOne(targetId.toString()); + return targetRoom != null && hasRoomPermission(userId, targetRoom, permission.toString()); case "content": final Content targetContent = contentRepository.findOne(targetId.toString()); - return targetContent != null && hasContentPermission(username, targetContent, permission.toString()); + return targetContent != null && hasContentPermission(userId, targetContent, permission.toString()); case "comment": final Comment targetComment = commentRepository.findOne(targetId.toString()); - return targetComment != null && hasCommentPermission(username, targetComment, permission.toString()); + return targetComment != null && hasCommentPermission(userId, targetComment, permission.toString()); + case "motd": + final Motd targetMotd = motdRepository.findOne(targetId.toString()); + return targetMotd != null && hasMotdPermission(userId, targetMotd, permission.toString()); default: return false; } } - private boolean hasSessionPermission( - final String username, - final Session targetSession, + private boolean hasUserProfilePermission( + final String userId, + final UserProfile targetUserProfile, final String permission) { switch (permission) { case "read": - return targetSession.isActive(); + return userId.equals(targetUserProfile.getId()); case "create": - return !username.isEmpty(); + return true; case "owner": case "update": case "delete": - return targetSession.getCreator().equals(username); + return userId.equals(targetUserProfile.getId()); + default: + return false; + } + } + + private boolean hasRoomPermission( + final String userId, + final Room targetRoom, + final String permission) { + switch (permission) { + case "read": + return !targetRoom.isClosed(); + case "create": + return !userId.isEmpty(); + case "owner": + case "update": + case "delete": + return targetRoom.getOwnerId().equals(userId); default: return false; } } private boolean hasContentPermission( - final String username, + final String userId, final Content targetContent, final String permission) { switch (permission) { case "read": - return sessionRepository.findOne(targetContent.getSessionId()).isActive(); + return !roomRepository.findOne(targetContent.getRoomId()).isClosed(); case "create": case "owner": case "update": case "delete": - final Session session = sessionRepository.findOne(targetContent.getSessionId()); - return session != null && session.getCreator().equals(username); + final Room room = roomRepository.findOne(targetContent.getRoomId()); + return room != null && room.getOwnerId().equals(userId); default: return false; } } private boolean hasCommentPermission( - final String username, + final String userId, final Comment targetComment, final String permission) { switch (permission) { case "create": - return !username.isEmpty() && sessionRepository.findOne(targetComment.getSessionId()).isActive(); + return !userId.isEmpty() && !roomRepository.findOne(targetComment.getRoomId()).isClosed(); case "owner": case "update": - return targetComment.getCreator() != null && targetComment.getCreator().equals(username); + return targetComment.getCreatorId() != null && targetComment.getCreatorId().equals(userId); case "read": case "delete": - if (targetComment.getCreator() != null && targetComment.getCreator().equals(username)) { + if (targetComment.getCreatorId() != null && targetComment.getCreatorId().equals(userId)) { return true; } /* Allow reading & deletion by session owner */ - final Session session = sessionRepository.findOne(targetComment.getSessionId()); + final Room room = roomRepository.findOne(targetComment.getRoomId()); + + return room != null && room.getOwnerId().equals(userId); + default: + return false; + } + } - return session != null && session.getCreator().equals(username); + private boolean hasMotdPermission( + final String userId, + final Motd targetMotd, + final String permission) { + Room room; + switch (permission) { + case "create": + case "owner": + case "update": + case "delete": + if (userId.isEmpty() || targetMotd.getRoomId() == null || targetMotd.getAudience() != Motd.Audience.ROOM) { + return false; + } + room = roomRepository.findOne(targetMotd.getRoomId()); + if (room == null) { + return false; + } + + return userId.equals(room.getOwnerId()); + case "read": + if (targetMotd.getAudience() != Motd.Audience.ROOM) { + return true; + } + room = roomRepository.findOne(targetMotd.getRoomId()); + + return room != null && !room.isClosed() || room.getOwnerId().equals(userId); default: return false; } @@ -170,32 +238,14 @@ public class ApplicationPermissionEvaluator implements PermissionEvaluator { return Arrays.asList(adminAccounts).contains(username); } - private String getUsername(final Authentication authentication) { - if (authentication == null || authentication instanceof AnonymousAuthenticationToken) { + private String getUserId(final Authentication authentication) { + if (authentication == null || authentication instanceof AnonymousAuthenticationToken || + !(authentication.getPrincipal() instanceof User)) { return ""; } + User user = (User) authentication.getPrincipal(); - if (authentication instanceof Pac4jAuthenticationToken) { - User user = null; - - final Pac4jAuthenticationToken token = (Pac4jAuthenticationToken) authentication; - if (token.getProfile() instanceof Google2Profile) { - final Google2Profile profile = (Google2Profile) token.getProfile(); - user = new User(profile); - } else if (token.getProfile() instanceof TwitterProfile) { - final TwitterProfile profile = (TwitterProfile) token.getProfile(); - user = new User(profile); - } else if (token.getProfile() instanceof FacebookProfile) { - final FacebookProfile profile = (FacebookProfile) token.getProfile(); - user = new User(profile); - } - - if (user != null) { - return user.getUsername(); - } - } - - return authentication.getName(); + return user.getId(); } private boolean isWebsocketAccess(Authentication auth) { diff --git a/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java b/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java index fc048b2ff8cae3420052226c3727294811f76470..a723eccb7a02d6bbd15d2962a52505165613bcd6 100644 --- a/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java +++ b/src/main/java/de/thm/arsnova/security/CasUserDetailsService.java @@ -17,18 +17,18 @@ */ package de.thm.arsnova.security; -import de.thm.arsnova.services.UserService; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.service.UserService; import org.jasig.cas.client.validation.Assertion; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.cas.userdetails.AbstractCasAssertionUserDetailsService; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; -import java.util.ArrayList; -import java.util.List; +import java.util.HashSet; +import java.util.Set; /** * Class to load a user based on the results from CAS. @@ -40,21 +40,15 @@ public class CasUserDetailsService extends AbstractCasAssertionUserDetailsServic @Override protected UserDetails loadUserDetails(final Assertion assertion) { - final List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); + final Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_CAS_USER")); final String uid = assertion.getPrincipal().getName(); if (userService.isAdmin(uid)) { grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } - return new User( - uid, - "", - true, - true, - true, - true, - grantedAuthorities - ); + return userService.loadUser(UserProfile.AuthProvider.CAS, assertion.getPrincipal().getName(), + grantedAuthorities, true); } } diff --git a/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java b/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java index c8086a0e55a2c831cc889b88cf4aa448b55bad6e..e9859decb0dfe3a6584b0ff8b40a1de1dcf63db0 100644 --- a/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java +++ b/src/main/java/de/thm/arsnova/security/CustomLdapUserDetailsMapper.java @@ -17,14 +17,19 @@ */ package de.thm.arsnova.security; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.service.UserService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.ldap.core.DirContextOperations; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import java.util.Collection; +import java.util.HashSet; /** * Replaces the user ID provided by the authenticating user with the one that is part of LDAP object. This is necessary @@ -35,18 +40,30 @@ public class CustomLdapUserDetailsMapper extends LdapUserDetailsMapper { private String userIdAttr; + @Autowired + private UserService userService; + public CustomLdapUserDetailsMapper(String ldapUserIdAttr) { this.userIdAttr = ldapUserIdAttr; } - public UserDetails mapUserFromContext(DirContextOperations ctx, String username, - Collection<? extends GrantedAuthority> authorities) { + public UserDetails mapUserFromContext( + final DirContextOperations ctx, + final String username, + final Collection<? extends GrantedAuthority> authorities) { String ldapUsername = ctx.getStringAttribute(userIdAttr); if (ldapUsername == null) { logger.warn("LDAP attribute {} not set. Falling back to lowercased user provided username.", userIdAttr); ldapUsername = username.toLowerCase(); } - return super.mapUserFromContext(ctx, ldapUsername, authorities); + final Collection<GrantedAuthority> grantedAuthorities = (Collection<GrantedAuthority>) authorities; + final Collection<GrantedAuthority> additionalAuthorities = new HashSet<>(); + additionalAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + additionalAuthorities.add(new SimpleGrantedAuthority("ROLE_LDAP_USER")); + grantedAuthorities.addAll(additionalAuthorities); + + return userService.loadUser(UserProfile.AuthProvider.LDAP, ldapUsername, + grantedAuthorities, true); } } diff --git a/src/main/java/de/thm/arsnova/security/GuestUserDetailsService.java b/src/main/java/de/thm/arsnova/security/GuestUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..5596de1c14cefc4e40bc8c870efab3ce1d18bc88 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/GuestUserDetailsService.java @@ -0,0 +1,59 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security; + +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.service.UserService; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashSet; + +/** + * Loads UserDetails for a guest user ({@link UserProfile.AuthProvider#ARSNOVA_GUEST}) based on the username (guest + * token). + * + * @author Daniel Gerhardt + */ +@Service +public class GuestUserDetailsService implements UserDetailsService { + private final UserService userService; + private final Collection<GrantedAuthority> grantedAuthorities; + + public GuestUserDetailsService(UserService userService) { + this.userService = userService; + grantedAuthorities = new HashSet<>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_GUEST_USER")); + } + + @Override + public UserDetails loadUserByUsername(final String loginId) throws UsernameNotFoundException { + return loadUserByUsername(loginId, false); + } + + public UserDetails loadUserByUsername(final String loginId, final boolean autoCreate) { + return userService.loadUser(UserProfile.AuthProvider.ARSNOVA_GUEST, loginId, + grantedAuthorities, autoCreate); + } +} diff --git a/src/main/java/de/thm/arsnova/security/DbUserDetailsService.java b/src/main/java/de/thm/arsnova/security/RegisteredUserDetailsService.java similarity index 52% rename from src/main/java/de/thm/arsnova/security/DbUserDetailsService.java rename to src/main/java/de/thm/arsnova/security/RegisteredUserDetailsService.java index 256fe3d3d00e27364be4726e07a63c9c46f755e3..24b3fc1037c506877ac77ddd5cfced763e390fb7 100644 --- a/src/main/java/de/thm/arsnova/security/DbUserDetailsService.java +++ b/src/main/java/de/thm/arsnova/security/RegisteredUserDetailsService.java @@ -1,6 +1,6 @@ /* * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * Copyright (C) 2012-2018 The ARSnova Team * * ARSnova Backend is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,55 +17,50 @@ */ package de.thm.arsnova.security; -import de.thm.arsnova.entities.DbUser; -import de.thm.arsnova.persistance.UserRepository; -import de.thm.arsnova.services.UserService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import java.util.ArrayList; -import java.util.List; +import java.util.Collection; +import java.util.HashSet; /** - * Class to load a user based on the username. + * Loads UserDetails for a registered user ({@link UserProfile.AuthProvider#ARSNOVA}) based on the username (loginId). + * + * @author Daniel Gerhardt */ @Service -public class DbUserDetailsService implements UserDetailsService { - @Autowired - private UserRepository userRepository; - - @Autowired +public class RegisteredUserDetailsService implements UserDetailsService { private UserService userService; + private final Collection<GrantedAuthority> grantedAuthorities; - private static final Logger logger = LoggerFactory - .getLogger(DbUserDetailsService.class); + public RegisteredUserDetailsService() { + grantedAuthorities = new HashSet<>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_REGISTERED_USER")); + } @Override - public UserDetails loadUserByUsername(String username) { - String uid = username.toLowerCase(); - logger.debug("Load user: " + uid); - DbUser dbUser = userRepository.findByUsername(uid); - if (null == dbUser) { - throw new UsernameNotFoundException("User does not exist."); - } - - final List<GrantedAuthority> grantedAuthorities = new ArrayList<>(); - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_DB_USER")); - if (userService.isAdmin(uid)) { - grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN")); + public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { + final String loginId = username.toLowerCase(); + Collection<GrantedAuthority> ga = grantedAuthorities; + if (userService.isAdmin(loginId)) { + ga = new ArrayList<>(grantedAuthorities); + ga.add(new SimpleGrantedAuthority("ROLE_ADMIN")); } + return userService.loadUser(UserProfile.AuthProvider.ARSNOVA, loginId, + ga, false); + } - return new User(uid, dbUser.getPassword(), - null == dbUser.getActivationKey(), true, true, true, - grantedAuthorities); + @Autowired + public void setUserService(final UserService userService) { + this.userService = userService; } } diff --git a/src/main/java/de/thm/arsnova/security/User.java b/src/main/java/de/thm/arsnova/security/User.java new file mode 100644 index 0000000000000000000000000000000000000000..35f1fb64bc7448a532b0477724017431e90a2c28 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/User.java @@ -0,0 +1,133 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security; + +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.UserProfile; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * UserDetails implementation which identifies a user by internal (database) and external (AuthProvider + loginId) ID. + * + * @author Daniel Gerhardt + */ +public class User implements org.springframework.security.core.userdetails.UserDetails { + private static final long serialVersionUID = 1L; + + private String id; + private String loginId; + private UserProfile.AuthProvider authProvider; + private String password; + private org.springframework.security.core.userdetails.UserDetails providerUserDetails; + private Collection<? extends GrantedAuthority> authorities; + private boolean enabled; + private String token; + + public User(final UserProfile profile, final Collection<? extends GrantedAuthority> authorities) { + if (profile == null || profile.getId() == null) { + throw new IllegalArgumentException(); + } + id = profile.getId(); + loginId = profile.getLoginId(); + authProvider = profile.getAuthProvider(); + password = profile.getAccount() == null ? null : profile.getAccount().getPassword(); + this.authorities = authorities; + enabled = profile.getAccount() == null || profile.getAccount().getActivationKey() == null; + } + + public User(final UserProfile profile, final Collection<? extends GrantedAuthority> authorities, + final org.springframework.security.core.userdetails.UserDetails details) { + this(profile, authorities); + providerUserDetails = details; + } + + public User(final ClientAuthentication clientAuthentication, final Collection<? extends GrantedAuthority> authorities) { + id = clientAuthentication.getId(); + loginId = clientAuthentication.getUsername(); + authProvider = clientAuthentication.getAuthProvider(); + this.authorities = authorities; + enabled = true; + } + + @Override + public Collection<? extends GrantedAuthority> getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return providerUserDetails != null ? providerUserDetails.getPassword() : password; + } + + @Override + public String getUsername() { + return loginId; + } + + @Override + public boolean isAccountNonExpired() { + return providerUserDetails == null || providerUserDetails.isAccountNonExpired(); + } + + @Override + public boolean isAccountNonLocked() { + return providerUserDetails == null || providerUserDetails.isAccountNonLocked(); + } + + @Override + public boolean isCredentialsNonExpired() { + return providerUserDetails == null || providerUserDetails.isCredentialsNonExpired(); + } + + @Override + public boolean isEnabled() { + return enabled && (providerUserDetails == null || providerUserDetails.isEnabled()); + } + + public UserProfile.AuthProvider getAuthProvider() { + return authProvider; + } + + public String getId() { + return id; + } + + public boolean hasRole(final String role) { + return getAuthorities().stream().anyMatch(ga -> ga.getAuthority().equals("ROLE_" + role)); + } + + public boolean isAdmin() { + return hasRole("ADMIN"); + } + + public String getToken() { + return token; + } + + public void setToken(final String token) { + this.token = token; + } + + @Override + public String toString() { + return String.format("Id: %s, LoginId: %s, AuthProvider: %s, Admin: %b", + id, loginId, authProvider, isAdmin()); + } +} diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtAuthenticationProvider.java b/src/main/java/de/thm/arsnova/security/jwt/JwtAuthenticationProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..569a4df5f46c8288a3290a80d7dde9726b21bc6a --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtAuthenticationProvider.java @@ -0,0 +1,29 @@ +package de.thm.arsnova.security.jwt; + +import de.thm.arsnova.security.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +public class JwtAuthenticationProvider implements AuthenticationProvider { + private JwtService jwtService; + + @Override + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { + final String token = (String) authentication.getCredentials(); + final User user = jwtService.verifyToken((String) authentication.getCredentials()); + + return new JwtToken(token, user, user.getAuthorities()); + } + + @Override + public boolean supports(final Class<?> aClass) { + return JwtToken.class.isAssignableFrom(aClass); + } + + @Autowired + public void setJwtService(final JwtService jwtService) { + this.jwtService = jwtService; + } +} diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtService.java b/src/main/java/de/thm/arsnova/security/jwt/JwtService.java new file mode 100644 index 0000000000000000000000000000000000000000..204a60f55eb630064e4f38e010fa2d59125aad85 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtService.java @@ -0,0 +1,81 @@ +package de.thm.arsnova.security.jwt; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.security.User; +import de.thm.arsnova.service.UserService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.stereotype.Service; + +import java.io.UnsupportedEncodingException; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.TemporalAmount; +import java.util.Collection; +import java.util.Date; +import java.util.stream.Collectors; + +@Service +public class JwtService { + private static final String CONFIG_PREFIX = "security.jwt."; + private static final String ROLE_PREFIX = "ROLE_"; + private static final String ROLES_CLAIM_NAME = "roles"; + private Algorithm algorithm; + private String serverId; + private TemporalAmount defaultValidityPeriod; + private TemporalAmount guestValidityPeriod; + private JWTVerifier verifier; + private UserService userService; + + public JwtService( + final UserService userService, + @Value("${" + CONFIG_PREFIX + "secret}") final String secret, + @Value("${" + CONFIG_PREFIX + "serverId}") final String serverId, + @Value("${" + CONFIG_PREFIX + "validity-period}") final String defaultValidityPeriod) + throws UnsupportedEncodingException { + this.userService = userService; + this.serverId = serverId; + try { + this.defaultValidityPeriod = Duration.parse("P" + defaultValidityPeriod); + } catch (Exception e) { + throw new IllegalArgumentException(defaultValidityPeriod, e); + } + guestValidityPeriod = Duration.parse("P180D"); + algorithm = Algorithm.HMAC256(secret); + verifier = JWT.require(algorithm) + .withAudience(serverId) + .build(); + } + + public String createSignedToken(final User user) { + String[] roles = user.getAuthorities().stream() + .map(ga -> ga.getAuthority()) + .filter(ga -> ga.startsWith(ROLE_PREFIX)) + .map(ga -> ga.substring(ROLE_PREFIX.length())).toArray(String[]::new); + final TemporalAmount expiresAt = user.getAuthProvider() == UserProfile.AuthProvider.ARSNOVA_GUEST + ? guestValidityPeriod : defaultValidityPeriod; + return JWT.create() + .withIssuer(serverId) + .withAudience(serverId) + .withIssuedAt(new Date()) + .withExpiresAt(Date.from(LocalDateTime.now().plus(expiresAt).toInstant(ZoneOffset.UTC))) + .withSubject(user.getId()) + .withArrayClaim(ROLES_CLAIM_NAME, roles) + .sign(algorithm); + } + + public User verifyToken(final String token) { + final DecodedJWT decodedJwt = verifier.verify(token); + final String userId = decodedJwt.getSubject(); + final Collection<GrantedAuthority> authorities = decodedJwt.getClaim(ROLES_CLAIM_NAME).asList(String.class).stream() + .map(role -> new SimpleGrantedAuthority(ROLE_PREFIX + role)).collect(Collectors.toList()); + + return userService.loadUser(userId, authorities); + } +} diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtToken.java b/src/main/java/de/thm/arsnova/security/jwt/JwtToken.java new file mode 100644 index 0000000000000000000000000000000000000000..ecce14b5bebb2669ab1007b076a6cb858f2538dc --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtToken.java @@ -0,0 +1,35 @@ +package de.thm.arsnova.security.jwt; + +import de.thm.arsnova.security.User; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; +import java.util.Collections; + +public class JwtToken extends AbstractAuthenticationToken { + private String token; + private User principal; + + public JwtToken(final String token, final User principal, + final Collection<? extends GrantedAuthority> grantedAuthorities) { + super(grantedAuthorities); + this.token = token; + this.principal = principal; + setAuthenticated(!grantedAuthorities.isEmpty()); + } + + public JwtToken(final String token) { + this(token, null, Collections.emptyList()); + } + + @Override + public Object getCredentials() { + return token; + } + + @Override + public Object getPrincipal() { + return principal; + } +} diff --git a/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java b/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..caa939c037ee3ac7870875201ae24da510e80678 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/jwt/JwtTokenFilter.java @@ -0,0 +1,55 @@ +package de.thm.arsnova.security.jwt; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; + +@Component +public class JwtTokenFilter extends GenericFilterBean { + private static final String JWT_HEADER_NAME = "Arsnova-Auth-Token"; + private static final Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class); + private JwtAuthenticationProvider jwtAuthenticationProvider; + + @Override + public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { + final HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest; + if (httpServletRequest.getRequestURI().startsWith("/v2/")) { + filterChain.doFilter(servletRequest, servletResponse); + return; + } + String jwtHeader = httpServletRequest.getHeader(JWT_HEADER_NAME); + if (jwtHeader != null) { + JwtToken token = new JwtToken(jwtHeader); + try { + Authentication authenticatedToken = jwtAuthenticationProvider.authenticate(token); + if (authenticatedToken != null) { + logger.debug("Storing JWT to SecurityContext: {}", authenticatedToken); + SecurityContextHolder.getContext().setAuthentication(authenticatedToken); + } else { + logger.debug("Could not authenticate JWT."); + } + } catch (final Exception e) { + logger.debug("JWT authentication failed", e); + } + } else { + logger.debug("No authentication header present."); + } + filterChain.doFilter(servletRequest, servletResponse); + } + + @Autowired + public void setJwtAuthenticationProvider(final JwtAuthenticationProvider jwtAuthenticationProvider) { + this.jwtAuthenticationProvider = jwtAuthenticationProvider; + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OAuthToken.java b/src/main/java/de/thm/arsnova/security/pac4j/OAuthToken.java new file mode 100644 index 0000000000000000000000000000000000000000..994bf74b66fe9e87e66f1636db7563d9fdf21305 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OAuthToken.java @@ -0,0 +1,57 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import de.thm.arsnova.security.User; +import org.pac4j.core.profile.CommonProfile; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +/** + * Authentication token implementation for OAuth. + * + * @author Daniel Gerhardt + */ +public class OAuthToken extends AbstractAuthenticationToken { + private User principal; + + public OAuthToken(User principal, CommonProfile profile, + Collection<? extends GrantedAuthority> grantedAuthorities) { + super(grantedAuthorities); + this.principal = principal; + this.setDetails(profile); + setAuthenticated(!grantedAuthorities.isEmpty()); + } + + @Override + public Object getCredentials() { + return null; + } + + @Override + public Object getPrincipal() { + return principal; + } + + @Override + public boolean isAuthenticated() { + return super.isAuthenticated(); + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OauthAuthenticationProvider.java b/src/main/java/de/thm/arsnova/security/pac4j/OauthAuthenticationProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..32262737820e4f81f07795424b3ecc191cf4b81e --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OauthAuthenticationProvider.java @@ -0,0 +1,54 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import de.thm.arsnova.security.User; +import org.pac4j.core.profile.CommonProfile; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.stereotype.Component; + +/** + * Sets up the SecurityContext OAuth users. + * + * @author Daniel Gerhardt + */ +@Component +public class OauthAuthenticationProvider implements AuthenticationProvider { + private OauthUserDetailsService oauthUserDetailsService; + + @Override + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { + OAuthToken oAuthToken = (OAuthToken) authentication; + User user = oauthUserDetailsService.loadUserDetails(oAuthToken); + + return new OAuthToken(user, (CommonProfile) oAuthToken.getDetails(), user.getAuthorities()); + } + + @Override + public boolean supports(final Class<?> aClass) { + return aClass.isAssignableFrom(OAuthToken.class); + } + + @Autowired + public void setOauthUserDetailsService(final OauthUserDetailsService oauthUserDetailsService) { + this.oauthUserDetailsService = oauthUserDetailsService; + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackFilter.java b/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..1b6023f39fa734b15ebb5c5015eaab38ea7069da --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OauthCallbackFilter.java @@ -0,0 +1,86 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import org.pac4j.core.client.Client; +import org.pac4j.core.client.Clients; +import org.pac4j.core.client.IndirectClient; +import org.pac4j.core.config.Config; +import org.pac4j.core.context.J2EContext; +import org.pac4j.core.credentials.Credentials; +import org.pac4j.core.exception.HttpAction; +import org.pac4j.core.profile.CommonProfile; +import org.pac4j.core.util.CommonHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.stereotype.Component; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; + +/** + * Handles callback requests by login redirects from OAuth providers. + * + * @author Daniel Gerhardt + */ +@Component +public class OauthCallbackFilter extends AbstractAuthenticationProcessingFilter { + private static final Logger logger = LoggerFactory.getLogger(OauthCallbackFilter.class); + private Config config; + + public OauthCallbackFilter(Config pac4jConfig) { + super(new AntPathRequestMatcher("/login/oauth")); + this.config = pac4jConfig; + } + + @Override + public Authentication attemptAuthentication( + final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse) + throws AuthenticationException { + CommonProfile profile = retrieveProfile(new J2EContext(httpServletRequest, httpServletResponse)); + return getAuthenticationManager().authenticate(new OAuthToken(null, profile, Collections.emptyList())); + } + + private CommonProfile retrieveProfile(J2EContext context) throws AuthenticationServiceException { + /* Adapted from Pac4j: org.pac4j.core.engine.DefaultCallbackLogic.perform */ + Clients clients = config.getClients(); + CommonHelper.assertNotNull("clients", clients); + Client client = clients.findClient(context); + logger.debug("client: {}", client); + CommonHelper.assertNotNull("client", client); + CommonHelper.assertTrue(client instanceof IndirectClient, + "only indirect clients are allowed on the callback url"); + + try { + Credentials credentials = client.getCredentials(context); + logger.debug("credentials: {}", credentials); + CommonProfile profile = client.getUserProfile(credentials, context); + logger.debug("profile: {}", profile); + + return profile; + } catch (final HttpAction e) { + throw new AuthenticationServiceException(e.getMessage()); + } + } +} diff --git a/src/main/java/de/thm/arsnova/security/pac4j/OauthUserDetailsService.java b/src/main/java/de/thm/arsnova/security/pac4j/OauthUserDetailsService.java new file mode 100644 index 0000000000000000000000000000000000000000..cb61fa8032fbb60b76065c18c9137d2deaad9040 --- /dev/null +++ b/src/main/java/de/thm/arsnova/security/pac4j/OauthUserDetailsService.java @@ -0,0 +1,74 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.security.pac4j; + +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.security.User; +import de.thm.arsnova.service.UserService; +import org.pac4j.oauth.profile.facebook.FacebookProfile; +import org.pac4j.oauth.profile.google2.Google2Profile; +import org.pac4j.oauth.profile.twitter.TwitterProfile; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.AuthenticationUserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +import java.util.Collection; +import java.util.HashSet; + +/** + * Loads UserDetails for an OAuth user (e.g. {@link UserProfile.AuthProvider#GOOGLE}) based on an unique identifier + * extracted from the OAuth profile. + * + * @author Daniel Gerhardt + */ +@Service +public class OauthUserDetailsService implements AuthenticationUserDetailsService<OAuthToken> { + private final UserService userService; + protected final Collection<GrantedAuthority> grantedAuthorities; + + public OauthUserDetailsService(UserService userService) { + this.userService = userService; + grantedAuthorities = new HashSet<>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_OAUTH_USER")); + } + + public User loadUserDetails(final OAuthToken token) + throws UsernameNotFoundException { + User user; + if (token.getDetails() instanceof Google2Profile) { + final Google2Profile profile = (Google2Profile) token.getDetails(); + user = userService.loadUser(UserProfile.AuthProvider.GOOGLE, profile.getEmail(), + grantedAuthorities, true); + } else if (token.getDetails() instanceof TwitterProfile) { + final TwitterProfile profile = (TwitterProfile) token.getDetails(); + user = userService.loadUser(UserProfile.AuthProvider.TWITTER, profile.getUsername(), + grantedAuthorities, true); + } else if (token.getDetails() instanceof FacebookProfile) { + final FacebookProfile profile = (FacebookProfile) token.getDetails(); + user = userService.loadUser(UserProfile.AuthProvider.FACEBOOK, profile.getId(), + grantedAuthorities, true); + } else { + throw new IllegalArgumentException("AuthenticationToken not supported"); + } + + return user; + } +} diff --git a/src/main/java/de/thm/arsnova/service/AnswerService.java b/src/main/java/de/thm/arsnova/service/AnswerService.java new file mode 100644 index 0000000000000000000000000000000000000000..536d410dd8057a67f44134bfb24d00d9111fa3e7 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/AnswerService.java @@ -0,0 +1,59 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.AnswerStatistics; +import de.thm.arsnova.model.TextAnswer; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; + +import java.util.List; +import java.util.Map; + +public interface AnswerService extends EntityService<Answer> { + Answer getMyAnswer(String contentId); + + void getFreetextAnswerAndMarkRead(String answerId, ClientAuthentication user); + + AnswerStatistics getStatistics(String contentId, int piRound); + + AnswerStatistics getStatistics(String contentId); + + AnswerStatistics getAllStatistics(String contentId); + + List<TextAnswer> getTextAnswers(String contentId, int piRound, int offset, int limit); + + List<TextAnswer> getTextAnswers(String contentId, int offset, int limit); + + List<TextAnswer> getAllTextAnswers(String contentId, int offset, int limit); + + int countAnswersByContentIdAndRound(String contentId); + + int countAnswersByContentIdAndRound(String contentId, int piRound); + + List<TextAnswer> getTextAnswersByContentId(String contentId, int offset, int limit); + + List<Answer> getMyAnswersByRoomId(String roomId); + + int countTotalAnswersByRoomId(String roomId); + + int countTotalAnswersByContentId(String contentId); + + void deleteAnswers(String contentId); + + Answer saveAnswer(String contentId, Answer answer); + + Answer updateAnswer(Answer answer); + + void deleteAnswer(String contentId, String answerId); + + Map<String, Object> countAnswersAndAbstentionsInternal(String contentId); + + int countLectureContentAnswers(String roomId); + + int countLectureQuestionAnswersInternal(String roomId); + + int countPreparationContentAnswers(String roomId); + + int countPreparationQuestionAnswersInternal(String roomId); + + int countTotalAbstentionsByContentId(String contentId); +} diff --git a/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java b/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5b5535ad9ae94961325c58fb4a26e2946d54221c --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/AnswerServiceImpl.java @@ -0,0 +1,455 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Answer; +import de.thm.arsnova.model.AnswerStatistics; +import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.TextAnswer; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.AnswerQueueElement; +import de.thm.arsnova.event.DeleteAnswerEvent; +import de.thm.arsnova.event.NewAnswerEvent; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.persistence.AnswerRepository; +import de.thm.arsnova.persistence.ContentRepository; +import de.thm.arsnova.persistence.RoomRepository; +import org.ektorp.DbAccessException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Performs all answer related operations. + */ +@Service +public class AnswerServiceImpl extends DefaultEntityServiceImpl<Answer> + implements AnswerService, ApplicationEventPublisherAware { + private static final Logger logger = LoggerFactory.getLogger(ContentServiceImpl.class); + + private final Queue<AnswerQueueElement> answerQueue = new ConcurrentLinkedQueue<>(); + + private ApplicationEventPublisher publisher; + + private RoomRepository roomRepository; + private ContentRepository contentRepository; + private AnswerRepository answerRepository; + private ContentService contentService; + private UserService userService; + + public AnswerServiceImpl( + AnswerRepository repository, + ContentRepository contentRepository, + RoomRepository roomRepository, + ContentService contentService, + UserService userService, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(Answer.class, repository, jackson2HttpMessageConverter.getObjectMapper()); + this.answerRepository = repository; + this.contentRepository = contentRepository; + this.roomRepository = roomRepository; + this.contentService = contentService; + this.userService = userService; + } + + @Scheduled(fixedDelay = 5000) + public void flushAnswerQueue() { + if (answerQueue.isEmpty()) { + // no need to send an empty bulk request. + return; + } + + final List<Answer> answerList = new ArrayList<>(); + final List<AnswerQueueElement> elements = new ArrayList<>(); + AnswerQueueElement entry; + while ((entry = this.answerQueue.poll()) != null) { + final Answer answer = entry.getAnswer(); + answerList.add(answer); + elements.add(entry); + } + try { + answerRepository.saveAll(answerList); + + // Send NewAnswerEvents ... + for (AnswerQueueElement e : elements) { + this.publisher.publishEvent(new NewAnswerEvent(this, e.getRoom(), e.getAnswer(), e.getUser(), e.getQuestion())); + } + } catch (final DbAccessException e) { + logger.error("Could not bulk save answers from queue.", e); + } + } + + @Override + @PreAuthorize("hasPermission(#contentId, 'content', 'owner')") + public void deleteAnswers(final String contentId) { + final Content content = contentRepository.findOne(contentId); + content.resetState(); + /* FIXME: cancel timer */ + contentService.update(content); + answerRepository.deleteByContentId(content.getId()); + } + + @Override + @PreAuthorize("isAuthenticated()") + public Answer getMyAnswer(final String contentId) { + final Content content = contentService.get(contentId); + if (content == null) { + throw new NotFoundException(); + } + return answerRepository.findByContentIdUserPiRound(contentId, Answer.class, userService.getCurrentUser(), content.getState().getRound()); + } + + @Override + public void getFreetextAnswerAndMarkRead(final String answerId, final ClientAuthentication user) { + final Answer answer = answerRepository.findOne(answerId); + if (!(answer instanceof TextAnswer)) { + throw new NotFoundException(); + } + final TextAnswer textAnswer = (TextAnswer) answer; + if (textAnswer.isRead()) { + return; + } + final Room room = roomRepository.findOne(textAnswer.getRoomId()); + if (room.getOwnerId().equals(user.getId())) { + textAnswer.setRead(true); + answerRepository.save(textAnswer); + } + } + + @Override + @PreAuthorize("isAuthenticated()") + public AnswerStatistics getStatistics(final String contentId, final int round) { + final ChoiceQuestionContent content = (ChoiceQuestionContent) contentRepository.findOne(contentId); + if (content == null) { + throw new NotFoundException(); + } + AnswerStatistics stats = answerRepository.findByContentIdRound( + content.getId(), round, content.getOptions().size()); + /* Fill list with zeros to prevent IndexOutOfBoundsExceptions */ + List<Integer> independentCounts = stats.getRoundStatistics().get(round - 1).getIndependentCounts(); + while (independentCounts.size() < content.getOptions().size()) { + independentCounts.add(0); + } + + return stats; + } + + @Override + @PreAuthorize("isAuthenticated()") + public AnswerStatistics getStatistics(final String contentId) { + final Content content = contentService.get(contentId); + if (content == null) { + throw new NotFoundException(); + } + + return getStatistics(content.getId(), content.getState().getRound()); + } + + @Override + @PreAuthorize("isAuthenticated()") + public AnswerStatistics getAllStatistics(final String contentId) { + final Content content = contentService.get(contentId); + if (content == null) { + throw new NotFoundException(); + } + AnswerStatistics stats = getStatistics(content.getId(), 1); + AnswerStatistics stats2 = getStatistics(content.getId(), 2); + stats.getRoundStatistics().add(stats2.getRoundStatistics().get(1)); + + return stats; + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<TextAnswer> getTextAnswers(final String contentId, final int piRound, final int offset, final int limit) { + /* FIXME: round support not implemented */ + final Content content = contentRepository.findOne(contentId); + if (content == null) { + throw new NotFoundException(); + } + + return getTextAnswersByContentId(contentId, offset, limit); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<TextAnswer> getTextAnswers(final String contentId, final int offset, final int limit) { + return getTextAnswers(contentId, 0, offset, limit); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<TextAnswer> getAllTextAnswers(final String contentId, final int offset, final int limit) { + final Content content = contentService.get(contentId); + if (content == null) { + throw new NotFoundException(); + } + + return getTextAnswersByContentId(contentId, offset, limit); + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countAnswersByContentIdAndRound(final String contentId) { + final Content content = contentService.get(contentId); + if (content == null) { + return 0; + } + + if (content.getFormat() == Content.Format.TEXT) { + return answerRepository.countByContentId(content.getId()); + } else { + return answerRepository.countByContentIdRound(content.getId(), content.getState().getRound()); + } + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countAnswersByContentIdAndRound(final String contentId, final int piRound) { + final Content content = contentService.get(contentId); + if (content == null) { + return 0; + } + + return answerRepository.countByContentIdRound(content.getId(), piRound); + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countTotalAbstentionsByContentId(final String contentId) { + final Content content = contentService.get(contentId); + if (content == null) { + return 0; + } + + return answerRepository.countByContentId(contentId); + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countTotalAnswersByContentId(final String contentId) { + final Content content = contentService.get(contentId); + if (content == null) { + return 0; + } + + return answerRepository.countByContentId(content.getId()); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<TextAnswer> getTextAnswersByContentId(final String contentId, final int offset, final int limit) { + final List<TextAnswer> answers = answerRepository.findByContentId(contentId, TextAnswer.class, offset, limit); + if (answers == null) { + throw new NotFoundException(); + } + + return answers; + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Answer> getMyAnswersByRoomId(final String roomId) { + // Load contents first because we are only interested in answers of the latest piRound. + final List<Content> contents = contentService.getByRoomId(roomId); + final Map<String, Content> contentIdToContent = new HashMap<>(); + for (final Content content : contents) { + contentIdToContent.put(content.getId(), content); + } + + /* filter answers by active piRound per content */ + final List<Answer> answers = answerRepository.findByUserRoomId(userService.getCurrentUser(), roomId); + final List<Answer> filteredAnswers = new ArrayList<>(); + for (final Answer answer : answers) { + final Content content = contentIdToContent.get(answer.getContentId()); + if (content == null) { + // Content is not present. Most likely it has been locked by the + // Room's creator. Locked Questions do not appear in this list. + continue; + } + + // discard all answers that aren't in the same piRound as the content + if (answer.getRound() == content.getState().getRound()) { + filteredAnswers.add(answer); + } + } + + return filteredAnswers; + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countTotalAnswersByRoomId(final String roomId) { + return answerRepository.countByRoomId(roomId); + } + + @Override + @PreAuthorize("isAuthenticated()") + @CacheEvict(value = "answerlists", key = "#contentId") + public Answer saveAnswer(final String contentId, final Answer answer) { + final ClientAuthentication user = userService.getCurrentUser(); + final Content content = contentService.get(contentId); + if (content == null) { + throw new NotFoundException(); + } + final Room room = roomRepository.findOne(content.getRoomId()); + + answer.setCreatorId(user.getId()); + answer.setContentId(content.getId()); + answer.setRoomId(room.getId()); + + /* FIXME: migrate + answer.setQuestionValue(content.calculateValue(answer)); + */ + + if (content.getFormat() == Content.Format.TEXT) { + answer.setRound(0); + /* FIXME: migrate + imageUtils.generateThumbnailImage(answer); + if (content.isFixedAnswer() && content.getBody() != null) { + answer.setAnswerTextRaw(answer.getAnswerText()); + + if (content.isStrictMode()) { + content.checkTextStrictOptions(answer); + } + answer.setQuestionValue(content.evaluateCorrectAnswerFixedText(answer.getAnswerTextRaw())); + answer.setSuccessfulFreeTextAnswer(content.isSuccessfulFreeTextAnswer(answer.getAnswerTextRaw())); + } + */ + } else { + answer.setRound(content.getState().getRound()); + } + + this.answerQueue.offer(new AnswerQueueElement(room, content, answer, user)); + + return answer; + } + + @Override + @PreAuthorize("isAuthenticated()") + @CacheEvict(value = "answerlists", allEntries = true) + public Answer updateAnswer(final Answer answer) { + final ClientAuthentication user = userService.getCurrentUser(); + final Answer realAnswer = this.getMyAnswer(answer.getContentId()); + if (user == null || realAnswer == null || !user.getId().equals(realAnswer.getCreatorId())) { + throw new UnauthorizedException(); + } + + final Content content = contentService.get(answer.getContentId()); + /* FIXME: migrate + if (content.getFormat() == Content.Format.TEXT) { + imageUtils.generateThumbnailImage(realAnswer); + content.checkTextStrictOptions(realAnswer); + } + */ + final Room room = roomRepository.findOne(content.getRoomId()); + answer.setCreatorId(user.getId()); + answer.setContentId(content.getId()); + answer.setRoomId(room.getId()); + answerRepository.save(realAnswer); + this.publisher.publishEvent(new NewAnswerEvent(this, room, answer, user, content)); + + return answer; + } + + @Override + @PreAuthorize("isAuthenticated()") + @CacheEvict(value = "answerlists", allEntries = true) + public void deleteAnswer(final String contentId, final String answerId) { + final Content content = contentRepository.findOne(contentId); + if (content == null) { + throw new NotFoundException(); + } + final ClientAuthentication user = userService.getCurrentUser(); + final Room room = roomRepository.findOne(content.getRoomId()); + if (user == null || room == null || !room.getOwnerId().equals(user.getId())) { + throw new UnauthorizedException(); + } + answerRepository.deleteById(answerId); + + this.publisher.publishEvent(new DeleteAnswerEvent(this, room, content)); + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public int countLectureQuestionAnswersInternal(final String roomId) { + return answerRepository.countByRoomIdOnlyLectureVariant(roomRepository.findOne(roomId).getId()); + } + + @Override + public Map<String, Object> countAnswersAndAbstentionsInternal(final String contentId) { + final Content content = contentService.get(contentId); + HashMap<String, Object> map = new HashMap<>(); + + if (content == null) { + return null; + } + + map.put("_id", contentId); + map.put("answers", answerRepository.countByContentIdRound(content.getId(), content.getState().getRound())); + map.put("abstentions", answerRepository.countByContentId(contentId)); + + return map; + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countLectureContentAnswers(final String roomId) { + return this.countLectureQuestionAnswersInternal(roomId); + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countPreparationContentAnswers(final String roomId) { + return this.countPreparationQuestionAnswersInternal(roomId); + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public int countPreparationQuestionAnswersInternal(final String roomId) { + return answerRepository.countByRoomIdOnlyPreparationVariant(roomRepository.findOne(roomId).getId()); + } + + @Override + public void setApplicationEventPublisher(final ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } +} diff --git a/src/main/java/de/thm/arsnova/service/AttachmentService.java b/src/main/java/de/thm/arsnova/service/AttachmentService.java new file mode 100644 index 0000000000000000000000000000000000000000..cb49b4861fe6f6be556f5fd32c31244bd792b8b7 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/AttachmentService.java @@ -0,0 +1,9 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Attachment; +import org.springframework.web.multipart.MultipartFile; + +public interface AttachmentService extends EntityService<Attachment> { + void upload(Attachment attachment, MultipartFile file); + void download(Attachment attachment); +} diff --git a/src/main/java/de/thm/arsnova/service/AttachmentServiceImpl.java b/src/main/java/de/thm/arsnova/service/AttachmentServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..6b2cc2edfe6c84a9e02e298e1b6b07998a8c6d70 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/AttachmentServiceImpl.java @@ -0,0 +1,28 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Attachment; +import de.thm.arsnova.persistence.AttachmentRepository; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.multipart.MultipartFile; + +public class AttachmentServiceImpl extends DefaultEntityServiceImpl<Attachment> implements AttachmentService { + private AttachmentRepository attachmentRepository; + + public AttachmentServiceImpl( + final AttachmentRepository repository, + @Qualifier("defaultJsonMessageConverter") final MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(Attachment.class, repository, jackson2HttpMessageConverter.getObjectMapper()); + this.attachmentRepository = repository; + } + + public void upload(Attachment attachment, MultipartFile file) { + /* TODO: implement file upload to storage */ + create(attachment); + } + + public void download(Attachment attachment) { + /* TODO: implement file download from external URL to storage */ + create(attachment); + } +} diff --git a/src/main/java/de/thm/arsnova/service/CommentFindQueryService.java b/src/main/java/de/thm/arsnova/service/CommentFindQueryService.java new file mode 100644 index 0000000000000000000000000000000000000000..fdbe5b88644e569b9df24ec3848bfc571ecebd23 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/CommentFindQueryService.java @@ -0,0 +1,48 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.FindQuery; +import de.thm.arsnova.model.Comment; +import org.springframework.stereotype.Service; + +import java.util.HashSet; +import java.util.Set; + +@Service +public class CommentFindQueryService implements FindQueryService<Comment> { + private CommentService commentService; + private UserService userService; + + public CommentFindQueryService(final CommentService commentService, final UserService userService) { + this.commentService = commentService; + this.userService = userService; + } + + @Override + public Set<String> resolveQuery(final FindQuery<Comment> findQuery) { + Set<String> ids = new HashSet<>(); + if (findQuery.getProperties().getRoomId() != null) { + for (Comment c : commentService.getByRoomId(findQuery.getProperties().getRoomId(), 0, 0)) { + ids.add(c.getId()); + } + } + + return ids; + } +} diff --git a/src/main/java/de/thm/arsnova/service/CommentService.java b/src/main/java/de/thm/arsnova/service/CommentService.java new file mode 100644 index 0000000000000000000000000000000000000000..a359bf881312b0c1f99846ade9140444c125e1be --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/CommentService.java @@ -0,0 +1,21 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.migration.v2.CommentReadingCount; + +import java.io.IOException; +import java.util.List; + +public interface CommentService extends EntityService<Comment> { + int count(String roomId); + + CommentReadingCount countRead(String roomId, String username); + + List<Comment> getByRoomId(String roomId, int offset, int limit); + + Comment getAndMarkRead(String commentId) throws IOException; + + void delete(String commentId); + + void deleteByRoomId(String roomId); +} diff --git a/src/main/java/de/thm/arsnova/service/CommentServiceImpl.java b/src/main/java/de/thm/arsnova/service/CommentServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3404880e8cab1bdadc64c4d6504852491002a9cc --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/CommentServiceImpl.java @@ -0,0 +1,151 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.migration.v2.CommentReadingCount; +import de.thm.arsnova.event.DeleteCommentEvent; +import de.thm.arsnova.web.exceptions.ForbiddenException; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.RoomRepository; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Performs all comment related operations. + */ +@Service +public class CommentServiceImpl extends DefaultEntityServiceImpl<Comment> implements CommentService, ApplicationEventPublisherAware { + private UserService userService; + + private CommentRepository commentRepository; + + private RoomRepository roomRepository; + + private ApplicationEventPublisher publisher; + + public CommentServiceImpl( + CommentRepository repository, + RoomRepository roomRepository, + UserService userService, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(Comment.class, repository, jackson2HttpMessageConverter.getObjectMapper()); + this.commentRepository = repository; + this.roomRepository = roomRepository; + this.userService = userService; + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } + + @Override + @PreAuthorize("isAuthenticated()") + public void prepareCreate(final Comment comment) { + final Room room = roomRepository.findOne(comment.getRoomId()); + final ClientAuthentication user = userService.getCurrentUser(); + comment.setCreatorId(user.getId()); + comment.setRead(false); + if (comment.getTimestamp() == null) { + comment.setTimestamp(new Date()); + } + /* TODO: fire event */ + } + + @Override + @PreAuthorize("hasPermission(#commentId, 'comment', 'owner')") + public void delete(final String commentId) { + final Comment comment = commentRepository.findOne(commentId); + if (comment == null) { + throw new NotFoundException(); + } + commentRepository.delete(comment); + + final Room room = roomRepository.findOne(comment.getRoomId()); + final DeleteCommentEvent event = new DeleteCommentEvent(this, room, comment); + this.publisher.publishEvent(event); + } + + @Override + @PreAuthorize("isAuthenticated()") + public void deleteByRoomId(final String roomId) { + final Room room = roomRepository.findOne(roomId); + if (room == null) { + throw new UnauthorizedException(); + } + final ClientAuthentication user = getCurrentUser(); + if (room.getOwnerId().equals(user.getId())) { + commentRepository.deleteByRoomId(room.getId()); + } else { + commentRepository.deleteByRoomIdAndUser(room.getId(), user); + } + } + + @Override + @PreAuthorize("isAuthenticated()") + public int count(final String roomId) { + return commentRepository.countByRoomId(roomId); + } + + @Override + @PreAuthorize("isAuthenticated()") + public CommentReadingCount countRead(final String roomId, String username) { + if (username == null) { + return commentRepository.countReadingByRoomId(roomId); + } else { + ClientAuthentication currentUser = userService.getCurrentUser(); + if (!currentUser.getUsername().equals(username)) { + throw new ForbiddenException(); + } + + return commentRepository.countReadingByRoomIdAndUser(roomId, currentUser); + } + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Comment> getByRoomId(final String roomId, final int offset, final int limit) { + final Room room = roomRepository.findOne(roomId); + final ClientAuthentication user = getCurrentUser(); + if (room.getOwnerId().equals(user.getId())) { + return commentRepository.findByRoomId(room.getId(), offset, limit); + } else { + return commentRepository.findByRoomIdAndUser(room.getId(), user, offset, limit); + } + } + + @Override + @PreAuthorize("hasPermission(#commentId, 'comment', 'update')") + public Comment getAndMarkRead(final String commentId) throws IOException { + final Comment comment = commentRepository.findOne(commentId); + if (comment == null) { + throw new NotFoundException(); + } + Map<String, Object> changes = new HashMap<>(); + changes.put("read", true); + patch(comment, changes); + + return comment; + } + + private ClientAuthentication getCurrentUser() { + final ClientAuthentication user = userService.getCurrentUser(); + if (user == null) { + throw new UnauthorizedException(); + } + return user; + } +} diff --git a/src/main/java/de/thm/arsnova/service/ContentService.java b/src/main/java/de/thm/arsnova/service/ContentService.java new file mode 100644 index 0000000000000000000000000000000000000000..253298413d7e5fc8242fbca3fee26ec8d10e4334 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/ContentService.java @@ -0,0 +1,91 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; + +import java.util.List; + +/** + * The functionality the question service should provide. + */ +public interface ContentService extends EntityService<Content> { + Content get(String id); + + List<Content> getByRoomId(String roomId); + + Iterable<Content> getByRoomIdAndGroup(String roomId, String group); + + int countByRoomId(String roomId); + + int countByRoomIdAndGroup(String roomId, String group); + + void delete(String questionId); + + List<String> getUnAnsweredContentIds(String roomId); + + int countFlashcardsForUserInternal(String roomId); + + void deleteAllContents(String roomId); + + void deleteLectureContents(String roomId); + + void deletePreparationContents(String roomId); + + void deleteFlashcards(String roomId); + + List<String> getUnAnsweredLectureContentIds(String roomId); + + List<String> getUnAnsweredLectureContentIds(String roomId, ClientAuthentication user); + + List<String> getUnAnsweredPreparationContentIds(String roomId); + + List<String> getUnAnsweredPreparationContentIds(String roomId, ClientAuthentication user); + + void publishAll(String roomId, boolean publish); + + void publishContents(String roomId, boolean publish, Iterable<Content> contents); + + void deleteAllContentsAnswers(String roomId); + + void deleteAllPreparationAnswers(String roomId); + + void deleteAllLectureAnswers(String roomId); + + void setVotingAdmission(String contentId, boolean disableVoting); + + void setVotingAdmissions(String roomId, boolean disableVoting, Iterable<Content> contents); +} diff --git a/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..4bd0553726bd4ff711fa46c119364dd6a7b28d24 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/ContentServiceImpl.java @@ -0,0 +1,552 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.event.*; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.persistence.AnswerRepository; +import de.thm.arsnova.persistence.ContentRepository; +import de.thm.arsnova.persistence.LogEntryRepository; +import de.thm.arsnova.persistence.RoomRepository; +import org.ektorp.DocumentNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Performs all content related operations. + */ +@Service +public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implements ContentService, ApplicationEventPublisherAware { + private UserService userService; + + private LogEntryRepository dbLogger; + + private RoomRepository roomRepository; + + private ContentRepository contentRepository; + + private AnswerRepository answerRepository; + + private ApplicationEventPublisher publisher; + + private static final Logger logger = LoggerFactory.getLogger(ContentServiceImpl.class); + + public ContentServiceImpl( + ContentRepository repository, + AnswerRepository answerRepository, + RoomRepository roomRepository, + LogEntryRepository dbLogger, + UserService userService, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(Content.class, repository, jackson2HttpMessageConverter.getObjectMapper()); + this.contentRepository = repository; + this.answerRepository = answerRepository; + this.roomRepository = roomRepository; + this.dbLogger = dbLogger; + this.userService = userService; + } + + @Cacheable("contents") + @Override + public Content get(final String id) { + try { + final Content content = super.get(id); + if (content.getFormat() != Content.Format.TEXT && 0 == content.getState().getRound()) { + /* needed for legacy questions whose piRound property has not been set */ + content.getState().setRound(1); + } + //content.setSessionKeyword(roomRepository.getSessionFromId(content.getRoomId()).getKeyword()); + + return content; + } catch (final DocumentNotFoundException e) { + logger.error("Could not get content {}.", id, e); + } + + return null; + } + + /* FIXME: caching */ + @Override + @PreAuthorize("isAuthenticated()") + //@Cacheable("contentlists") + public List<Content> getByRoomId(final String roomId) { + final Room room = roomRepository.findOne(roomId); + final ClientAuthentication user = userService.getCurrentUser(); + if (room.getOwnerId().equals(user.getId())) { + return contentRepository.findByRoomIdForSpeaker(roomId); + } else { + return contentRepository.findByRoomIdForUsers(roomId); + } + } + + @Override + public Iterable<Content> getByRoomIdAndGroup(final String roomId, final String group) { + final Room room = roomRepository.findOne(roomId); + final Room.ContentGroup contentGroup = room.getContentGroups().get(group); + if (contentGroup == null) { + throw new NotFoundException("Content group does not exist."); + } + Set<String> contentIds = contentGroup.getContentIds(); + + return get(contentIds); + } + + @Override + @PreAuthorize("isAuthenticated()") + public int countByRoomId(final String roomId) { + return contentRepository.countByRoomId(roomId); + } + + @Override + public int countByRoomIdAndGroup(final String roomId, final String group) { + final Room room = roomRepository.findOne(roomId); + final Room.ContentGroup contentGroup = room.getContentGroups().get(group); + if (contentGroup == null) { + throw new NotFoundException("Content group does not exist."); + } + + return contentGroup.getContentIds().size(); + } + + @Override + public void prepareCreate(final Content content) { + content.setTimestamp(new Date()); + + if (content.getFormat() == Content.Format.TEXT) { + content.getState().setRound(0); + } else if (content.getState().getRound() < 1 || content.getState().getRound() > 2) { + content.getState().setRound(1); + } + + /* FIXME: migrate + // convert imageurl to base64 if neccessary + if ("grid".equals(content.getFormat()) && !content.getImage().startsWith("http")) { + // base64 adds offset to filesize, formula taken from: http://en.wikipedia.org/wiki/Base64#MIME + final int fileSize = (int) ((content.getImage().length() - 814) / 1.37); + if (fileSize > uploadFileSizeByte) { + logger.error("Could not save file. File is too large with {} Byte.", fileSize); + throw new BadRequestException(); + } + } + */ + } + + @Override + public void finalizeCreate(final Content content) { + /* Update content groups of room */ + final Room room = roomRepository.findOne(content.getRoomId()); + for (final String groupName : content.getGroups()) { + Room.ContentGroup group = room.getContentGroups().getOrDefault(groupName, new Room.ContentGroup()); + room.getContentGroups().put(groupName, group); + group.getContentIds().add(content.getId()); + } + roomRepository.save(room); + + final NewQuestionEvent event = new NewQuestionEvent(this, room, content); + this.publisher.publishEvent(event); + } + + @Override + public void prepareUpdate(final Content content) { + final ClientAuthentication user = userService.getCurrentUser(); + final Content oldContent = contentRepository.findOne(content.getId()); + if (null == oldContent) { + throw new NotFoundException(); + } + + final Room room = roomRepository.findOne(content.getRoomId()); + if (user == null || room == null || !room.getOwnerId().equals(user.getId())) { + throw new UnauthorizedException(); + } + + if (content.getFormat() == Content.Format.TEXT) { + content.getState().setRound(0); + } else if (content.getState().getRound() < 1 || content.getState().getRound() > 2) { + content.getState().setRound(oldContent.getState().getRound() > 0 ? oldContent.getState().getRound() : 1); + } + + content.setId(oldContent.getId()); + content.setRevision(oldContent.getRevision()); + } + + @Override + public void finalizeUpdate(final Content content) { + /* Update content groups of room */ + final Room room = roomRepository.findOne(content.getRoomId()); + final Set<String> contentsGroupNames = content.getGroups(); + final Set<String> allGroupNames = new HashSet<>(contentsGroupNames); + allGroupNames.addAll(room.getContentGroups().keySet()); + for (final String groupName : allGroupNames) { + Room.ContentGroup group = room.getContentGroups().getOrDefault(groupName, new Room.ContentGroup()); + if (contentsGroupNames.contains(groupName)) { + group.getContentIds().add(content.getId()); + } else { + group.getContentIds().remove(content.getId()); + } + } + roomRepository.save(room); + + /* TODO: not sure yet how to refactor this code - we need access to the old and new entity + if (!oldContent.getState().isVisible() && content.getState().isVisible()) { + final UnlockQuestionEvent event = new UnlockQuestionEvent(this, room, content); + this.publisher.publishEvent(event); + } else if (oldContent.getState().isVisible() && !content.getState().isVisible()) { + final LockQuestionEvent event = new LockQuestionEvent(this, room, content); + this.publisher.publishEvent(event); + } + */ + } + + /* TODO: Only evict cache entry for the content's session. This requires some refactoring. */ + @Override + @PreAuthorize("hasPermission(#contentId, 'content', 'owner')") + @Caching(evict = { + @CacheEvict("answerlists"), + @CacheEvict(value = "contents", key = "#contentId"), + @CacheEvict(value = "contentlists", allEntries = true), + @CacheEvict(value = "lecturecontentlists", allEntries = true /*, condition = "#content.getGroups().contains('lecture')"*/), + @CacheEvict(value = "preparationcontentlists", allEntries = true /*, condition = "#content.getGroups().contains('preparation')"*/), + @CacheEvict(value = "flashcardcontentlists", allEntries = true /*, condition = "#content.getGroups().contains('flashcard')"*/) }) + public void delete(final String contentId) { + final Content content = contentRepository.findOne(contentId); + if (content == null) { + throw new NotFoundException(); + } + + final Room room = roomRepository.findOne(content.getRoomId()); + if (room == null) { + throw new UnauthorizedException(); + } + + for (final String groupName : content.getGroups()) { + Room.ContentGroup group = room.getContentGroups().getOrDefault(groupName, new Room.ContentGroup()); + group.getContentIds().remove(content.getId()); + } + roomRepository.save(room); + + try { + final int count = answerRepository.deleteByContentId(contentId); + contentRepository.deleteById(contentId); + dbLogger.log("delete", "type", "content", "answerCount", count); + } catch (final IllegalArgumentException e) { + logger.error("Could not delete content {}.", contentId, e); + } + + final DeleteQuestionEvent event = new DeleteQuestionEvent(this, room, content); + this.publisher.publishEvent(event); + } + + @PreAuthorize("hasPermission(#session, 'owner')") + @Caching(evict = { + @CacheEvict(value = "contents", allEntries = true), + @CacheEvict(value = "contentlists", key = "#room.getId()"), + @CacheEvict(value = "lecturecontentlists", key = "#room.getId()", condition = "'lecture'.equals(#variant)"), + @CacheEvict(value = "preparationcontentlists", key = "#room.getId()", condition = "'preparation'.equals(#variant)"), + @CacheEvict(value = "flashcardcontentlists", key = "#room.getId()", condition = "'flashcard'.equals(#variant)") }) + private void deleteBySessionAndVariant(final Room room, final String variant) { + final List<String> contentIds; + if ("all".equals(variant)) { + contentIds = contentRepository.findIdsByRoomId(room.getId()); + } else { + contentIds = contentRepository.findIdsByRoomIdAndVariant(room.getId(), variant); + } + + final int answerCount = answerRepository.deleteByContentIds(contentIds); + final int contentCount = contentRepository.deleteByRoomId(room.getId()); + dbLogger.log("delete", "type", "question", "questionCount", contentCount); + dbLogger.log("delete", "type", "answer", "answerCount", answerCount); + + final DeleteAllQuestionsEvent event = new DeleteAllQuestionsEvent(this, room); + this.publisher.publishEvent(event); + } + + @Override + @PreAuthorize("isAuthenticated()") + public void deleteAllContents(final String roomId) { + final Room room = getRoomWithAuthCheck(roomId); + deleteBySessionAndVariant(room, "all"); + } + + @Override + @PreAuthorize("isAuthenticated()") + public void deleteLectureContents(final String roomId) { + final Room room = getRoomWithAuthCheck(roomId); + deleteBySessionAndVariant(room, "lecture"); + } + + @Override + @PreAuthorize("isAuthenticated()") + public void deletePreparationContents(final String roomId) { + final Room room = getRoomWithAuthCheck(roomId); + deleteBySessionAndVariant(room, "preparation"); + } + + @Override + @PreAuthorize("isAuthenticated()") + public void deleteFlashcards(final String roomId) { + final Room room = getRoomWithAuthCheck(roomId); + deleteBySessionAndVariant(room, "flashcard"); + } + + @Override + @PreAuthorize("hasPermission(#contentId, 'content', 'owner')") + public void setVotingAdmission(final String contentId, final boolean disableVoting) { + final Content content = contentRepository.findOne(contentId); + final Room room = roomRepository.findOne(content.getRoomId()); + content.getState().setResponsesEnabled(!disableVoting); + + if (!disableVoting && !content.getState().isVisible()) { + content.getState().setVisible(true); + update(content); + } else { + update(content); + } + ArsnovaEvent event; + if (disableVoting) { + event = new LockVoteEvent(this, room, content); + } else { + event = new UnlockVoteEvent(this, room, content); + } + this.publisher.publishEvent(event); + } + + @Override + @PreAuthorize("isAuthenticated()") + @Caching(evict = { @CacheEvict(value = "contents", allEntries = true), + @CacheEvict(value = "contentlists", key = "#roomId"), + @CacheEvict(value = "lecturecontentlists", key = "#roomId"), + @CacheEvict(value = "preparationcontentlists", key = "#roomId"), + @CacheEvict(value = "flashcardcontentlists", key = "#roomId") }) + public void setVotingAdmissions(final String roomId, final boolean disableVoting, Iterable<Content> contents) { + final ClientAuthentication user = getCurrentUser(); + final Room room = roomRepository.findOne(roomId); + if (!room.getOwnerId().equals(user.getId())) { + throw new UnauthorizedException(); + } + /* FIXME: Filter flashcards - flashcard format not yet implemented */ + //contents.stream().filter(c -> c.getFormat() != Format.?).collect(Collectors.toList()); + final Map<String, Object> patches = new HashMap<>(); + patches.put("responsesEnabled", !disableVoting); + try { + patch(contents, patches, Content::getState); + ArsnovaEvent event; + List<Content> list = new ArrayList<>(); + contents.forEach(list::add); + if (disableVoting) { + event = new LockVotesEvent(this, room, list); + } else { + event = new UnlockVotesEvent(this, room, list); + } + this.publisher.publishEvent(event); + } catch (IOException e) { + logger.error("Patching of contents failed", e); + } + } + + private Room getRoomWithAuthCheck(final String roomId) { + final ClientAuthentication user = userService.getCurrentUser(); + final Room room = roomRepository.findOne(roomId); + if (user == null || room == null || !room.getOwnerId().equals(user.getId())) { + throw new UnauthorizedException(); + } + return room; + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<String> getUnAnsweredContentIds(final String roomId) { + final ClientAuthentication user = getCurrentUser(); + return contentRepository.findUnansweredIdsByRoomIdAndUser(roomId, user); + } + + private ClientAuthentication getCurrentUser() { + final ClientAuthentication user = userService.getCurrentUser(); + if (user == null) { + throw new UnauthorizedException(); + } + return user; + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public int countFlashcardsForUserInternal(final String roomId) { + return contentRepository.findByRoomIdOnlyFlashcardVariantAndActive(roomId).size(); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<String> getUnAnsweredLectureContentIds(final String roomId) { + final ClientAuthentication user = getCurrentUser(); + return this.getUnAnsweredLectureContentIds(roomId, user); + } + + @Override + public List<String> getUnAnsweredLectureContentIds(final String roomId, final ClientAuthentication user) { + return contentRepository.findUnansweredIdsByRoomIdAndUserOnlyLectureVariant(roomId, user); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<String> getUnAnsweredPreparationContentIds(final String roomId) { + final ClientAuthentication user = getCurrentUser(); + return this.getUnAnsweredPreparationContentIds(roomId, user); + } + + @Override + public List<String> getUnAnsweredPreparationContentIds(final String roomId, final ClientAuthentication user) { + return contentRepository.findUnansweredIdsByRoomIdAndUserOnlyPreparationVariant(roomId, user); + } + + @Override + @PreAuthorize("isAuthenticated()") + public void publishAll(final String roomId, final boolean publish) { + /* TODO: resolve redundancies */ + final ClientAuthentication user = getCurrentUser(); + final Room room = roomRepository.findOne(roomId); + if (!room.getOwnerId().equals(user.getId())) { + throw new UnauthorizedException(); + } + final List<Content> contents = contentRepository.findByRoomId(room.getId()); + publishContents(roomId, publish, contents); + } + + @Override + @PreAuthorize("isAuthenticated()") + @Caching(evict = { @CacheEvict(value = "contents", allEntries = true), + @CacheEvict(value = "contentlists", key = "#roomId"), + @CacheEvict(value = "lecturecontentlists", key = "#roomId"), + @CacheEvict(value = "preparationcontentlists", key = "#roomId"), + @CacheEvict(value = "flashcardcontentlists", key = "#roomId") }) + public void publishContents(final String roomId, final boolean publish, Iterable<Content> contents) { + final ClientAuthentication user = getCurrentUser(); + final Room room = roomRepository.findOne(roomId); + if (!room.getOwnerId().equals(user.getId())) { + throw new UnauthorizedException(); + } + for (final Content content : contents) { + content.getState().setVisible(publish); + } + contentRepository.saveAll(contents); + ArsnovaEvent event; + List<Content> list = new ArrayList<>(); + contents.forEach(list::add); + if (publish) { + event = new UnlockQuestionsEvent(this, room, list); + } else { + event = new LockQuestionsEvent(this, room, list); + } + this.publisher.publishEvent(event); + } + + /* TODO: Split and move answer part to AnswerService */ + @Override + @PreAuthorize("isAuthenticated()") + @CacheEvict(value = "answerlists", allEntries = true) + public void deleteAllContentsAnswers(final String roomId) { + final ClientAuthentication user = getCurrentUser(); + final Room room = roomRepository.findOne(roomId); + if (!room.getOwnerId().equals(user.getId())) { + throw new UnauthorizedException(); + } + + final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId()); + resetContentsRoundState(room.getId(), contents); + final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); + answerRepository.deleteAllAnswersForQuestions(contentIds); + + this.publisher.publishEvent(new DeleteAllQuestionsAnswersEvent(this, room)); + } + + /* TODO: Split and move answer part to AnswerService */ + /* TODO: Only evict cache entry for the answer's content. This requires some refactoring. */ + @Override + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") + @CacheEvict(value = "answerlists", allEntries = true) + public void deleteAllPreparationAnswers(String roomId) { + final Room room = roomRepository.findOne(roomId); + + final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId(), "preparation"); + resetContentsRoundState(room.getId(), contents); + final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); + answerRepository.deleteAllAnswersForQuestions(contentIds); + + this.publisher.publishEvent(new DeleteAllPreparationAnswersEvent(this, room)); + } + + /* TODO: Split and move answer part to AnswerService */ + /* TODO: Only evict cache entry for the answer's content. This requires some refactoring. */ + @Override + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") + @CacheEvict(value = "answerlists", allEntries = true) + public void deleteAllLectureAnswers(String roomId) { + final Room room = roomRepository.findOne(roomId); + + final List<Content> contents = contentRepository.findByRoomIdAndVariantAndActive(room.getId(), "lecture"); + resetContentsRoundState(room.getId(), contents); + final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); + answerRepository.deleteAllAnswersForQuestions(contentIds); + + this.publisher.publishEvent(new DeleteAllLectureAnswersEvent(this, room)); + } + + @Caching(evict = { + @CacheEvict(value = "contents", allEntries = true), + @CacheEvict(value = "contentlists", key = "#roomId"), + @CacheEvict(value = "lecturecontentlists", key = "#roomId"), + @CacheEvict(value = "preparationcontentlists", key = "#roomId"), + @CacheEvict(value = "flashcardcontentlists", key = "#roomId") }) + private void resetContentsRoundState(final String roomId, final List<Content> contents) { + for (final Content q : contents) { + /* TODO: Check if setting the sessionId is necessary. */ + q.setRoomId(roomId); + q.resetState(); + } + contentRepository.saveAll(contents); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } +} diff --git a/src/main/java/de/thm/arsnova/services/DefaultEntityServiceImpl.java b/src/main/java/de/thm/arsnova/service/DefaultEntityServiceImpl.java similarity index 57% rename from src/main/java/de/thm/arsnova/services/DefaultEntityServiceImpl.java rename to src/main/java/de/thm/arsnova/service/DefaultEntityServiceImpl.java index c65f2f09fa0680a390fdbf252c39bf2922058a94..9f11f884ba0477dc8facf99bd70416357a3bf461 100644 --- a/src/main/java/de/thm/arsnova/services/DefaultEntityServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/DefaultEntityServiceImpl.java @@ -15,19 +15,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; -import de.thm.arsnova.entities.Entity; -import de.thm.arsnova.entities.serialization.View; -import org.springframework.data.repository.CrudRepository; +import de.thm.arsnova.model.Entity; +import de.thm.arsnova.model.serialization.View; +import de.thm.arsnova.persistence.CrudRepository; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import java.io.IOException; -import java.util.Collection; +import java.util.Date; import java.util.Map; import java.util.function.Function; @@ -55,20 +55,90 @@ public class DefaultEntityServiceImpl<T extends Entity> implements EntityService return repository.findOne(id); } + @Override + public T get(final String id, final boolean internal) { + if (internal) { + T entity = repository.findOne(id); + entity.setInternal(true); + + return entity; + } + + return get(id); + } + + @Override + @PreFilter(value = "hasPermission(filterObject, #this.this.getTypeName(), 'read')", filterTarget = "ids") + public Iterable<T> get(final Iterable<String> ids) { + return repository.findAllById(ids); + } + @Override @PreAuthorize("hasPermission(#entity, 'create')") public T create(final T entity) { if (entity.getId() != null || entity.getRevision() != null) { throw new IllegalArgumentException("Entity is not new."); } - return repository.save(entity); + entity.setCreationTimestamp(new Date()); + + prepareCreate(entity); + final T createdEntity = repository.save(entity); + finalizeCreate(entity); + + return createdEntity; + } + + /** + * This method can be overridden by subclasses to modify the entity before creation. + * + * @param entity The entity to be created + */ + protected void prepareCreate(final T entity) { + + } + + /** + * This method can be overridden by subclasses to modify the entity after creation. + * + * @param entity The entity which has been created + */ + protected void finalizeCreate(final T entity) { + + } + + public T update(final T entity) { + return update(repository.findOne(entity.getId()), entity); } @Override @PreAuthorize("hasPermission(#oldEntity, 'update')") public T update(final T oldEntity, final T newEntity) { newEntity.setId(oldEntity.getId()); - return repository.save(newEntity); + newEntity.setUpdateTimestamp(new Date()); + + prepareUpdate(newEntity); + final T updatedEntity = repository.save(newEntity); + finalizeUpdate(updatedEntity); + + return updatedEntity; + } + + /** + * This method can be overridden by subclasses to modify the entity before updating. + * + * @param entity The entity to be updated + */ + protected void prepareUpdate(final T entity) { + + } + + /** + * This method can be overridden by subclasses to modify the entity after updating. + * + * @param entity The entity which has been updated + */ + protected void finalizeUpdate(final T entity) { + } @Override @@ -84,27 +154,41 @@ public class DefaultEntityServiceImpl<T extends Entity> implements EntityService ObjectReader reader = objectMapper.readerForUpdating(obj).withView(View.Public.class); JsonNode tree = objectMapper.valueToTree(changes); reader.readValue(tree); + entity.setUpdateTimestamp(new Date()); + preparePatch(entity); return repository.save(entity); } @Override - public Iterable<T> patch(final Collection<T> entities, final Map<String, Object> changes) throws IOException { + public Iterable<T> patch(final Iterable<T> entities, final Map<String, Object> changes) throws IOException { return patch(entities, changes, Function.identity()); } @Override @PreFilter(value = "hasPermission(filterObject, 'update')", filterTarget = "entities") - public Iterable<T> patch(final Collection<T> entities, final Map<String, Object> changes, + public Iterable<T> patch(final Iterable<T> entities, final Map<String, Object> changes, final Function<T, ? extends Object> propertyGetter) throws IOException { final JsonNode tree = objectMapper.valueToTree(changes); for (T entity : entities) { Object obj = propertyGetter.apply(entity); ObjectReader reader = objectMapper.readerForUpdating(obj).withView(View.Public.class); reader.readValue(tree); + entity.setUpdateTimestamp(new Date()); + preparePatch(entity); } - return repository.save(entities); + return repository.saveAll(entities); + } + + /** + * This method can be overridden by subclasses to modify the entity before patching. By default, the implementation + * of {@link #prepareUpdate} is used. + * + * @param entity The entity to be patched + */ + protected void preparePatch(final T entity) { + prepareUpdate(entity); } @Override @@ -113,6 +197,15 @@ public class DefaultEntityServiceImpl<T extends Entity> implements EntityService repository.delete(entity); } + /** + * This method can be overridden by subclasses to do additional entity related actions before deletion. + * + * @param entity The entity to be deleted + */ + protected void prepareDelete(final T entity) { + + } + public String getTypeName() { return type.getSimpleName().toLowerCase(); } diff --git a/src/main/java/de/thm/arsnova/services/EntityService.java b/src/main/java/de/thm/arsnova/service/EntityService.java similarity index 79% rename from src/main/java/de/thm/arsnova/services/EntityService.java rename to src/main/java/de/thm/arsnova/service/EntityService.java index 6e8e61fa638c635ed2885f10dbe87f9372b7c784..9158e4f5b61cc74931805d03d651e44d2784a9cf 100644 --- a/src/main/java/de/thm/arsnova/services/EntityService.java +++ b/src/main/java/de/thm/arsnova/service/EntityService.java @@ -15,14 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; -import de.thm.arsnova.entities.Entity; +import de.thm.arsnova.model.Entity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreFilter; import java.io.IOException; -import java.util.Collection; import java.util.Map; import java.util.function.Function; @@ -36,9 +35,16 @@ public interface EntityService<T extends Entity> { @PreAuthorize("hasPermission(#id, #this.this.getTypeName(), 'read')") T get(String id); + T get(String id, boolean internal); + + @PreFilter(value = "hasPermission(filterObject, #this.this.getTypeName(), 'read')", filterTarget = "ids") + Iterable<T> get(Iterable<String> ids); + @PreAuthorize("hasPermission(#entity, 'create')") T create(T entity); + T update(T entity); + @PreAuthorize("hasPermission(#oldEntity, 'update')") T update(T oldEntity, T newEntity); @@ -48,10 +54,10 @@ public interface EntityService<T extends Entity> { T patch(T entity, Map<String, Object> changes, Function<T, ? extends Object> propertyGetter) throws IOException; - Iterable<T> patch(Collection<T> entities, Map<String, Object> changes) throws IOException; + Iterable<T> patch(Iterable<T> entities, Map<String, Object> changes) throws IOException; @PreFilter(value = "hasPermission(filterObject, 'update')", filterTarget = "entities") - Iterable<T> patch(Collection<T> entities, Map<String, Object> changes, Function<T, ? extends Object> propertyGetter) + Iterable<T> patch(Iterable<T> entities, Map<String, Object> changes, Function<T, ? extends Object> propertyGetter) throws IOException; @PreAuthorize("hasPermission(#entity, 'delete')") diff --git a/src/main/java/de/thm/arsnova/services/FeedbackService.java b/src/main/java/de/thm/arsnova/service/FeedbackService.java similarity index 62% rename from src/main/java/de/thm/arsnova/services/FeedbackService.java rename to src/main/java/de/thm/arsnova/service/FeedbackService.java index 78df6399e94e30c375c140f717b831017d123330..1b5ab72e691b963939da7516e512e57aace1e836 100644 --- a/src/main/java/de/thm/arsnova/services/FeedbackService.java +++ b/src/main/java/de/thm/arsnova/service/FeedbackService.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; -import de.thm.arsnova.entities.Feedback; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.Feedback; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; /** * The functionality the feedback service should provide. @@ -26,17 +26,17 @@ import de.thm.arsnova.entities.User; public interface FeedbackService { void cleanFeedbackVotes(); - void cleanFeedbackVotesBySessionKey(String keyword, int cleanupFeedbackDelayInMins); + void cleanFeedbackVotesByRoomId(String roomId, int cleanupFeedbackDelayInMins); - Feedback getBySessionKey(String keyword); + Feedback getByRoomId(String roomId); - int countFeedbackBySessionKey(String keyword); + int countFeedbackByRoomId(String roomId); - double calculateAverageFeedback(String sessionkey); + double calculateAverageFeedback(String roomId); - long calculateRoundedAverageFeedback(String sessionkey); + long calculateRoundedAverageFeedback(String roomId); - boolean save(String keyword, int value, User user); + boolean save(String roomId, int value, ClientAuthentication user); - Integer getBySessionKeyAndUser(String keyword, User user); + Integer getByRoomIdAndUser(String roomId, ClientAuthentication user); } diff --git a/src/main/java/de/thm/arsnova/services/FeedbackServiceImpl.java b/src/main/java/de/thm/arsnova/service/FeedbackServiceImpl.java similarity index 53% rename from src/main/java/de/thm/arsnova/services/FeedbackServiceImpl.java rename to src/main/java/de/thm/arsnova/service/FeedbackServiceImpl.java index 25845d2ae9dfd30bdde805cab4f27be0daab11a2..81a3660135ac3364ead1f25a448b2449dbc57ec2 100644 --- a/src/main/java/de/thm/arsnova/services/FeedbackServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/FeedbackServiceImpl.java @@ -15,16 +15,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Feedback; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.events.DeleteFeedbackForSessionsEvent; -import de.thm.arsnova.events.NewFeedbackEvent; -import de.thm.arsnova.exceptions.NoContentException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.persistance.SessionRepository; +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Feedback; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.event.DeleteFeedbackForRoomsEvent; +import de.thm.arsnova.event.NewFeedbackEvent; +import de.thm.arsnova.web.exceptions.NoContentException; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.persistence.RoomRepository; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; @@ -52,93 +52,93 @@ public class FeedbackServiceImpl implements FeedbackService, ApplicationEventPub @Value("${feedback.cleanup}") private int cleanupFeedbackDelay; - private SessionRepository sessionRepository; + private RoomRepository roomRepository; private FeedbackStorageService feedbackStorage; private ApplicationEventPublisher publisher; - public FeedbackServiceImpl(FeedbackStorageService feedbackStorage, SessionRepository sessionRepository) { + public FeedbackServiceImpl(FeedbackStorageService feedbackStorage, RoomRepository roomRepository) { this.feedbackStorage = feedbackStorage; - this.sessionRepository = sessionRepository; + this.roomRepository = roomRepository; } @Override @Scheduled(fixedDelay = DEFAULT_SCHEDULER_DELAY) public void cleanFeedbackVotes() { - Map<Session, List<User>> deletedFeedbackOfUsersInSession = feedbackStorage.cleanVotes(cleanupFeedbackDelay); + Map<Room, List<ClientAuthentication>> deletedFeedbackOfUsersInSession = feedbackStorage.cleanVotes(cleanupFeedbackDelay); /* - * mapping (Session -> Users) is not suitable for web sockets, because we want to sent all affected + * mapping (Room -> Users) is not suitable for web sockets, because we want to sent all affected * sessions to a single user in one go instead of sending multiple messages for each session. Hence, * we need the mapping (User -> Sessions) */ - final Map<User, Set<Session>> affectedSessionsOfUsers = new HashMap<>(); + final Map<ClientAuthentication, Set<Room>> affectedSessionsOfUsers = new HashMap<>(); - for (Map.Entry<Session, List<User>> entry : deletedFeedbackOfUsersInSession.entrySet()) { - final Session session = entry.getKey(); - final List<User> users = entry.getValue(); - for (User user : users) { - Set<Session> affectedSessions; + for (Map.Entry<Room, List<ClientAuthentication>> entry : deletedFeedbackOfUsersInSession.entrySet()) { + final Room room = entry.getKey(); + final List<ClientAuthentication> users = entry.getValue(); + for (ClientAuthentication user : users) { + Set<Room> affectedSessions; if (affectedSessionsOfUsers.containsKey(user)) { affectedSessions = affectedSessionsOfUsers.get(user); } else { affectedSessions = new HashSet<>(); } - affectedSessions.add(session); + affectedSessions.add(room); affectedSessionsOfUsers.put(user, affectedSessions); } } // Send feedback reset event to all affected users - for (Map.Entry<User, Set<Session>> entry : affectedSessionsOfUsers.entrySet()) { - final User user = entry.getKey(); - final Set<Session> arsSessions = entry.getValue(); - this.publisher.publishEvent(new DeleteFeedbackForSessionsEvent(this, arsSessions, user)); + for (Map.Entry<ClientAuthentication, Set<Room>> entry : affectedSessionsOfUsers.entrySet()) { + final ClientAuthentication user = entry.getKey(); + final Set<Room> arsSessions = entry.getValue(); + this.publisher.publishEvent(new DeleteFeedbackForRoomsEvent(this, arsSessions, user)); } // For each session that has deleted feedback, send the new feedback to all clients - for (Session session : deletedFeedbackOfUsersInSession.keySet()) { + for (Room session : deletedFeedbackOfUsersInSession.keySet()) { this.publisher.publishEvent(new NewFeedbackEvent(this, session)); } } @Override - public void cleanFeedbackVotesBySessionKey(final String keyword, final int cleanupFeedbackDelayInMins) { - final Session session = sessionRepository.findByKeyword(keyword); - List<User> affectedUsers = feedbackStorage.cleanVotesBySession(session, cleanupFeedbackDelayInMins); - Set<Session> sessionSet = new HashSet<>(); - sessionSet.add(session); + public void cleanFeedbackVotesByRoomId(final String roomId, final int cleanupFeedbackDelayInMins) { + final Room room = roomRepository.findOne(roomId); + List<ClientAuthentication> affectedUsers = feedbackStorage.cleanVotesByRoom(room, cleanupFeedbackDelayInMins); + Set<Room> sessionSet = new HashSet<>(); + sessionSet.add(room); // Send feedback reset event to all affected users - for (User user : affectedUsers) { - this.publisher.publishEvent(new DeleteFeedbackForSessionsEvent(this, sessionSet, user)); + for (ClientAuthentication user : affectedUsers) { + this.publisher.publishEvent(new DeleteFeedbackForRoomsEvent(this, sessionSet, user)); } // send the new feedback to all clients in affected session - this.publisher.publishEvent(new NewFeedbackEvent(this, session)); + this.publisher.publishEvent(new NewFeedbackEvent(this, room)); } @Override - public Feedback getBySessionKey(final String keyword) { - final Session session = sessionRepository.findByKeyword(keyword); - if (session == null) { + public Feedback getByRoomId(final String roomId) { + final Room room = roomRepository.findOne(roomId); + if (room == null) { throw new NotFoundException(); } - return feedbackStorage.getBySession(session); + return feedbackStorage.getByRoom(room); } @Override - public int countFeedbackBySessionKey(final String keyword) { - final Feedback feedback = this.getBySessionKey(keyword); + public int countFeedbackByRoomId(final String roomId) { + final Feedback feedback = this.getByRoomId(roomId); final List<Integer> values = feedback.getValues(); return values.get(Feedback.FEEDBACK_FASTER) + values.get(Feedback.FEEDBACK_OK) + values.get(Feedback.FEEDBACK_SLOWER) + values.get(Feedback.FEEDBACK_AWAY); } @Override - public double calculateAverageFeedback(final String sessionkey) { - final Session session = sessionRepository.findByKeyword(sessionkey); - if (session == null) { + public double calculateAverageFeedback(final String roomId) { + final Room room = roomRepository.findOne(roomId); + if (room == null) { throw new NotFoundException(); } - final Feedback feedback = feedbackStorage.getBySession(session); + final Feedback feedback = feedbackStorage.getByRoom(room); final List<Integer> values = feedback.getValues(); final double count = values.get(Feedback.FEEDBACK_FASTER) + values.get(Feedback.FEEDBACK_OK) + values.get(Feedback.FEEDBACK_SLOWER) + values.get(Feedback.FEEDBACK_AWAY); @@ -152,29 +152,29 @@ public class FeedbackServiceImpl implements FeedbackService, ApplicationEventPub } @Override - public long calculateRoundedAverageFeedback(final String sessionkey) { - return Math.round(calculateAverageFeedback(sessionkey)); + public long calculateRoundedAverageFeedback(final String roomId) { + return Math.round(calculateAverageFeedback(roomId)); } @Override - public boolean save(final String keyword, final int value, final User user) { - final Session session = sessionRepository.findByKeyword(keyword); - if (session == null) { + public boolean save(final String roomId, final int value, final ClientAuthentication user) { + final Room room = roomRepository.findOne(roomId); + if (room == null) { throw new NotFoundException(); } - feedbackStorage.save(session, value, user); + feedbackStorage.save(room, value, user); - this.publisher.publishEvent(new NewFeedbackEvent(this, session)); + this.publisher.publishEvent(new NewFeedbackEvent(this, room)); return true; } @Override - public Integer getBySessionKeyAndUser(final String keyword, final User user) { - final Session session = sessionRepository.findByKeyword(keyword); - if (session == null) { + public Integer getByRoomIdAndUser(final String roomId, final ClientAuthentication user) { + final Room room = roomRepository.findOne(roomId); + if (room == null) { throw new NotFoundException(); } - return feedbackStorage.getBySessionAndUser(session, user); + return feedbackStorage.getByRoomAndUser(room, user); } @Override diff --git a/src/main/java/de/thm/arsnova/service/FeedbackStorageService.java b/src/main/java/de/thm/arsnova/service/FeedbackStorageService.java new file mode 100644 index 0000000000000000000000000000000000000000..74dc27e735366df72b2680afaf6038d8586fea35 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/FeedbackStorageService.java @@ -0,0 +1,16 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Feedback; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; + +import java.util.List; +import java.util.Map; + +public interface FeedbackStorageService { + Feedback getByRoom(Room room); + Integer getByRoomAndUser(Room room, ClientAuthentication u); + void save(Room room, int value, ClientAuthentication user); + Map<Room, List<ClientAuthentication>> cleanVotes(int cleanupFeedbackDelay); + List<ClientAuthentication> cleanVotesByRoom(Room room, int cleanupFeedbackDelayInMins); +} diff --git a/src/main/java/de/thm/arsnova/services/FeedbackStorageServiceImpl.java b/src/main/java/de/thm/arsnova/service/FeedbackStorageServiceImpl.java similarity index 53% rename from src/main/java/de/thm/arsnova/services/FeedbackStorageServiceImpl.java rename to src/main/java/de/thm/arsnova/service/FeedbackStorageServiceImpl.java index cb72d608fb2b643d29c9c8933044cb52380a109d..ef8df172c17eab42df5b86ebec22efc794b01d64 100644 --- a/src/main/java/de/thm/arsnova/services/FeedbackStorageServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/FeedbackStorageServiceImpl.java @@ -15,11 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; -import de.thm.arsnova.entities.Feedback; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.Feedback; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; @@ -40,9 +42,9 @@ public class FeedbackStorageServiceImpl implements FeedbackStorageService { private static class FeedbackStorageObject { private final int value; private final Date timestamp; - private final User user; + private final ClientAuthentication user; - public FeedbackStorageObject(final int initValue, final User u) { + public FeedbackStorageObject(final int initValue, final ClientAuthentication u) { value = initValue; timestamp = new Date(); user = u; @@ -54,26 +56,28 @@ public class FeedbackStorageServiceImpl implements FeedbackStorageService { public Date getTimestamp() { return timestamp; } - public boolean fromUser(final User u) { + public boolean fromUser(final ClientAuthentication u) { return user.equals(u); } } - private final Map<Session, Map<User, FeedbackStorageObject>> data = + private static final Logger logger = LoggerFactory.getLogger(FeedbackStorageServiceImpl.class); + + private final Map<Room, Map<ClientAuthentication, FeedbackStorageObject>> data = new ConcurrentHashMap<>(); @Override - public Feedback getBySession(final Session session) { + public Feedback getByRoom(final Room room) { int a = 0; int b = 0; int c = 0; int d = 0; - if (data.get(session) == null) { + if (data.get(room) == null) { return new Feedback(0, 0, 0, 0); } - for (final FeedbackStorageObject fso : data.get(session).values()) { + for (final FeedbackStorageObject fso : data.get(room).values()) { switch (fso.getValue()) { case Feedback.FEEDBACK_FASTER: a++; @@ -95,12 +99,12 @@ public class FeedbackStorageServiceImpl implements FeedbackStorageService { } @Override - public Integer getBySessionAndUser(final Session session, final User u) { - if (data.get(session) == null) { + public Integer getByRoomAndUser(final Room room, final ClientAuthentication u) { + if (data.get(room) == null) { return null; } - for (final FeedbackStorageObject fso : data.get(session).values()) { + for (final FeedbackStorageObject fso : data.get(room).values()) { if (fso.fromUser(u)) { return fso.getValue(); } @@ -111,23 +115,28 @@ public class FeedbackStorageServiceImpl implements FeedbackStorageService { @Override @Transactional(isolation = Isolation.READ_COMMITTED) - public void save(final Session session, final int value, final User user) { - if (data.get(session) == null) { - data.put(session, new ConcurrentHashMap<User, FeedbackStorageObject>()); + public void save(final Room room, final int value, final ClientAuthentication user) { + logger.debug("Feedback data for {} Rooms is stored", data.size()); + logger.debug("Saving feedback: Room: {}, Value: {}, User: {}", room, value, user); + Map<ClientAuthentication, FeedbackStorageObject> roomData = data.get(room); + if (roomData == null) { + logger.debug("Creating new feedback container for Room: {}", room); + roomData = new ConcurrentHashMap<ClientAuthentication, FeedbackStorageObject>(); + data.put(room, roomData); } - - data.get(session).put(user, new FeedbackStorageObject(value, user)); + logger.debug("Feedback values for Room {}: {}", room.getId(), roomData.size()); + roomData.put(user, new FeedbackStorageObject(value, user)); } @Override @Transactional(isolation = Isolation.READ_COMMITTED) - public Map<Session, List<User>> cleanVotes(final int cleanupFeedbackDelay) { - final Map<Session, List<User>> removedFeedbackOfUsersInSession = new HashMap<>(); - for (final Session session : data.keySet()) { - if (session.getFeatures() == null || !session.getFeatures().isLiveClicker()) { - List<User> affectedUsers = cleanVotesBySession(session, cleanupFeedbackDelay); + public Map<Room, List<ClientAuthentication>> cleanVotes(final int cleanupFeedbackDelay) { + final Map<Room, List<ClientAuthentication>> removedFeedbackOfUsersInSession = new HashMap<>(); + for (final Room room : data.keySet()) { + if (!room.getSettings().isQuickSurveyEnabled()) { + List<ClientAuthentication> affectedUsers = cleanVotesByRoom(room, cleanupFeedbackDelay); if (!affectedUsers.isEmpty()) { - removedFeedbackOfUsersInSession.put(session, affectedUsers); + removedFeedbackOfUsersInSession.put(room, affectedUsers); } } } @@ -136,22 +145,22 @@ public class FeedbackStorageServiceImpl implements FeedbackStorageService { @Override @Transactional(isolation = Isolation.READ_COMMITTED) - public List<User> cleanVotesBySession(final Session session, final int cleanupFeedbackDelayInMins) { + public List<ClientAuthentication> cleanVotesByRoom(final Room room, final int cleanupFeedbackDelayInMins) { final long timelimitInMillis = TimeUnit.MILLISECONDS.convert(cleanupFeedbackDelayInMins, TimeUnit.MINUTES); final Date maxAllowedTime = new Date(System.currentTimeMillis() - timelimitInMillis); final boolean forceClean = cleanupFeedbackDelayInMins == 0; - final Map<User, FeedbackStorageObject> sessionFeedbacks = data.get(session); - final List<User> affectedUsers = new ArrayList<>(); + final Map<ClientAuthentication, FeedbackStorageObject> roomFeedbacks = data.get(room); + final List<ClientAuthentication> affectedUsers = new ArrayList<>(); - if (sessionFeedbacks != null) { - for (final Map.Entry<User, FeedbackStorageObject> entry : sessionFeedbacks.entrySet()) { - final User user = entry.getKey(); + if (roomFeedbacks != null) { + for (final Map.Entry<ClientAuthentication, FeedbackStorageObject> entry : roomFeedbacks.entrySet()) { + final ClientAuthentication user = entry.getKey(); final FeedbackStorageObject feedback = entry.getValue(); final boolean timeIsUp = feedback.getTimestamp().before(maxAllowedTime); - final boolean isAwayFeedback = getBySessionAndUser(session, user).equals(Feedback.FEEDBACK_AWAY); + final boolean isAwayFeedback = getByRoomAndUser(room, user).equals(Feedback.FEEDBACK_AWAY); if (forceClean || timeIsUp && !isAwayFeedback) { - sessionFeedbacks.remove(user); + roomFeedbacks.remove(user); affectedUsers.add(user); } } diff --git a/src/main/java/de/thm/arsnova/persistance/UserRepository.java b/src/main/java/de/thm/arsnova/service/FindQueryService.java similarity index 71% rename from src/main/java/de/thm/arsnova/persistance/UserRepository.java rename to src/main/java/de/thm/arsnova/service/FindQueryService.java index 45d911efaa24c36a118ef4429bb66214d3a8b8ba..09f3aae1bfe8b003fc66fb125cff0b4922706451 100644 --- a/src/main/java/de/thm/arsnova/persistance/UserRepository.java +++ b/src/main/java/de/thm/arsnova/service/FindQueryService.java @@ -15,12 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.persistance; +package de.thm.arsnova.service; -import de.thm.arsnova.entities.DbUser; -import org.springframework.data.repository.CrudRepository; +import de.thm.arsnova.model.Entity; +import de.thm.arsnova.model.FindQuery; -public interface UserRepository extends CrudRepository<DbUser, String> { - DbUser findByUsername(String username); - int deleteInactiveUsers(long lastActivityBefore); +import java.util.Set; + +/** + * + * @param <E> Entity type + * @author Daniel Gerhardt + */ +public interface FindQueryService<E extends Entity> { + Set<String> resolveQuery(FindQuery<E> findQuery); } diff --git a/src/main/java/de/thm/arsnova/services/MotdService.java b/src/main/java/de/thm/arsnova/service/MotdService.java similarity index 64% rename from src/main/java/de/thm/arsnova/services/MotdService.java rename to src/main/java/de/thm/arsnova/service/MotdService.java index 9f7e850f70f49d81e949bffb88b01606b0f9febb..6de4cc1d24135a3bec0015c6dfca451ae92683fc 100644 --- a/src/main/java/de/thm/arsnova/services/MotdService.java +++ b/src/main/java/de/thm/arsnova/service/MotdService.java @@ -15,10 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; -import de.thm.arsnova.entities.Motd; -import de.thm.arsnova.entities.MotdList; +import de.thm.arsnova.model.Motd; import java.util.Date; import java.util.List; @@ -27,35 +26,27 @@ import java.util.List; * The functionality the motd service should provide. */ public interface MotdService extends EntityService<Motd> { - Motd getByKey(String keyword); - List<Motd> getAdminMotds(); //all w/o the sessionmotds - List<Motd> getAllSessionMotds(final String sessionkey); + List<Motd> getAllRoomMotds(final String roomId); List<Motd> getCurrentMotds(final Date clientdate, final String audience); - List<Motd> getCurrentSessionMotds(final Date clientdate, final String sessionkey); + List<Motd> getCurrentRoomMotds(final Date clientdate, final String roomId); List<Motd> filterMotdsByDate(List<Motd> list, Date clientdate); - List<Motd> filterMotdsByList(List<Motd> list, MotdList motdList); + List<Motd> filterMotdsByList(List<Motd> list, List<String> ids); void delete(Motd motd); - void deleteBySessionKey(final String sessionkey, Motd motd); + void deleteByRoomId(final String roomId, Motd motd); Motd save(Motd motd); - Motd save(final String sessionkey, final Motd motd); + Motd save(final String roomId, final Motd motd); Motd update(Motd motd); - Motd update(final String sessionkey, Motd motd); - - MotdList getMotdListByUsername(final String username); - - MotdList saveMotdList(MotdList motdList); - - MotdList updateMotdList(MotdList motdList); + Motd update(final String roomId, Motd motd); } diff --git a/src/main/java/de/thm/arsnova/service/MotdServiceImpl.java b/src/main/java/de/thm/arsnova/service/MotdServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..66f6e0142bfe777046468eaf4756310bb8bbf8af --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/MotdServiceImpl.java @@ -0,0 +1,167 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Motd; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.web.exceptions.BadRequestException; +import de.thm.arsnova.persistence.MotdRepository; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Performs all question, interposed question, and answer related operations. + */ +@Service +public class MotdServiceImpl extends DefaultEntityServiceImpl<Motd> implements MotdService { + private UserService userService; + + private RoomService roomService; + + private MotdRepository motdRepository; + + public MotdServiceImpl( + MotdRepository repository, + UserService userService, + RoomService roomService, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(Motd.class, repository, jackson2HttpMessageConverter.getObjectMapper()); + this.motdRepository = repository; + this.userService = userService; + this.roomService = roomService; + } + + @Override + @PreAuthorize("hasPermission('', 'motd', 'admin')") + public List<Motd> getAdminMotds() { + return motdRepository.findGlobalForAdmin(); + } + + @Override + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") + public List<Motd> getAllRoomMotds(final String roomId) { + return motdRepository.findByRoomId(roomId); + } + + @Override + @Cacheable(cacheNames = "motds", key = "'ROOM' + #roomId") + public List<Motd> getCurrentRoomMotds(final Date clientdate, final String roomId) { + final List<Motd> motds = motdRepository.findByRoomId(roomId); + return filterMotdsByDate(motds, clientdate); + } + + @Override + @Cacheable(cacheNames = "motds", key = "#audience") + public List<Motd> getCurrentMotds(final Date clientdate, final String audience) { + final List<Motd> motds; + switch (audience) { + case "all": motds = motdRepository.findGlobalForAll(); break; + case "loggedIn": motds = motdRepository.findGlobalForLoggedIn(); break; + case "students": motds = motdRepository.findForStudents(); break; + case "tutors": motds = motdRepository.findGlobalForTutors(); break; + default: throw new IllegalArgumentException("Invalid audience."); + } + + return filterMotdsByDate(motds, clientdate); + } + + @Override + public List<Motd> filterMotdsByDate(List<Motd> list, Date clientdate) { + List<Motd> returns = new ArrayList<>(); + for (Motd motd : list) { + if (motd.getStartDate().before(clientdate) && motd.getEndDate().after(clientdate)) { + returns.add(motd); + } + } + return returns; + } + + @Override + public List<Motd> filterMotdsByList(List<Motd> list, List<String> ids) { + return list.stream().filter(id -> ids.contains(id)).collect(Collectors.toList()); + } + + @Override + @PreAuthorize("hasPermission('', 'motd', 'admin')") + public Motd save(final Motd motd) { + return createOrUpdateMotd(motd); + } + + @Override + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") + public Motd save(final String roomId, final Motd motd) { + Room room = roomService.get(roomId); + motd.setRoomId(room.getId()); + + return createOrUpdateMotd(motd); + } + + @Override + @PreAuthorize("hasPermission('', 'motd', 'admin')") + public Motd update(final Motd motd) { + return createOrUpdateMotd(motd); + } + + @Override + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") + public Motd update(final String roomId, final Motd motd) { + return createOrUpdateMotd(motd); + } + + @CacheEvict(cacheNames = "motds", key = "#motd.audience + #motd.roomId") + private Motd createOrUpdateMotd(final Motd motd) { + if (motd.getId() != null) { + Motd oldMotd = motdRepository.findOne(motd.getId()); + if (!(motd.getId().equals(oldMotd.getId()) && motd.getRoomId().equals(oldMotd.getRoomId()) + && motd.getAudience().equals(oldMotd.getAudience()))) { + throw new BadRequestException(); + } + } + + if (null != motd.getId()) { + Motd oldMotd = get(motd.getId()); + motd.setId(oldMotd.getId()); + + return super.update(oldMotd, motd); + } + + return super.create(motd); + } + + @Override + @PreAuthorize("hasPermission('', 'motd', 'admin')") + @CacheEvict(cacheNames = "motds", key = "#motd.audience + #motd.roomId") + public void delete(Motd motd) { + motdRepository.delete(motd); + } + + @Override + @PreAuthorize("hasPermission(#roomId, 'room', 'owner')") + public void deleteByRoomId(final String roomId, Motd motd) { + motdRepository.delete(motd); + } +} diff --git a/src/main/java/de/thm/arsnova/services/ResponseProviderService.java b/src/main/java/de/thm/arsnova/service/ResponseProviderService.java similarity index 96% rename from src/main/java/de/thm/arsnova/services/ResponseProviderService.java rename to src/main/java/de/thm/arsnova/service/ResponseProviderService.java index 79aaf92b2fb38340d04cbb44aa5e9e7ac3f4fe24..cc311debf2b2166d140347c266a646e0da538b50 100644 --- a/src/main/java/de/thm/arsnova/services/ResponseProviderService.java +++ b/src/main/java/de/thm/arsnova/service/ResponseProviderService.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; import javax.servlet.http.HttpServletResponse; diff --git a/src/main/java/de/thm/arsnova/services/ResponseProviderServiceImpl.java b/src/main/java/de/thm/arsnova/service/ResponseProviderServiceImpl.java similarity index 97% rename from src/main/java/de/thm/arsnova/services/ResponseProviderServiceImpl.java rename to src/main/java/de/thm/arsnova/service/ResponseProviderServiceImpl.java index abcbba9eb315d42134c138b62ebc5c7b8af1b699..8120714850e44486b1573b5c0ebe7dea627ba72f 100644 --- a/src/main/java/de/thm/arsnova/services/ResponseProviderServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/ResponseProviderServiceImpl.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; diff --git a/src/main/java/de/thm/arsnova/service/RoomFindQueryService.java b/src/main/java/de/thm/arsnova/service/RoomFindQueryService.java new file mode 100644 index 0000000000000000000000000000000000000000..80edfe8eb078b2628add93b0a1fe8712bbd4f2a8 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/RoomFindQueryService.java @@ -0,0 +1,55 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.FindQuery; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.UserProfile; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +public class RoomFindQueryService implements FindQueryService<Room> { + private RoomService roomService; + private UserService userService; + + public RoomFindQueryService(final RoomService roomService, final UserService userService) { + this.roomService = roomService; + this.userService = userService; + } + + @Override + public Set<String> resolveQuery(final FindQuery<Room> findQuery) { + List<List<String>> ids = new ArrayList<>(); + if (findQuery.getExternalFilters().get("inHistoryOfUserId") instanceof String) { + UserProfile inHistoryOfUser = userService.get( + (String) findQuery.getExternalFilters().get("inHistoryOfUserId")); + ids.add(inHistoryOfUser.getRoomHistory().stream() + .map(UserProfile.RoomHistoryEntry::getRoomId).collect(Collectors.toList())); + } + if (findQuery.getProperties().getOwnerId() != null) { + ids.add(roomService.getUserRoomIds(findQuery.getProperties().getOwnerId())); + } + + return ids.stream().flatMap(list -> list.stream()).collect(Collectors.toSet()); + } +} diff --git a/src/main/java/de/thm/arsnova/service/RoomService.java b/src/main/java/de/thm/arsnova/service/RoomService.java new file mode 100644 index 0000000000000000000000000000000000000000..4fb3543326b3daf89d274cec66bdd3f3faf85c6b --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/RoomService.java @@ -0,0 +1,96 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.connector.model.Course; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ImportExportContainer; +import de.thm.arsnova.model.transport.ScoreStatistics; + +import java.util.List; +import java.util.UUID; + +/** + * The functionality the session service should provide. + */ +public interface RoomService extends EntityService<Room> { + String getIdByShortId(String shortId); + + Room getByShortId(String shortId); + + Room getForAdmin(final String id); + + Room getInternal(String id, ClientAuthentication user); + + boolean isShortIdAvailable(String shortId); + + String generateShortId(); + + List<Room> getUserRooms(String userId); + + List<String> getUserRoomIds(String userId); + + List<Room> getUserRoomHistory(String userId); + + List<Room> getMyRooms(int offset, int limit); + + List<Room> getMyRoomHistory(int offset, int limit); + + int countRoomsByCourses(List<Course> courses); + + int activeUsers(String id); + + Room setActive(String id, Boolean lock); + + Room join(String id, UUID socketId); + + Room updateCreator(String id, String newCreator); + + Room updateInternal(Room room, ClientAuthentication user); + + int[] deleteCascading(Room room); + + ScoreStatistics getLearningProgress(String id, String type, String questionVariant); + + ScoreStatistics getMyLearningProgress(String id, String type, String questionVariant); + + List<Room> getMyRoomsInfo(int offset, int limit); + + List<Room> getPublicPoolRoomsInfo(); + + List<Room> getMyPublicPoolRoomsInfo(); + + List<Room> getMyRoomHistoryInfo(int offset, int limit); + + Room importRooms(ImportExportContainer importExportRoom); + + ImportExportContainer exportRoom(String id, Boolean withAnswerStatistics, Boolean withFeedbackQuestions); + + Room copyRoomToPublicPool(String id, ImportExportContainer.PublicPool pp); + + Room.Settings getFeatures(String id); + + Room.Settings updateFeatures(String id, Room.Settings settings); + + boolean lockFeedbackInput(String id, Boolean lock); + + boolean flipFlashcards(String id, Boolean flip); + + void deleteInactiveRooms(); +} diff --git a/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..5546c98c62b9699945c16066b4a5eee0371a0a69 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/RoomServiceImpl.java @@ -0,0 +1,536 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.connector.client.ConnectorClient; +import de.thm.arsnova.connector.model.Course; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.model.transport.ImportExportContainer; +import de.thm.arsnova.model.transport.ScoreStatistics; +import de.thm.arsnova.event.DeleteRoomEvent; +import de.thm.arsnova.event.FeatureChangeEvent; +import de.thm.arsnova.event.FlipFlashcardsEvent; +import de.thm.arsnova.event.LockFeedbackEvent; +import de.thm.arsnova.event.StatusRoomEvent; +import de.thm.arsnova.web.exceptions.ForbiddenException; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.persistence.AnswerRepository; +import de.thm.arsnova.persistence.CommentRepository; +import de.thm.arsnova.persistence.ContentRepository; +import de.thm.arsnova.persistence.LogEntryRepository; +import de.thm.arsnova.persistence.RoomRepository; +import de.thm.arsnova.service.score.ScoreCalculator; +import de.thm.arsnova.service.score.ScoreCalculatorFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.cache.annotation.Caching; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * Performs all room related operations. + */ +@Service +public class RoomServiceImpl extends DefaultEntityServiceImpl<Room> implements RoomService, ApplicationEventPublisherAware { + private static final long ROOM_INACTIVITY_CHECK_INTERVAL_MS = 30 * 60 * 1000L; + + private static final Logger logger = LoggerFactory.getLogger(RoomServiceImpl.class); + + private LogEntryRepository dbLogger; + + private RoomRepository roomRepository; + + private ContentRepository contentRepository; + + private AnswerRepository answerRepository; + + private CommentRepository commentRepository; + + private UserService userService; + + private FeedbackService feedbackService; + + private ScoreCalculatorFactory scoreCalculatorFactory; + + private ConnectorClient connectorClient; + + @Value("${session.guest-session.cleanup-days:0}") + private int guestRoomInactivityThresholdDays; + + @Value("${pp.logofilesize_b}") + private int uploadFileSizeByte; + + private ApplicationEventPublisher publisher; + + public RoomServiceImpl( + RoomRepository repository, + ContentRepository contentRepository, + AnswerRepository answerRepository, + CommentRepository commentRepository, + LogEntryRepository dbLogger, + UserService userService, + FeedbackService feedbackService, + ScoreCalculatorFactory scoreCalculatorFactory, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(Room.class, repository, jackson2HttpMessageConverter.getObjectMapper()); + this.roomRepository = repository; + this.contentRepository = contentRepository; + this.answerRepository = answerRepository; + this.commentRepository = commentRepository; + this.dbLogger = dbLogger; + this.userService = userService; + this.feedbackService = feedbackService; + this.scoreCalculatorFactory = scoreCalculatorFactory; + } + + public static class RoomNameComparator implements Comparator<Room>, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public int compare(final Room room1, final Room room2) { + return room1.getName().compareToIgnoreCase(room2.getName()); + } + } + + public static class RoomShortNameComparator implements Comparator<Room>, Serializable { + private static final long serialVersionUID = 1L; + + @Override + public int compare(final Room room1, final Room room2) { + return room1.getAbbreviation().compareToIgnoreCase(room2.getAbbreviation()); + } + } + + @Autowired(required = false) + public void setConnectorClient(ConnectorClient connectorClient) { + this.connectorClient = connectorClient; + } + + @Scheduled(fixedDelay = ROOM_INACTIVITY_CHECK_INTERVAL_MS) + public void deleteInactiveRooms() { + if (guestRoomInactivityThresholdDays > 0) { + logger.info("Delete inactive rooms."); + long unixTime = System.currentTimeMillis(); + long lastActivityBefore = unixTime - guestRoomInactivityThresholdDays * 24 * 60 * 60 * 1000L; + int totalCount[] = new int[] {0, 0, 0}; + List<Room> inactiveRooms = roomRepository.findInactiveGuestRoomsMetadata(lastActivityBefore); + for (Room room : inactiveRooms) { + int[] count = deleteCascading(room); + totalCount[0] += count[0]; + totalCount[1] += count[1]; + totalCount[2] += count[2]; + } + + if (!inactiveRooms.isEmpty()) { + logger.info("Deleted {} inactive guest rooms.", inactiveRooms.size()); + dbLogger.log("cleanup", "type", "session", + "sessionCount", inactiveRooms.size(), + "questionCount", totalCount[0], + "answerCount", totalCount[1], + "commentCount", totalCount[2]); + } + } + } + + @Override + public Room join(final String id, final UUID socketId) { + Room room = null != id ? roomRepository.findOne(id) : null; + if (null == room) { + userService.removeUserFromRoomBySocketId(socketId); + return null; + } + + /* FIXME: migrate LMS course support + if (connectorClient != null && room.isCourseSession()) { + final String courseid = room.getCourseId(); + if (!connectorClient.getMembership(user.getUsername(), courseid).isMember()) { + throw new ForbiddenException("User is no course member."); + } + } + */ + + userService.addUserToRoomBySocketId(socketId, id); + userService.addRoomToHistory(userService.getCurrentUserProfile(), room); + + return room; + } + + @Override + @PreAuthorize("isAuthenticated()") + public Room getByShortId(final String shortId) { + final ClientAuthentication user = userService.getCurrentUser(); + return this.getInternal(getIdByShortId(shortId), user); + } + + @Override + @Cacheable("room.id-by-shortid") + public String getIdByShortId(final String shortId) { + if (shortId == null) { + throw new NullPointerException("shortId cannot be null"); + } + Room room = roomRepository.findByShortId(shortId); + if (room == null) { + throw new NotFoundException("No Room exists for short ID"); + } + + return room.getId(); + } + + @PreAuthorize("hasPermission(#id, 'room', 'owner')") + public Room getForAdmin(final String id) { + return roomRepository.findOne(id); + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public Room getInternal(final String id, final ClientAuthentication user) { + final Room room = roomRepository.findOne(id); + if (room == null) { + throw new NotFoundException(); + } + if (room.isClosed() && !room.getOwnerId().equals(user.getId())) { + throw new ForbiddenException("User is not room creator."); + } + + /* FIXME: migrate LMS course support + if (connectorClient != null && room.isCourseSession()) { + final String courseid = room.getCourseId(); + if (!connectorClient.getMembership(user.getUsername(), courseid).isMember()) { + throw new ForbiddenException("User is no course member."); + } + } + */ + + return room; + } + + /* TODO: Updated SpEL expression has not been tested yet */ + @Override + @PreAuthorize("isAuthenticated() and hasPermission(#userId, 'userprofile', 'owner')") + public List<Room> getUserRooms(String userId) { + return roomRepository.findByOwnerId(userId, 0, 0); + } + + /* TODO: Updated SpEL expression has not been tested yet */ + @Override + @PreAuthorize("isAuthenticated() and hasPermission(#userId, 'userprofile', 'owner')") + public List<String> getUserRoomIds(String userId) { + return roomRepository.findIdsByOwnerId(userId); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Room> getMyRooms(final int offset, final int limit) { + return roomRepository.findByOwner(userService.getCurrentUser(), offset, limit); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Room> getPublicPoolRoomsInfo() { + return roomRepository.findInfosForPublicPool(); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Room> getMyPublicPoolRoomsInfo() { + return roomRepository.findInfosForPublicPoolByOwner(userService.getCurrentUser()); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Room> getMyRoomsInfo(final int offset, final int limit) { + final ClientAuthentication user = userService.getCurrentUser(); + return roomRepository.getRoomsWithStatsForOwner(user, offset, limit); + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Room> getMyRoomHistory(final int offset, final int limit) { + /* TODO: implement pagination */ + return getUserRoomHistory(userService.getCurrentUser().getId()); + } + + @Override + @PreAuthorize("hasPermission(#userId, 'userprofile', 'read')") + public List<Room> getUserRoomHistory(final String userId) { + final UserProfile profile = userService.get(userId); + final List<String> roomIds = profile.getRoomHistory().stream().map(entry -> entry.getRoomId()).collect(Collectors.toList()); + List<Room> rooms = new ArrayList<>(); + roomRepository.findAllById(roomIds).forEach(rooms::add); + + return rooms; + } + + @Override + @PreAuthorize("isAuthenticated()") + public List<Room> getMyRoomHistoryInfo(final int offset, final int limit) { + List<Room> rooms = getMyRoomHistory(0, 0); + roomRepository.getRoomHistoryWithStatsForUser(rooms, userService.getCurrentUser()); + + return rooms; + } + + @Override + /* TODO: move caching to DefaultEntityServiceImpl */ + //@Caching(evict = @CacheEvict(cacheNames = "rooms", key = "#result.id")) + public void prepareCreate(final Room room) { + /* FIXME: migrate LMS course support + if (connectorClient != null && room.getCourseId() != null) { + if (!connectorClient.getMembership( + userService.getCurrentUser().getUsername(), room.getCourseId()).isMember() + ) { + throw new ForbiddenException(); + } + } + */ + + handleLogo(room); + + Room.Settings sf = new Room.Settings(); + room.setSettings(sf); + + room.setShortId(generateShortId()); + room.setOwnerId(userService.getCurrentUser().getId()); + room.setClosed(false); + + /* FIXME: event */ + // this.publisher.publishEvent(new NewRoomEvent(this, result)); + } + + @Override + public boolean isShortIdAvailable(final String shortId) { + try { + return getIdByShortId(shortId) == null; + } catch (final NotFoundException e) { + return true; + } + } + + @Override + public String generateShortId() { + final int low = 10000000; + final int high = 100000000; + final String keyword = String + .valueOf((int) (Math.random() * (high - low) + low)); + + if (isShortIdAvailable(keyword)) { + return keyword; + } + return generateShortId(); + } + + @Override + public int countRoomsByCourses(final List<Course> courses) { + final List<Room> rooms = roomRepository.findRoomsByCourses(courses); + if (rooms == null) { + return 0; + } + return rooms.size(); + } + + @Override + public int activeUsers(final String id) { + return userService.getUsersByRoomId(id).size(); + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'owner')") + public Room setActive(final String id, final Boolean lock) { + final Room room = roomRepository.findOne(id); + room.setClosed(!lock); + this.publisher.publishEvent(new StatusRoomEvent(this, room)); + roomRepository.save(room); + + return room; + } + + @Override + /* TODO: move caching to DefaultEntityServiceImpl */ + //@CachePut(value = "rooms", key = "#room") + protected void prepareUpdate(final Room room) { + final Room existingRoom = roomRepository.findOne(room.getId()); + room.setOwnerId(existingRoom.getOwnerId()); + handleLogo(room); + + /* TODO: only publish event when feedback has changed */ + /* FIXME: event */ + // this.publisher.publishEvent(new FeatureChangeEvent(this, room)); + } + + @Override + @PreAuthorize("hasPermission('', 'motd', 'admin')") + @Caching(evict = { @CacheEvict("rooms"), @CacheEvict(cacheNames = "rooms", key = "#id") }) + public Room updateCreator(String id, String newCreator) { + throw new UnsupportedOperationException("No longer implemented."); + } + + /* + * The "internal" suffix means it is called by internal services that have no authentication! + * TODO: Find a better way of doing this... + */ + @Override + public Room updateInternal(final Room room, final ClientAuthentication user) { + if (room.getOwnerId().equals(user.getId())) { + roomRepository.save(room); + return room; + } + return null; + } + + @Override + @PreAuthorize("hasPermission(#room, 'owner')") + @Caching(evict = { + @CacheEvict("rooms"), + @CacheEvict(value = "room.id-by-shortid", key = "#room.shortId") + }) + public int[] deleteCascading(final Room room) { + int[] count = new int[] {0, 0, 0}; + List<String> contentIds = contentRepository.findIdsByRoomId(room.getId()); + count[2] = commentRepository.deleteByRoomId(room.getId()); + count[1] = answerRepository.deleteByContentIds(contentIds); + count[0] = contentRepository.deleteByRoomId(room.getId()); + roomRepository.delete(room); + logger.debug("Deleted room document {} and related data.", room.getId()); + dbLogger.log("delete", "type", "session", "id", room.getId()); + + this.publisher.publishEvent(new DeleteRoomEvent(this, room)); + + return count; + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'read')") + public ScoreStatistics getLearningProgress(final String id, final String type, final String questionVariant) { + final Room room = roomRepository.findOne(id); + ScoreCalculator scoreCalculator = scoreCalculatorFactory.create(type, questionVariant); + return scoreCalculator.getCourseProgress(room); + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'read')") + public ScoreStatistics getMyLearningProgress(final String id, final String type, final String questionVariant) { + final Room room = roomRepository.findOne(id); + final ClientAuthentication user = userService.getCurrentUser(); + ScoreCalculator scoreCalculator = scoreCalculatorFactory.create(type, questionVariant); + return scoreCalculator.getMyProgress(room, user); + } + + @Override + @PreAuthorize("hasPermission('', 'room', 'create')") + public Room importRooms(ImportExportContainer importRoom) { + final ClientAuthentication user = userService.getCurrentUser(); + final Room info = roomRepository.importRoom(user, importRoom); + if (info == null) { + throw new NullPointerException("Could not import room."); + } + return info; + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'owner')") + public ImportExportContainer exportRoom(String id, Boolean withAnswerStatistics, Boolean withFeedbackQuestions) { + return roomRepository.exportRoom(id, withAnswerStatistics, withFeedbackQuestions); + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'owner')") + public Room copyRoomToPublicPool(String id, ImportExportContainer.PublicPool pp) { + ImportExportContainer temp = roomRepository.exportRoom(id, false, false); + temp.getSession().setPublicPool(pp); + temp.getSession().setSessionType("public_pool"); + final ClientAuthentication user = userService.getCurrentUser(); + return roomRepository.importRoom(user, temp); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'read')") + public Room.Settings getFeatures(String id) { + return roomRepository.findOne(id).getSettings(); + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'owner')") + public Room.Settings updateFeatures(String id, Room.Settings settings) { + final Room room = roomRepository.findOne(id); + final ClientAuthentication user = userService.getCurrentUser(); + room.setSettings(settings); + this.publisher.publishEvent(new FeatureChangeEvent(this, room)); + roomRepository.save(room); + + return room.getSettings(); + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'owner')") + public boolean lockFeedbackInput(String id, Boolean lock) { + final Room room = roomRepository.findOne(id); + final ClientAuthentication user = userService.getCurrentUser(); + if (!lock) { + feedbackService.cleanFeedbackVotesByRoomId(id, 0); + } + + room.getSettings().setFeedbackLocked(lock); + this.publisher.publishEvent(new LockFeedbackEvent(this, room)); + roomRepository.save(room); + + return room.getSettings().isFeedbackLocked(); + } + + @Override + @PreAuthorize("hasPermission(#id, 'room', 'owner')") + public boolean flipFlashcards(String id, Boolean flip) { + final Room room = roomRepository.findOne(id); + this.publisher.publishEvent(new FlipFlashcardsEvent(this, room)); + + return flip; + } + + private void handleLogo(Room room) { + if (room.getAuthor() != null && room.getAuthor().getOrganizationLogo() != null) { + if (!room.getAuthor().getOrganizationLogo().startsWith("http")) { + throw new IllegalArgumentException("Invalid logo URL."); + } + } + } +} diff --git a/src/main/java/de/thm/arsnova/services/StatisticsService.java b/src/main/java/de/thm/arsnova/service/StatisticsService.java similarity index 92% rename from src/main/java/de/thm/arsnova/services/StatisticsService.java rename to src/main/java/de/thm/arsnova/service/StatisticsService.java index 0739baa2f2917990c291477b183a431a1884eb35..aa74d212dd6bf569213e5e21bc1aa563f140921c 100644 --- a/src/main/java/de/thm/arsnova/services/StatisticsService.java +++ b/src/main/java/de/thm/arsnova/service/StatisticsService.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; -import de.thm.arsnova.entities.Statistics; +import de.thm.arsnova.model.Statistics; /** * The functionality the statistics service should provide. diff --git a/src/main/java/de/thm/arsnova/services/StatisticsServiceImpl.java b/src/main/java/de/thm/arsnova/service/StatisticsServiceImpl.java similarity index 93% rename from src/main/java/de/thm/arsnova/services/StatisticsServiceImpl.java rename to src/main/java/de/thm/arsnova/service/StatisticsServiceImpl.java index 828607a40a18682064fea57c82dde00a08147feb..53ba877279b8203b1b0c95116336aa648c7071f5 100644 --- a/src/main/java/de/thm/arsnova/services/StatisticsServiceImpl.java +++ b/src/main/java/de/thm/arsnova/service/StatisticsServiceImpl.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; -import de.thm.arsnova.entities.Statistics; -import de.thm.arsnova.persistance.StatisticsRepository; +import de.thm.arsnova.model.Statistics; +import de.thm.arsnova.persistence.StatisticsRepository; import org.springframework.cache.annotation.Cacheable; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; diff --git a/src/main/java/de/thm/arsnova/service/StatusService.java b/src/main/java/de/thm/arsnova/service/StatusService.java new file mode 100644 index 0000000000000000000000000000000000000000..7052dfb9af34b6ac4d2b8b7bce9b407d115fabaf --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/StatusService.java @@ -0,0 +1,27 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import java.util.Map; + +public interface StatusService { + void putMaintenanceReason(Class<?> type, String reason); + void removeMaintenanceReason(Class<?> type); + Map<Class<?>, String> getMaintenanceReasons(); + boolean isMaintenanceActive(); +} diff --git a/src/main/java/de/thm/arsnova/service/StatusServiceImpl.java b/src/main/java/de/thm/arsnova/service/StatusServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d1ca23dd2111fba6281712a0a927aff02e7b3105 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/StatusServiceImpl.java @@ -0,0 +1,54 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +/** + * Keeps track of maintenance reasons registered by other components. While at least one maintenance reason is active, + * API access is blocked by {@link de.thm.arsnova.web.MaintenanceModeFilter}. + * + * @author Daniel Gerhardt + */ +@Service +public class StatusServiceImpl implements StatusService { + private final Map<Class<?>, String> maintenanceReasons = new HashMap<>(); + + @Override + public void putMaintenanceReason(final Class<?> type, final String reason) { + maintenanceReasons.put(type, reason); + } + + @Override + public void removeMaintenanceReason(final Class<?> type) { + maintenanceReasons.remove(type); + } + + @Override + public Map<Class<?>, String> getMaintenanceReasons() { + return maintenanceReasons; + } + + @Override + public boolean isMaintenanceActive() { + return !maintenanceReasons.isEmpty(); + } +} diff --git a/src/main/java/de/thm/arsnova/service/TimerService.java b/src/main/java/de/thm/arsnova/service/TimerService.java new file mode 100644 index 0000000000000000000000000000000000000000..1db313d6390b890c8aae9814715ea18e025665b9 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/TimerService.java @@ -0,0 +1,11 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.migration.v2.ClientAuthentication; + +public interface TimerService { + void startNewRound(final String contentId, ClientAuthentication user); + void startNewRoundDelayed(final String contentId, final int time); + void cancelRoundChange(final String contentId); + void cancelDelayedRoundChange(final String contentId); + void resetRoundState(final String contentId); +} diff --git a/src/main/java/de/thm/arsnova/service/TimerServiceImpl.java b/src/main/java/de/thm/arsnova/service/TimerServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..b676a41e1e9120e199bfad295b201d3a8834ca34 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/TimerServiceImpl.java @@ -0,0 +1,169 @@ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.event.PiRoundCancelEvent; +import de.thm.arsnova.event.PiRoundDelayedStartEvent; +import de.thm.arsnova.event.PiRoundEndEvent; +import de.thm.arsnova.event.PiRoundResetEvent; +import de.thm.arsnova.persistence.AnswerRepository; +import de.thm.arsnova.persistence.ContentRepository; +import de.thm.arsnova.persistence.RoomRepository; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.ApplicationEventPublisherAware; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.HashMap; +import java.util.Timer; +import java.util.TimerTask; + +@Service +public class TimerServiceImpl implements TimerService, ApplicationEventPublisherAware { + private HashMap<String, Timer> timerList = new HashMap<>(); + private UserService userService; + private RoomRepository roomRepository; + private ContentRepository contentRepository; + private AnswerRepository answerRepository; + private ApplicationEventPublisher publisher; + + public TimerServiceImpl(final UserService userService, final RoomRepository roomRepository, + final ContentRepository contentRepository, final AnswerRepository answerRepository) { + this.userService = userService; + this.roomRepository = roomRepository; + this.contentRepository = contentRepository; + this.answerRepository = answerRepository; + } + + @Override + @PreAuthorize("isAuthenticated() and hasPermission(#contentId, 'content', 'owner')") + public void startNewRound(final String contentId, ClientAuthentication user) { + final Content content = contentRepository.findOne(contentId); + final Room room = roomRepository.findOne(content.getRoomId()); + + if (null == user) { + user = userService.getCurrentUser(); + } + + cancelDelayedRoundChange(contentId); + + content.getState().setRoundEndTimestamp(null); + content.getState().setResponsesEnabled(false); + updateRoundManagementState(content); + contentRepository.save(content); + + this.publisher.publishEvent(new PiRoundEndEvent(this, room, content)); + } + + @Override + @PreAuthorize("hasPermission(#contentId, 'content', 'owner')") + public void startNewRoundDelayed(final String contentId, final int time) { + final ClientAuthentication user = userService.getCurrentUser(); + final Content content = contentRepository.findOne(contentId); + final Room room = roomRepository.findOne(content.getRoomId()); + + final Date date = new Date(); + final Timer timer = new Timer(); + final Date endDate = new Date(date.getTime() + (time * 1000)); + updateRoundStartVariables(content, date, endDate); + contentRepository.save(content); + + this.publisher.publishEvent(new PiRoundDelayedStartEvent(this, room, content)); + timerList.put(contentId, timer); + + timer.schedule(new TimerTask() { + @Override + public void run() { + startNewRound(contentId, user); + } + }, endDate); + } + + @Override + @PreAuthorize("hasPermission(#contentId, 'content', 'owner')") + public void cancelRoundChange(final String contentId) { + final Content content = contentRepository.findOne(contentId); + final Room room = roomRepository.findOne(content.getRoomId()); + + cancelDelayedRoundChange(contentId); + resetRoundManagementState(content); + + if (content.getState().getRound() > 1) { + content.getState().setRound(content.getState().getRound() - 1); + } + content.getState().setRoundEndTimestamp(null); + + contentRepository.save(content); + this.publisher.publishEvent(new PiRoundCancelEvent(this, room, content)); + } + + @Override + public void cancelDelayedRoundChange(final String contentId) { + Timer timer = timerList.get(contentId); + + if (null != timer) { + timer.cancel(); + timerList.remove(contentId); + timer.purge(); + } + } + + @Override + @PreAuthorize("hasPermission(#contentId, 'content', 'owner')") + @CacheEvict("answerlists") + public void resetRoundState(final String contentId) { + final Content content = contentRepository.findOne(contentId); + final Room room = roomRepository.findOne(content.getRoomId()); + cancelDelayedRoundChange(contentId); + + if ("freetext".equals(content.getFormat())) { + content.getState().setRound(0); + } else { + content.getState().setRound(1); + } + + resetRoundManagementState(content); + answerRepository.deleteByContentId(content.getId()); + contentRepository.save(content); + this.publisher.publishEvent(new PiRoundResetEvent(this, room, content)); + } + + private void updateRoundStartVariables(final Content content, final Date start, final Date end) { + if (content.getState().getRound() == 1 && content.getState().getRoundEndTimestamp() == null) { + content.getState().setRound(2); + } + + content.getState().setVisible(true); + content.getState().setSolutionVisible(false); + content.getState().setResponsesVisible(false); + content.getState().setResponsesEnabled(true); + content.getState().setRoundEndTimestamp(end); + } + + private void updateRoundManagementState(final Content content) { + if (content.getState().getRoundEndTimestamp() != null && new Date().compareTo(content.getState().getRoundEndTimestamp()) > 0) { + content.getState().setRoundEndTimestamp(null); + } + } + + private void resetRoundManagementState(final Content content) { + content.getState().setSolutionVisible(false); + content.getState().setResponsesVisible(false); + content.getState().setResponsesEnabled(false); + content.getState().setRoundEndTimestamp(null); + } + + private void resetContentState(final Content content) { + content.getState().setResponsesEnabled(true); + content.getState().setRound(1); + content.getState().setRoundEndTimestamp(null); + } + + @Override + public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { + this.publisher = applicationEventPublisher; + } +} diff --git a/src/main/java/de/thm/arsnova/service/UserService.java b/src/main/java/de/thm/arsnova/service/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..0f2138223930a86752a700ba28b33679942cd107 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/UserService.java @@ -0,0 +1,93 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.security.User; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +/** + * The functionality the user service should provide. + */ +public interface UserService extends EntityService<UserProfile> { + UserProfile getCurrentUserProfile(); + + ClientAuthentication getCurrentUser(); + + de.thm.arsnova.model.ClientAuthentication getCurrentClientAuthentication(); + + boolean isAdmin(String username); + + boolean isBannedFromLogin(String addr); + + void increaseFailedLoginCount(String addr); + + ClientAuthentication getUserToSocketId(UUID socketId); + + void putUserToSocketId(UUID socketId, ClientAuthentication user); + + void removeUserToSocketId(UUID socketId); + + Set<Map.Entry<UUID, ClientAuthentication>> getSocketIdToUser(); + + boolean isUserInRoom(ClientAuthentication user, String roomId); + + Set<ClientAuthentication> getUsersByRoomId(String roomId); + + String getRoomIdByUserId(String userId); + + void addUserToRoomBySocketId(UUID socketId, String roomId); + + void removeUserFromRoomBySocketId(UUID socketId); + + void removeUserFromMaps(ClientAuthentication user); + + int loggedInUsers(); + + void authenticate(UsernamePasswordAuthenticationToken token, UserProfile.AuthProvider authProvider); + + User loadUser(UserProfile.AuthProvider authProvider, String loginId, + Collection<GrantedAuthority> grantedAuthorities, boolean autoCreate) throws UsernameNotFoundException; + + User loadUser(String userId, Collection<GrantedAuthority> grantedAuthorities); + + UserProfile getByAuthProviderAndLoginId(UserProfile.AuthProvider authProvider, String loginId); + + UserProfile getByUsername(String username); + + UserProfile create(String username, String password); + + UserProfile update(UserProfile userProfile); + + UserProfile deleteByUsername(String username); + + void addRoomToHistory(UserProfile userProfile, Room room); + + void initiatePasswordReset(String username); + + boolean resetPassword(UserProfile userProfile, String key, String password); +} diff --git a/src/main/java/de/thm/arsnova/service/UserServiceImpl.java b/src/main/java/de/thm/arsnova/service/UserServiceImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..51498b832d4c1c1e9f2dcd11bea0559571aa40d0 --- /dev/null +++ b/src/main/java/de/thm/arsnova/service/UserServiceImpl.java @@ -0,0 +1,644 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import com.codahale.metrics.annotation.Gauge; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.web.exceptions.BadRequestException; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.persistence.UserRepository; +import de.thm.arsnova.security.GuestUserDetailsService; +import de.thm.arsnova.security.User; +import de.thm.arsnova.security.jwt.JwtService; +import de.thm.arsnova.security.jwt.JwtToken; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mail.MailException; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.codec.Hex; +import org.springframework.security.crypto.keygen.BytesKeyGenerator; +import org.springframework.security.crypto.keygen.KeyGenerators; +import org.springframework.security.ldap.authentication.LdapAuthenticationProvider; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Isolation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.util.UriUtils; +import org.stagemonitor.core.metrics.MonitorGauges; + +import javax.annotation.PreDestroy; +import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; +import java.io.IOException; +import java.text.MessageFormat; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +/** + * Performs all user related operations. + */ +@Service +@MonitorGauges +public class UserServiceImpl extends DefaultEntityServiceImpl<UserProfile> implements UserService { + + private static final int LOGIN_TRY_RESET_DELAY_MS = 30 * 1000; + + private static final int LOGIN_BAN_RESET_DELAY_MS = 2 * 60 * 1000; + + private static final int REPEATED_PASSWORD_RESET_DELAY_MS = 3 * 60 * 1000; + + private static final int PASSWORD_RESET_KEY_DURABILITY_MS = 2 * 60 * 60 * 1000; + + private static final long ACTIVATION_KEY_CHECK_INTERVAL_MS = 30 * 60 * 1000L; + private static final long ACTIVATION_KEY_DURABILITY_MS = 6 * 60 * 60 * 1000L; + + private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); + + private static final ConcurrentHashMap<UUID, ClientAuthentication> socketIdToUser = new ConcurrentHashMap<>(); + + /* used for Socket.IO online check solution (new) */ + private static final ConcurrentHashMap<ClientAuthentication, String> userToRoomId = new ConcurrentHashMap<>(); + + private UserRepository userRepository; + private JwtService jwtService; + private JavaMailSender mailSender; + + @Autowired(required = false) + private GuestUserDetailsService guestUserDetailsService; + + @Autowired(required = false) + private DaoAuthenticationProvider daoProvider; + + @Autowired(required = false) + private LdapAuthenticationProvider ldapAuthenticationProvider; + + @Value("${root-url}") + private String rootUrl; + + @Value("${customization.path}") + private String customizationPath; + + @Value("${security.user-db.allowed-email-domains}") + private String allowedEmailDomains; + + @Value("${security.user-db.activation-path}") + private String activationPath; + + @Value("${security.user-db.reset-password-path}") + private String resetPasswordPath; + + @Value("${mail.sender.address}") + private String mailSenderAddress; + + @Value("${mail.sender.name}") + private String mailSenderName; + + @Value("${security.user-db.registration-mail.subject}") + private String regMailSubject; + + @Value("${security.user-db.registration-mail.body}") + private String regMailBody; + + @Value("${security.user-db.reset-password-mail.subject}") + private String resetPasswordMailSubject; + + @Value("${security.user-db.reset-password-mail.body}") + private String resetPasswordMailBody; + + @Value("${security.authentication.login-try-limit}") + private int loginTryLimit; + + @Value("${security.admin-accounts}") + private String[] adminAccounts; + + private Pattern mailPattern; + private BytesKeyGenerator keygen; + private BCryptPasswordEncoder encoder; + private ConcurrentHashMap<String, Byte> loginTries; + private Set<String> loginBans; + + { + loginTries = new ConcurrentHashMap<>(); + loginBans = Collections.synchronizedSet(new HashSet<String>()); + } + + public UserServiceImpl( + UserRepository repository, + JavaMailSender mailSender, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(UserProfile.class, repository, jackson2HttpMessageConverter.getObjectMapper()); + this.userRepository = repository; + this.mailSender = mailSender; + } + + @Scheduled(fixedDelay = LOGIN_TRY_RESET_DELAY_MS) + public void resetLoginTries() { + if (!loginTries.isEmpty()) { + logger.debug("Reset failed login counters."); + loginTries.clear(); + } + } + + @Scheduled(fixedDelay = LOGIN_BAN_RESET_DELAY_MS) + public void resetLoginBans() { + if (!loginBans.isEmpty()) { + logger.info("Reset temporary login bans."); + loginBans.clear(); + } + } + + @Scheduled(fixedDelay = ACTIVATION_KEY_CHECK_INTERVAL_MS) + public void deleteInactiveUsers() { + logger.info("Delete inactive users."); + long unixTime = System.currentTimeMillis(); + long lastActivityBefore = unixTime - ACTIVATION_KEY_DURABILITY_MS; + userRepository.deleteInactiveUsers(lastActivityBefore); + } + + @Override + public UserProfile getCurrentUserProfile() { + final ClientAuthentication authentication = getCurrentUser(); + return getByAuthProviderAndLoginId(authentication.getAuthProvider(), authentication.getUsername()); + } + + @Override + public ClientAuthentication getCurrentUser() { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || authentication.getPrincipal() == null) { + return null; + } + + ClientAuthentication user = new ClientAuthentication(authentication); + if (user == null || "anonymous".equals(user.getUsername())) { + throw new UnauthorizedException(); + } + user.setAdmin(Arrays.asList(adminAccounts).contains(user.getUsername())); + + return user; + } + + @Override + public de.thm.arsnova.model.ClientAuthentication getCurrentClientAuthentication() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !(authentication.getPrincipal() instanceof User)) { + return null; + } + User user = (User) authentication.getPrincipal(); + String jwt = authentication instanceof JwtToken ? + (String) authentication.getCredentials() : jwtService.createSignedToken(user); + + de.thm.arsnova.model.ClientAuthentication clientAuthentication = + new de.thm.arsnova.model.ClientAuthentication(user.getId(), user.getUsername(), + user.getAuthProvider(), jwt); + + return clientAuthentication; + } + + @Override + public boolean isAdmin(final String username) { + return Arrays.asList(adminAccounts).contains(username); + } + + @Override + public boolean isBannedFromLogin(String addr) { + return loginBans.contains(addr); + } + + @Override + public void increaseFailedLoginCount(String addr) { + Byte tries = loginTries.get(addr); + if (null == tries) { + tries = 0; + } + if (tries < loginTryLimit) { + loginTries.put(addr, ++tries); + if (loginTryLimit == tries) { + logger.info("Temporarily banned {} from login.", addr); + loginBans.add(addr); + } + } + } + + @Override + public ClientAuthentication getUserToSocketId(final UUID socketId) { + return socketIdToUser.get(socketId); + } + + @Override + public void putUserToSocketId(final UUID socketId, final ClientAuthentication user) { + socketIdToUser.put(socketId, user); + } + + @Override + public Set<Map.Entry<UUID, ClientAuthentication>> getSocketIdToUser() { + return socketIdToUser.entrySet(); + } + + @Override + public void removeUserToSocketId(final UUID socketId) { + socketIdToUser.remove(socketId); + } + + @Override + public boolean isUserInRoom(final ClientAuthentication user, final String expectedRoomId) { + String actualRoomId = userToRoomId.get(user); + + return actualRoomId != null && actualRoomId.equals(expectedRoomId); + } + + @Override + public Set<ClientAuthentication> getUsersByRoomId(final String roomId) { + final Set<ClientAuthentication> result = new HashSet<>(); + for (final Entry<ClientAuthentication, String> e : userToRoomId.entrySet()) { + if (e.getValue().equals(roomId)) { + result.add(e.getKey()); + } + } + + return result; + } + + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + public void addUserToRoomBySocketId(final UUID socketId, final String roomId) { + final ClientAuthentication user = socketIdToUser.get(socketId); + userToRoomId.put(user, roomId); + } + + @Override + @Transactional(isolation = Isolation.READ_COMMITTED) + public void removeUserFromRoomBySocketId(final UUID socketId) { + final ClientAuthentication user = socketIdToUser.get(socketId); + if (null == user) { + logger.warn("No user exists for socket {}.", socketId); + + return; + } + userToRoomId.remove(user); + } + + @Override + public String getRoomIdByUserId(final String userId) { + for (final Entry<ClientAuthentication, String> entry : userToRoomId.entrySet()) { + if (entry.getKey().getId().equals(userId)) { + return entry.getValue(); + } + } + + return null; + } + + @PreDestroy + public void destroy() { + logger.error("Destroy UserServiceImpl"); + } + + @Override + public void removeUserFromMaps(final ClientAuthentication user) { + if (user != null) { + userToRoomId.remove(user); + } + } + + @Override + @Gauge + public int loggedInUsers() { + return userToRoomId.size(); + } + + @Override + public void authenticate(final UsernamePasswordAuthenticationToken token, + final UserProfile.AuthProvider authProvider) { + Authentication auth; + switch (authProvider) { + case LDAP: + auth = ldapAuthenticationProvider.authenticate(token); + break; + case ARSNOVA: + auth = daoProvider.authenticate(token); + break; + case ARSNOVA_GUEST: + String id = token.getName(); + boolean autoCreate = false; + if (id == null || id.isEmpty()) { + id = generateGuestId(); + autoCreate = true; + } + UserDetails userDetails = guestUserDetailsService.loadUserByUsername(id, autoCreate); + if (userDetails == null) { + throw new UsernameNotFoundException("Guest user does not exist"); + } + auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + + break; + default: + throw new IllegalArgumentException("Unsupported authentication provider"); + } + + if (!auth.isAuthenticated()) { + throw new BadRequestException(); + } + SecurityContextHolder.getContext().setAuthentication(auth); + } + + @Override + public User loadUser(final UserProfile.AuthProvider authProvider, final String loginId, + final Collection<GrantedAuthority> grantedAuthorities, final boolean autoCreate) + throws UsernameNotFoundException { + logger.debug("Load user: LoginId: {}, AuthProvider: {}", loginId, authProvider); + UserProfile userProfile = userRepository.findByAuthProviderAndLoginId(authProvider, loginId); + if (userProfile == null) { + if (autoCreate) { + userProfile = new UserProfile(authProvider, loginId); + /* Repository is accessed directly without EntityService to skip permission check */ + userRepository.save(userProfile); + } else { + throw new UsernameNotFoundException("User does not exist."); + } + } + + return new User(userProfile, grantedAuthorities); + } + + @Override + public User loadUser(final String userId, final Collection<GrantedAuthority> grantedAuthorities) + throws UsernameNotFoundException { + logger.debug("Load user: UserId: {}", userId); + UserProfile userProfile = userRepository.findOne(userId); + if (userProfile == null) { + throw new UsernameNotFoundException("User does not exist."); + } + + return new User(userProfile, grantedAuthorities); + } + + @Override + public UserProfile getByAuthProviderAndLoginId(final UserProfile.AuthProvider authProvider, final String loginId) { + return userRepository.findByAuthProviderAndLoginId(authProvider, loginId); + } + + @Override + public UserProfile getByUsername(String username) { + return userRepository.findByAuthProviderAndLoginId(UserProfile.AuthProvider.ARSNOVA, username.toLowerCase()); + } + + @Override + public UserProfile create(String username, String password) { + String lcUsername = username.toLowerCase(); + + if (null == keygen) { + keygen = KeyGenerators.secureRandom(16); + } + + if (null == mailPattern) { + parseMailAddressPattern(); + } + + if (null == mailPattern || !mailPattern.matcher(lcUsername).matches()) { + logger.info("User registration failed. {} does not match pattern.", lcUsername); + + return null; + } + + if (null != userRepository.findByAuthProviderAndLoginId(UserProfile.AuthProvider.ARSNOVA, lcUsername)) { + logger.info("User registration failed. {} already exists.", lcUsername); + + return null; + } + + UserProfile userProfile = new UserProfile(); + UserProfile.Account account = new UserProfile.Account(); + userProfile.setAccount(account); + userProfile.setAuthProvider(UserProfile.AuthProvider.ARSNOVA); + userProfile.setLoginId(lcUsername); + account.setPassword(encodePassword(password)); + account.setActivationKey(RandomStringUtils.randomAlphanumeric(32)); + userProfile.setCreationTimestamp(new Date()); + + /* Repository is accessed directly without EntityService to skip permission check */ + UserProfile result = userRepository.save(userProfile); + if (null != result) { + sendActivationEmail(result); + } else { + logger.error("User registration failed. {} could not be created.", lcUsername); + } + + return result; + } + + private String encodePassword(String password) { + if (null == encoder) { + encoder = new BCryptPasswordEncoder(12); + } + + return encoder.encode(password); + } + + private void sendActivationEmail(UserProfile userProfile) { + String activationUrl = MessageFormat.format( + "{0}{1}/{2}?action=activate&username={3}&key={4}", + rootUrl, + customizationPath, + activationPath, + UriUtils.encodeQueryParam(userProfile.getLoginId(), "UTF-8"), + userProfile.getAccount().getActivationKey()); + + sendEmail(userProfile, regMailSubject, MessageFormat.format(regMailBody, activationUrl)); + } + + private void parseMailAddressPattern() { + /* TODO: Add Unicode support */ + + List<String> domainList = Arrays.asList(allowedEmailDomains.split(",")); + + if (!domainList.isEmpty()) { + List<String> patterns = new ArrayList<>(); + if (domainList.contains("*")) { + patterns.add("([a-z0-9-]+\\.)+[a-z0-9-]+"); + } else { + Pattern patternPattern = Pattern.compile("[a-z0-9.*-]+", Pattern.CASE_INSENSITIVE); + for (String patternStr : domainList) { + if (patternPattern.matcher(patternStr).matches()) { + patterns.add(patternStr.replaceAll("[.]", "[.]").replaceAll("[*]", "[a-z0-9-]+?")); + } + } + } + + mailPattern = Pattern.compile("[a-z0-9._-]+?@(" + StringUtils.join(patterns, "|") + ")", Pattern.CASE_INSENSITIVE); + logger.info("Allowed e-mail addresses (pattern) for registration: '{}'.", mailPattern.pattern()); + } + } + + @Override + public UserProfile update(UserProfile userProfile) { + if (null != userProfile.getId()) { + return userRepository.save(userProfile); + } + + return null; + } + + @Override + public UserProfile deleteByUsername(String username) { + ClientAuthentication user = getCurrentUser(); + if (!user.getUsername().equals(username.toLowerCase()) + && !SecurityContextHolder.getContext().getAuthentication().getAuthorities() + .contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) { + throw new UnauthorizedException(); + } + + UserProfile userProfile = getByUsername(username); + if (null == userProfile) { + throw new NotFoundException(); + } + + userRepository.delete(userProfile); + + return userProfile; + } + + @Override + @PreAuthorize("hasPermission(#userProfile, 'update')") + public void addRoomToHistory(final UserProfile userProfile, final Room room) { + if (userProfile.getId().equals(room.getOwnerId())) { + return; + } + Set<UserProfile.RoomHistoryEntry> roomHistory = userProfile.getRoomHistory(); + UserProfile.RoomHistoryEntry entry = new UserProfile.RoomHistoryEntry(room.getId(), new Date()); + /* TODO: lastVisit in roomHistory is currently not updated by subsequent method invocations */ + if (!roomHistory.contains(entry)) { + roomHistory.add(entry); + Map<String, Object> changes = Collections.singletonMap("roomHistory", roomHistory); + try { + super.patch(userProfile, changes); + } catch (IOException e) { + logger.error("Could not patch RoomHistory"); + } + } + } + + @Override + public void initiatePasswordReset(String username) { + UserProfile userProfile = getByUsername(username); + if (null == userProfile) { + logger.info("Password reset failed. User {} does not exist.", username); + + throw new NotFoundException(); + } + UserProfile.Account account = userProfile.getAccount(); + if (System.currentTimeMillis() < account.getPasswordResetTime().getTime() + REPEATED_PASSWORD_RESET_DELAY_MS) { + logger.info("Password reset failed. The reset delay for User {} is still active.", username); + + throw new BadRequestException(); + } + + account.setPasswordResetKey(RandomStringUtils.randomAlphanumeric(32)); + account.setPasswordResetTime(new Date()); + + if (null == userRepository.save(userProfile)) { + logger.error("Password reset failed. {} could not be updated.", username); + } + + String resetPasswordUrl = MessageFormat.format( + "{0}{1}/{2}?action=resetpassword&username={3}&key={4}", + rootUrl, + customizationPath, + resetPasswordPath, + UriUtils.encodeQueryParam(userProfile.getLoginId(), "UTF-8"), account.getPasswordResetKey()); + + sendEmail(userProfile, resetPasswordMailSubject, MessageFormat.format(resetPasswordMailBody, resetPasswordUrl)); + } + + @Override + public boolean resetPassword(UserProfile userProfile, String key, String password) { + UserProfile.Account account = userProfile.getAccount(); + if (null == key || "".equals(key) || !key.equals(account.getPasswordResetKey())) { + logger.info("Password reset failed. Invalid key provided for User {}.", userProfile.getLoginId()); + + return false; + } + if (System.currentTimeMillis() > account.getPasswordResetTime().getTime() + PASSWORD_RESET_KEY_DURABILITY_MS) { + logger.info("Password reset failed. Key provided for User {} is no longer valid.", userProfile.getLoginId()); + + account.setPasswordResetKey(null); + account.setPasswordResetTime(new Date(0)); + update(userProfile); + + return false; + } + + account.setPassword(encodePassword(password)); + account.setPasswordResetKey(null); + if (null == update(userProfile)) { + logger.error("Password reset failed. {} could not be updated.", userProfile.getLoginId()); + } + + return true; + } + + private void sendEmail(UserProfile userProfile, String subject, String body) { + MimeMessage msg = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8"); + try { + helper.setFrom(mailSenderName + "<" + mailSenderAddress + ">"); + helper.setTo(userProfile.getLoginId()); + helper.setSubject(subject); + helper.setText(body); + + logger.info("Sending mail \"{}\" from \"{}\" to \"{}\"", subject, msg.getFrom(), userProfile.getLoginId()); + mailSender.send(msg); + } catch (MailException | MessagingException e) { + logger.warn("Mail \"{}\" could not be sent.", subject, e); + } + } + + private String generateGuestId() { + if (null == keygen) { + keygen = KeyGenerators.secureRandom(16); + } + + return new String(Hex.encode(keygen.generateKey())); + } + + @Autowired + public void setJwtService(final JwtService jwtService) { + this.jwtService = jwtService; + } +} diff --git a/src/main/java/de/thm/arsnova/services/package-info.java b/src/main/java/de/thm/arsnova/service/package-info.java similarity index 62% rename from src/main/java/de/thm/arsnova/services/package-info.java rename to src/main/java/de/thm/arsnova/service/package-info.java index d87d15a849a564276d79631c8f75dc5a420e6d3d..dfa2ac6653ff0124f47edd81ba7168da78bd6c7f 100644 --- a/src/main/java/de/thm/arsnova/services/package-info.java +++ b/src/main/java/de/thm/arsnova/service/package-info.java @@ -1,4 +1,4 @@ /** * Classes and interfaces for the service layer */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; diff --git a/src/main/java/de/thm/arsnova/services/score/QuestionBasedScoreCalculator.java b/src/main/java/de/thm/arsnova/service/score/QuestionBasedScoreCalculator.java similarity index 88% rename from src/main/java/de/thm/arsnova/services/score/QuestionBasedScoreCalculator.java rename to src/main/java/de/thm/arsnova/service/score/QuestionBasedScoreCalculator.java index 788531f437290f351638f3925b7eab8a1c97c953..0eed46c4d470384e693aa2357699ca96ba0dffa2 100644 --- a/src/main/java/de/thm/arsnova/services/score/QuestionBasedScoreCalculator.java +++ b/src/main/java/de/thm/arsnova/service/score/QuestionBasedScoreCalculator.java @@ -15,11 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ScoreStatistics; -import de.thm.arsnova.persistance.SessionStatisticsRepository; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ScoreStatistics; +import de.thm.arsnova.persistence.SessionStatisticsRepository; /** * Calculates learning progress based on overall correctness of an answer. A question is answered correctly if and @@ -71,7 +71,7 @@ public class QuestionBasedScoreCalculator extends VariantScoreCalculator { } @Override - protected ScoreStatistics createMyProgress(User user) { + protected ScoreStatistics createMyProgress(ClientAuthentication user) { final int numerator = numQuestionsCorrectForUser(user); final int denominator = courseScore.getQuestionCount(); ScoreStatistics lpv = new ScoreStatistics(); @@ -84,7 +84,7 @@ public class QuestionBasedScoreCalculator extends VariantScoreCalculator { return lpv; } - private int numQuestionsCorrectForUser(User user) { + private int numQuestionsCorrectForUser(ClientAuthentication user) { int numQuestionsCorrect = 0; for (QuestionScore questionScore : courseScore) { numQuestionsCorrect += countCorrectAnswersForUser(user, questionScore); @@ -92,7 +92,7 @@ public class QuestionBasedScoreCalculator extends VariantScoreCalculator { return numQuestionsCorrect; } - private int countCorrectAnswersForUser(User user, QuestionScore questionScore) { + private int countCorrectAnswersForUser(ClientAuthentication user, QuestionScore questionScore) { int numQuestionsCorrect = 0; int requiredScore = questionScore.getMaximum(); for (UserScore userScore : questionScore) { diff --git a/src/main/java/de/thm/arsnova/services/score/QuestionScore.java b/src/main/java/de/thm/arsnova/service/score/QuestionScore.java similarity index 93% rename from src/main/java/de/thm/arsnova/services/score/QuestionScore.java rename to src/main/java/de/thm/arsnova/service/score/QuestionScore.java index c5c35159b151b37a4656bd0792f5312042f42e1a..0ea10b92d1b197bb60538b2ca4fb3f92a60a7f2e 100644 --- a/src/main/java/de/thm/arsnova/services/score/QuestionScore.java +++ b/src/main/java/de/thm/arsnova/service/score/QuestionScore.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; import java.util.ArrayList; import java.util.Iterator; @@ -74,7 +74,7 @@ public class QuestionScore implements Iterable<UserScore> { return totalScore; } - public int getTotalUserScore(User user) { + public int getTotalUserScore(ClientAuthentication user) { int totalScore = 0; for (UserScore score : userScores) { if (score.isUser(user)) { diff --git a/src/main/java/de/thm/arsnova/services/score/Score.java b/src/main/java/de/thm/arsnova/service/score/Score.java similarity index 95% rename from src/main/java/de/thm/arsnova/services/score/Score.java rename to src/main/java/de/thm/arsnova/service/score/Score.java index 8ecb7e5da4178f3ad8ee92389cc4fe9df58ae48c..d3c5e4d299db8a54afe93ccfcffbab8836753314 100644 --- a/src/main/java/de/thm/arsnova/services/score/Score.java +++ b/src/main/java/de/thm/arsnova/service/score/Score.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; import java.util.HashMap; import java.util.HashSet; @@ -90,7 +90,7 @@ public class Score implements Iterable<QuestionScore> { return score; } - public double getTotalUserScore(User user) { + public double getTotalUserScore(ClientAuthentication user) { int score = 0; for (QuestionScore questionScore : this) { score += questionScore.getTotalUserScore(user); diff --git a/src/main/java/de/thm/arsnova/services/score/ScoreBasedScoreCalculator.java b/src/main/java/de/thm/arsnova/service/score/ScoreBasedScoreCalculator.java similarity index 88% rename from src/main/java/de/thm/arsnova/services/score/ScoreBasedScoreCalculator.java rename to src/main/java/de/thm/arsnova/service/score/ScoreBasedScoreCalculator.java index aa4886e1f232718290f6e4aee6a194763b7feb5b..7b34fddbded6fd919ae780831eb87521fe1e6d1b 100644 --- a/src/main/java/de/thm/arsnova/services/score/ScoreBasedScoreCalculator.java +++ b/src/main/java/de/thm/arsnova/service/score/ScoreBasedScoreCalculator.java @@ -15,11 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ScoreStatistics; -import de.thm.arsnova.persistance.SessionStatisticsRepository; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ScoreStatistics; +import de.thm.arsnova.persistence.SessionStatisticsRepository; /** * Calculates score based on a question's value. @@ -54,7 +54,7 @@ public class ScoreBasedScoreCalculator extends VariantScoreCalculator { } @Override - protected ScoreStatistics createMyProgress(User user) { + protected ScoreStatistics createMyProgress(ClientAuthentication user) { ScoreStatistics lpv = new ScoreStatistics(); lpv.setCourseProgress(coursePercentage()); lpv.setNumQuestions(courseScore.getQuestionCount()); @@ -65,7 +65,7 @@ public class ScoreBasedScoreCalculator extends VariantScoreCalculator { return lpv; } - private int myPercentage(User user) { + private int myPercentage(ClientAuthentication user) { final int courseMaximumValue = courseScore.getMaximumScore(); final double userTotalValue = courseScore.getTotalUserScore(user); if (courseMaximumValue == 0) { diff --git a/src/main/java/de/thm/arsnova/services/score/ScoreCalculator.java b/src/main/java/de/thm/arsnova/service/score/ScoreCalculator.java similarity index 74% rename from src/main/java/de/thm/arsnova/services/score/ScoreCalculator.java rename to src/main/java/de/thm/arsnova/service/score/ScoreCalculator.java index 25f53022dada8a33fb788ba813f013458fb06f56..7eb61b0c72983782f91e96174378be3c90b81214 100644 --- a/src/main/java/de/thm/arsnova/services/score/ScoreCalculator.java +++ b/src/main/java/de/thm/arsnova/service/score/ScoreCalculator.java @@ -15,18 +15,18 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ScoreStatistics; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ScoreStatistics; /** * Defines the core functionality which the score calculation should provide. */ public interface ScoreCalculator { - ScoreStatistics getCourseProgress(Session session); + ScoreStatistics getCourseProgress(Room room); - ScoreStatistics getMyProgress(Session session, User user); + ScoreStatistics getMyProgress(Room room, ClientAuthentication user); } diff --git a/src/main/java/de/thm/arsnova/services/score/ScoreCalculatorFactory.java b/src/main/java/de/thm/arsnova/service/score/ScoreCalculatorFactory.java similarity index 95% rename from src/main/java/de/thm/arsnova/services/score/ScoreCalculatorFactory.java rename to src/main/java/de/thm/arsnova/service/score/ScoreCalculatorFactory.java index 473c1dccefe0f6a921c37ca7812e10f9ea8f63ee..5294a35873516eb7fc0b6e261a1d6393a6092ec7 100644 --- a/src/main/java/de/thm/arsnova/services/score/ScoreCalculatorFactory.java +++ b/src/main/java/de/thm/arsnova/service/score/ScoreCalculatorFactory.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; /** * Interface for Spring dependency injection. diff --git a/src/main/java/de/thm/arsnova/services/score/ScoreCalculatorFactoryImpl.java b/src/main/java/de/thm/arsnova/service/score/ScoreCalculatorFactoryImpl.java similarity index 78% rename from src/main/java/de/thm/arsnova/services/score/ScoreCalculatorFactoryImpl.java rename to src/main/java/de/thm/arsnova/service/score/ScoreCalculatorFactoryImpl.java index 44925d7442175e73117347af5eeb500b4c038999..78acff908dc1d575d4d06111ef4de0a4b996181d 100644 --- a/src/main/java/de/thm/arsnova/services/score/ScoreCalculatorFactoryImpl.java +++ b/src/main/java/de/thm/arsnova/service/score/ScoreCalculatorFactoryImpl.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.events.*; -import de.thm.arsnova.persistance.SessionStatisticsRepository; +import de.thm.arsnova.event.*; +import de.thm.arsnova.persistence.SessionStatisticsRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheEvict; import org.springframework.context.ApplicationEventPublisher; @@ -56,92 +56,92 @@ public class ScoreCalculatorFactoryImpl implements ArsnovaEventVisitor, ScoreCal @Override public void visit(DeleteCommentEvent deleteCommentEvent) { } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(NewQuestionEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(UnlockQuestionEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(UnlockQuestionsEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(LockQuestionEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(LockQuestionsEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(NewAnswerEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(DeleteAnswerEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(DeleteQuestionEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(DeleteAllQuestionsEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(DeleteAllQuestionsAnswersEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(DeleteAllPreparationAnswersEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(DeleteAllLectureAnswersEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } - @CacheEvict(value = "score", key = "#event.Session") + @CacheEvict(value = "score", key = "#event.Room") @Override public void visit(PiRoundResetEvent event) { - this.publisher.publishEvent(new ChangeScoreEvent(this, event.getSession())); + this.publisher.publishEvent(new ChangeScoreEvent(this, event.getRoom())); } @Override public void visit(NewFeedbackEvent newFeedbackEvent) { } @Override - public void visit(DeleteFeedbackForSessionsEvent deleteFeedbackEvent) { } + public void visit(DeleteFeedbackForRoomsEvent deleteFeedbackEvent) { } @Override - public void visit(StatusSessionEvent statusSessionEvent) { } + public void visit(StatusRoomEvent statusSessionEvent) { } @Override public void visit(ChangeScoreEvent changeLearningProgress) { } @@ -156,10 +156,10 @@ public class ScoreCalculatorFactoryImpl implements ArsnovaEventVisitor, ScoreCal public void visit(PiRoundCancelEvent piRoundCancelEvent) { } @Override - public void visit(NewSessionEvent event) { } + public void visit(NewRoomEvent event) { } @Override - public void visit(DeleteSessionEvent event) { } + public void visit(DeleteRoomEvent event) { } @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { diff --git a/src/main/java/de/thm/arsnova/services/score/ScoreCalculatorListener.java b/src/main/java/de/thm/arsnova/service/score/ScoreCalculatorListener.java similarity index 91% rename from src/main/java/de/thm/arsnova/services/score/ScoreCalculatorListener.java rename to src/main/java/de/thm/arsnova/service/score/ScoreCalculatorListener.java index 113d5d7e213d13c4aba9529d50a5b6cf3a998f02..d78cc886327e289d5649f0476dbfebea9f3755ae 100644 --- a/src/main/java/de/thm/arsnova/services/score/ScoreCalculatorListener.java +++ b/src/main/java/de/thm/arsnova/service/score/ScoreCalculatorListener.java @@ -15,10 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.events.ArsnovaEvent; -import de.thm.arsnova.events.ArsnovaEventVisitor; +import de.thm.arsnova.event.ArsnovaEvent; +import de.thm.arsnova.event.ArsnovaEventVisitor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; diff --git a/src/main/java/de/thm/arsnova/services/score/UserScore.java b/src/main/java/de/thm/arsnova/service/score/UserScore.java similarity index 88% rename from src/main/java/de/thm/arsnova/services/score/UserScore.java rename to src/main/java/de/thm/arsnova/service/score/UserScore.java index 7ab9b7a285ca030f5e81aee20f5d4d85976caff5..bc91a25594f83537e180a655fc265888e32d9a52 100644 --- a/src/main/java/de/thm/arsnova/services/score/UserScore.java +++ b/src/main/java/de/thm/arsnova/service/score/UserScore.java @@ -15,9 +15,9 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.entities.User; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; /** * The learning progress answer score of a particular user. @@ -41,7 +41,7 @@ public class UserScore { return score; } - public boolean isUser(User user) { + public boolean isUser(ClientAuthentication user) { return user.getUsername().equals(username); } diff --git a/src/main/java/de/thm/arsnova/services/score/VariantScoreCalculator.java b/src/main/java/de/thm/arsnova/service/score/VariantScoreCalculator.java similarity index 72% rename from src/main/java/de/thm/arsnova/services/score/VariantScoreCalculator.java rename to src/main/java/de/thm/arsnova/service/score/VariantScoreCalculator.java index f518ac6fec937f75095bb143d3e0341297a7f97c..de9b62054cc45457a6b4daed1f898dadf0785e0b 100644 --- a/src/main/java/de/thm/arsnova/services/score/VariantScoreCalculator.java +++ b/src/main/java/de/thm/arsnova/service/score/VariantScoreCalculator.java @@ -15,12 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; +package de.thm.arsnova.service.score; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ScoreStatistics; -import de.thm.arsnova.persistance.SessionStatisticsRepository; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ScoreStatistics; +import de.thm.arsnova.persistence.SessionStatisticsRepository; import org.springframework.cache.annotation.Cacheable; /** @@ -39,12 +39,12 @@ abstract class VariantScoreCalculator implements ScoreCalculator { } @Cacheable("score") - private Score loadProgress(final Session session) { - return sessionStatisticsRepository.getLearningProgress(session); + private Score loadProgress(final Room room) { + return sessionStatisticsRepository.getLearningProgress(room); } - private void refreshProgress(final Session session) { - this.courseScore = sessionStatisticsRepository.getLearningProgress(session); + private void refreshProgress(final Room room) { + this.courseScore = sessionStatisticsRepository.getLearningProgress(room); } public void setQuestionVariant(final String variant) { @@ -52,8 +52,8 @@ abstract class VariantScoreCalculator implements ScoreCalculator { } @Override - public ScoreStatistics getCourseProgress(Session session) { - this.refreshProgress(session); + public ScoreStatistics getCourseProgress(Room room) { + this.refreshProgress(room); this.filterVariant(); return this.createCourseProgress(); } @@ -61,8 +61,8 @@ abstract class VariantScoreCalculator implements ScoreCalculator { protected abstract ScoreStatistics createCourseProgress(); @Override - public ScoreStatistics getMyProgress(Session session, User user) { - this.refreshProgress(session); + public ScoreStatistics getMyProgress(Room room, ClientAuthentication user) { + this.refreshProgress(room); this.filterVariant(); return this.createMyProgress(user); } @@ -73,6 +73,6 @@ abstract class VariantScoreCalculator implements ScoreCalculator { } } - protected abstract ScoreStatistics createMyProgress(User user); + protected abstract ScoreStatistics createMyProgress(ClientAuthentication user); } diff --git a/src/main/java/de/thm/arsnova/services/CommentService.java b/src/main/java/de/thm/arsnova/services/CommentService.java deleted file mode 100644 index 4713dfb8d30fb318cc0bb381a23dbc6db6d68348..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/CommentService.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.CommentReadingCount; -import de.thm.arsnova.entities.User; - -import java.util.List; - -public interface CommentService extends EntityService<Comment> { - boolean save(Comment comment); - - int count(String sessionKey); - - CommentReadingCount countRead(String sessionKey, String username); - - List<Comment> getBySessionKey(String sessionKey, int offset, int limit); - - Comment getAndMarkRead(String commentId); - - Comment getAndMarkReadInternal(String commentId, User user); - - void delete(String commentId); - - void deleteBySessionKey(String sessionKeyword); -} diff --git a/src/main/java/de/thm/arsnova/services/CommentServiceImpl.java b/src/main/java/de/thm/arsnova/services/CommentServiceImpl.java deleted file mode 100644 index 0066191a7b9eb07812b2dc591ebe8a8525ad590d..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/CommentServiceImpl.java +++ /dev/null @@ -1,182 +0,0 @@ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.CommentReadingCount; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.events.DeleteCommentEvent; -import de.thm.arsnova.events.NewCommentEvent; -import de.thm.arsnova.exceptions.ForbiddenException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.persistance.CommentRepository; -import de.thm.arsnova.persistance.SessionRepository; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Service; - -import java.util.List; - -/** - * Performs all comment related operations. - */ -@Service -public class CommentServiceImpl extends DefaultEntityServiceImpl<Comment> implements CommentService, ApplicationEventPublisherAware { - private UserService userService; - - private CommentRepository commentRepository; - - private SessionRepository sessionRepository; - - private ApplicationEventPublisher publisher; - - public CommentServiceImpl( - CommentRepository repository, - SessionRepository sessionRepository, - UserService userService, - @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { - super(Comment.class, repository, jackson2HttpMessageConverter.getObjectMapper()); - this.commentRepository = repository; - this.sessionRepository = sessionRepository; - this.userService = userService; - } - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { - this.publisher = applicationEventPublisher; - } - - @Override - @PreAuthorize("isAuthenticated()") - public boolean save(final Comment comment) { - final Session session = sessionRepository.findByKeyword(comment.getSessionId()); - final User user = userService.getCurrentUser(); - comment.setSessionId(session.getId()); - comment.setCreator(user.getUsername()); - comment.setRead(false); - if (comment.getTimestamp() == 0) { - comment.setTimestamp(System.currentTimeMillis()); - } - final Comment result = super.create(comment); - - if (null != result) { - final NewCommentEvent event = new NewCommentEvent(this, session, result); - this.publisher.publishEvent(event); - return true; - } - return false; - } - - @Override - @PreAuthorize("hasPermission(#commentId, 'comment', 'owner')") - public void delete(final String commentId) { - final Comment comment = commentRepository.findOne(commentId); - if (comment == null) { - throw new NotFoundException(); - } - commentRepository.delete(comment); - - final Session session = sessionRepository.findByKeyword(comment.getSessionId()); - final DeleteCommentEvent event = new DeleteCommentEvent(this, session, comment); - this.publisher.publishEvent(event); - } - - @Override - @PreAuthorize("isAuthenticated()") - public void deleteBySessionKey(final String sessionKeyword) { - final Session session = sessionRepository.findByKeyword(sessionKeyword); - if (session == null) { - throw new UnauthorizedException(); - } - final User user = getCurrentUser(); - if (session.isCreator(user)) { - commentRepository.deleteBySessionId(session.getId()); - } else { - commentRepository.deleteBySessionIdAndUser(session.getId(), user); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public int count(final String sessionKey) { - return commentRepository.countBySessionId(getSession(sessionKey).getId()); - } - - @Override - @PreAuthorize("isAuthenticated()") - public CommentReadingCount countRead(final String sessionKey, String username) { - final Session session = sessionRepository.findByKeyword(sessionKey); - if (session == null) { - throw new NotFoundException(); - } - if (username == null) { - return commentRepository.countReadingBySessionId(session.getId()); - } else { - User currentUser = userService.getCurrentUser(); - if (!currentUser.getUsername().equals(username)) { - throw new ForbiddenException(); - } - - return commentRepository.countReadingBySessionIdAndUser(session.getId(), currentUser); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Comment> getBySessionKey(final String sessionKey, final int offset, final int limit) { - final Session session = this.getSession(sessionKey); - final User user = getCurrentUser(); - if (session.isCreator(user)) { - return commentRepository.findBySessionId(session.getId(), offset, limit); - } else { - return commentRepository.findBySessionIdAndUser(session.getId(), user, offset, limit); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public Comment getAndMarkRead(final String commentId) { - final User user = userService.getCurrentUser(); - return this.getAndMarkReadInternal(commentId, user); - } - - /* - * The "internal" suffix means it is called by internal services that have no authentication! - * TODO: Find a better way of doing this... - */ - @Override - public Comment getAndMarkReadInternal(final String commentId, User user) { - final Comment comment = commentRepository.findOne(commentId); - if (comment == null) { - throw new NotFoundException(); - } - final Session session = sessionRepository.findOne(comment.getSessionId()); - if (!comment.isCreator(user) && !session.isCreator(user)) { - throw new UnauthorizedException(); - } - if (session.isCreator(user)) { - comment.setRead(true); - save(comment); - } - return comment; - } - - private User getCurrentUser() { - final User user = userService.getCurrentUser(); - if (user == null) { - throw new UnauthorizedException(); - } - return user; - } - - private Session getSession(final String sessionkey) { - final Session session = sessionRepository.findByKeyword(sessionkey); - if (session == null) { - throw new NotFoundException(); - } - return session; - } -} diff --git a/src/main/java/de/thm/arsnova/services/ContentService.java b/src/main/java/de/thm/arsnova/services/ContentService.java deleted file mode 100644 index 9dbb6db5fc526268818f89863d0fc249148130a3..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/ContentService.java +++ /dev/null @@ -1,152 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.User; - -import java.util.List; -import java.util.Map; - -/** - * The functionality the question service should provide. - */ -public interface ContentService extends EntityService<Content> { - Content save(Content content); - - Content get(String id); - - List<Content> getBySessionKey(String sessionkey); - - int countBySessionKey(String sessionkey); - - void delete(String questionId); - - void startNewPiRound(String questionId, User user); - - void startNewPiRoundDelayed(String questionId, int time); - - void cancelPiRoundChange(String questionId); - - void cancelDelayedPiRoundChange(String questionId); - - void resetPiRoundState(String questionId); - - List<String> getUnAnsweredQuestionIds(String sessionKey); - - Answer getMyAnswer(String questionId); - - void getFreetextAnswerAndMarkRead(String answerId, User user); - - List<Answer> getAnswers(String questionId, int piRound, int offset, int limit); - - List<Answer> getAnswers(String questionId, int offset, int limit); - - List<Answer> getAllAnswers(String questionId, int offset, int limit); - - int countAnswersByQuestionIdAndRound(String questionId); - - int countAnswersByQuestionIdAndRound(String questionId, int piRound); - - List<Answer> getFreetextAnswersByQuestionId(String questionId, int offset, int limit); - - List<Answer> getMyAnswersBySessionKey(String sessionKey); - - int countTotalAnswersBySessionKey(String sessionKey); - - int countTotalAnswersByQuestionId(String questionId); - - Content save(final String sessionId, final Content content); - - Content update(Content content); - - void deleteAnswers(String questionId); - - Answer saveAnswer(String questionId, Answer answer); - - Answer updateAnswer(Answer answer); - - void deleteAnswer(String questionId, String answerId); - - List<Content> getLectureQuestions(String sessionkey); - - List<Content> getFlashcards(String sessionkey); - - List<Content> getPreparationQuestions(String sessionkey); - - int countLectureQuestions(String sessionkey); - - int countFlashcards(String sessionkey); - - int countPreparationQuestions(String sessionkey); - - Map<String, Object> countAnswersAndAbstentionsInternal(String questionid); - - int countLectureQuestionAnswers(String sessionkey); - - int countLectureQuestionAnswersInternal(String sessionkey); - - int countPreparationQuestionAnswers(String sessionkey); - - int countPreparationQuestionAnswersInternal(String sessionkey); - - int countFlashcardsForUserInternal(String sessionkey); - - void deleteAllContent(String sessionkey); - - void deleteLectureQuestions(String sessionkey); - - void deletePreparationQuestions(String sessionkey); - - void deleteFlashcards(String sessionkey); - - List<String> getUnAnsweredLectureQuestionIds(String sessionkey); - - List<String> getUnAnsweredLectureQuestionIds(String sessionKey, User user); - - List<String> getUnAnsweredPreparationQuestionIds(String sessionkey); - - List<String> getUnAnsweredPreparationQuestionIds(String sessionKey, User user); - - void publishAll(String sessionkey, boolean publish); - - void publishQuestions(String sessionkey, boolean publish, List<Content> contents); - - void deleteAllQuestionsAnswers(String sessionkey); - - void deleteAllPreparationAnswers(String sessionkey); - - void deleteAllLectureAnswers(String sessionkey); - - int countTotalAbstentionsByQuestionId(String questionId); - - String getImage(String questionId, String answerId); - - void setVotingAdmission(String questionId, boolean disableVoting); - - void setVotingAdmissions(String sessionkey, boolean disableVoting, List<Content> contents); - - void setVotingAdmissionForAllQuestions(String sessionkey, boolean disableVoting); - - String getQuestionImage(String questionId); - - String getQuestionFcImage(String questionId); - - List<Content> replaceImageData(List<Content> contents); -} diff --git a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java b/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java deleted file mode 100644 index 7c2c92a9b60d1d4868c9ef3d6fffb6a72d69f152..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/ContentServiceImpl.java +++ /dev/null @@ -1,1104 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.transport.AnswerQueueElement; -import de.thm.arsnova.persistance.LogEntryRepository; -import de.thm.arsnova.util.ImageUtils; -import de.thm.arsnova.entities.Answer; -import de.thm.arsnova.entities.Content; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.events.*; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.persistance.AnswerRepository; -import de.thm.arsnova.persistance.ContentRepository; -import de.thm.arsnova.persistance.SessionRepository; -import org.ektorp.DbAccessException; -import org.ektorp.DocumentNotFoundException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.cache.annotation.Caching; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Queue; -import java.util.Timer; -import java.util.TimerTask; -import java.util.concurrent.ConcurrentLinkedQueue; -import java.util.stream.Collectors; - -/** - * Performs all content and answer related operations. - */ -@Service -public class ContentServiceImpl extends DefaultEntityServiceImpl<Content> implements ContentService, ApplicationEventPublisherAware { - private UserService userService; - - private LogEntryRepository dbLogger; - - private SessionRepository sessionRepository; - - private ContentRepository contentRepository; - - private AnswerRepository answerRepository; - - private ImageUtils imageUtils; - - @Value("${upload.filesize_b}") - private int uploadFileSizeByte; - - private ApplicationEventPublisher publisher; - - private static final Logger logger = LoggerFactory.getLogger(ContentServiceImpl.class); - - private HashMap<String, Timer> timerList = new HashMap<>(); - - private final Queue<AnswerQueueElement> answerQueue = new ConcurrentLinkedQueue<>(); - - public ContentServiceImpl( - ContentRepository repository, - AnswerRepository answerRepository, - SessionRepository sessionRepository, - LogEntryRepository dbLogger, - UserService userService, - ImageUtils imageUtils, - @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { - super(Content.class, repository, jackson2HttpMessageConverter.getObjectMapper()); - this.contentRepository = repository; - this.answerRepository = answerRepository; - this.sessionRepository = sessionRepository; - this.dbLogger = dbLogger; - this.userService = userService; - this.imageUtils = imageUtils; - } - - @Scheduled(fixedDelay = 5000) - public void flushAnswerQueue() { - if (answerQueue.isEmpty()) { - // no need to send an empty bulk request. - return; - } - - final List<Answer> answerList = new ArrayList<>(); - final List<AnswerQueueElement> elements = new ArrayList<>(); - AnswerQueueElement entry; - while ((entry = this.answerQueue.poll()) != null) { - final Answer answer = entry.getAnswer(); - answerList.add(answer); - elements.add(entry); - } - try { - answerRepository.save(answerList); - - // Send NewAnswerEvents ... - for (AnswerQueueElement e : elements) { - this.publisher.publishEvent(new NewAnswerEvent(this, e.getSession(), e.getAnswer(), e.getUser(), e.getQuestion())); - } - } catch (final DbAccessException e) { - logger.error("Could not bulk save answers from queue.", e); - } - } - - @Cacheable("contents") - @Override - public Content get(final String id) { - try { - final Content content = super.get(id); - if (!"freetext".equals(content.getQuestionType()) && 0 == content.getPiRound()) { - /* needed for legacy questions whose piRound property has not been set */ - content.setPiRound(1); - } - content.updateRoundManagementState(); - //content.setSessionKeyword(sessionRepository.getSessionFromId(content.getSessionId()).getKeyword()); - - return content; - } catch (final DocumentNotFoundException e) { - logger.error("Could not get question {}.", id, e); - } - - return null; - } - - @Override - @Caching(evict = {@CacheEvict(value = "contentlists", key = "#sessionId"), - @CacheEvict(value = "lecturecontentlists", key = "#sessionId", condition = "#content.getQuestionVariant().equals('lecture')"), - @CacheEvict(value = "preparationcontentlists", key = "#sessionId", condition = "#content.getQuestionVariant().equals('preparation')"), - @CacheEvict(value = "flashcardcontentlists", key = "#sessionId", condition = "#content.getQuestionVariant().equals('flashcard')") }, - put = {@CachePut(value = "contents", key = "#content.id")}) - public Content save(final String sessionId, final Content content) { - content.setSessionId(sessionId); - try { - contentRepository.save(content); - - return content; - } catch (final IllegalArgumentException e) { - logger.error("Could not save content {}.", content, e); - } - - return null; - } - - @Override - @PreAuthorize("isAuthenticated()") - @Caching(evict = { - @CacheEvict(value = "contentlists", allEntries = true), - @CacheEvict(value = "lecturecontentlists", allEntries = true, condition = "#content.getQuestionVariant().equals('lecture')"), - @CacheEvict(value = "preparationcontentlists", allEntries = true, condition = "#content.getQuestionVariant().equals('preparation')"), - @CacheEvict(value = "flashcardcontentlists", allEntries = true, condition = "#content.getQuestionVariant().equals('flashcard')") }, - put = {@CachePut(value = "contents", key = "#content.id")}) - public Content update(final Content content) { - final User user = userService.getCurrentUser(); - final Content oldContent = contentRepository.findOne(content.getId()); - if (null == oldContent) { - throw new NotFoundException(); - } - - final Session session = sessionRepository.findOne(content.getSessionId()); - if (user == null || session == null || !session.isCreator(user)) { - throw new UnauthorizedException(); - } - - if ("freetext".equals(content.getQuestionType())) { - content.setPiRound(0); - } else if (content.getPiRound() < 1 || content.getPiRound() > 2) { - content.setPiRound(oldContent.getPiRound() > 0 ? oldContent.getPiRound() : 1); - } - - content.setId(oldContent.getId()); - content.setRevision(oldContent.getRevision()); - content.updateRoundManagementState(); - contentRepository.save(content); - - if (!oldContent.isActive() && content.isActive()) { - final UnlockQuestionEvent event = new UnlockQuestionEvent(this, session, content); - this.publisher.publishEvent(event); - } else if (oldContent.isActive() && !content.isActive()) { - final LockQuestionEvent event = new LockQuestionEvent(this, session, content); - this.publisher.publishEvent(event); - } - return content; - } - - /* FIXME: caching */ - @Override - @PreAuthorize("isAuthenticated()") - //@Cacheable("contentlists") - public List<Content> getBySessionKey(final String sessionkey) { - final Session session = getSession(sessionkey); - final User user = userService.getCurrentUser(); - if (session.isCreator(user)) { - return contentRepository.findBySessionIdForSpeaker(session.getId()); - } else { - return contentRepository.findBySessionIdForUsers(session.getId()); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countBySessionKey(final String sessionkey) { - final Session session = sessionRepository.findByKeyword(sessionkey); - return contentRepository.countBySessionId(session.getId()); - } - - /* FIXME: #content.getSessionKeyword() cannot be checked since keyword is no longer set for content. */ - @Override - @PreAuthorize("hasPermission(#content.getSessionKeyword(), 'session', 'owner')") - public Content save(final Content content) { - final Session session = sessionRepository.findByKeyword(content.getSessionKeyword()); - content.setSessionId(session.getId()); - content.setTimestamp(System.currentTimeMillis() / 1000L); - - if ("freetext".equals(content.getQuestionType())) { - content.setPiRound(0); - } else if (content.getPiRound() < 1 || content.getPiRound() > 2) { - content.setPiRound(1); - } - - // convert imageurl to base64 if neccessary - if ("grid".equals(content.getQuestionType()) && !content.getImage().startsWith("http")) { - // base64 adds offset to filesize, formula taken from: http://en.wikipedia.org/wiki/Base64#MIME - final int fileSize = (int) ((content.getImage().length() - 814) / 1.37); - if (fileSize > uploadFileSizeByte) { - logger.error("Could not save file. File is too large with {} Byte.", fileSize); - throw new BadRequestException(); - } - } - - final Content result = save(session.getId(), content); - - final NewQuestionEvent event = new NewQuestionEvent(this, session, result); - this.publisher.publishEvent(event); - - return result; - } - - /* TODO: Only evict cache entry for the content's session. This requires some refactoring. */ - @Override - @PreAuthorize("hasPermission(#contentId, 'content', 'owner')") - @Caching(evict = { - @CacheEvict("answerlists"), - @CacheEvict(value = "contents", key = "#contentId"), - @CacheEvict(value = "contentlists", allEntries = true), - @CacheEvict(value = "lecturecontentlists", allEntries = true /*, condition = "#content.getQuestionVariant().equals('lecture')"*/), - @CacheEvict(value = "preparationcontentlists", allEntries = true /*, condition = "#content.getQuestionVariant().equals('preparation')"*/), - @CacheEvict(value = "flashcardcontentlists", allEntries = true /*, condition = "#content.getQuestionVariant().equals('flashcard')"*/) }) - public void delete(final String contentId) { - final Content content = contentRepository.findOne(contentId); - if (content == null) { - throw new NotFoundException(); - } - - final Session session = sessionRepository.findOne(content.getSessionId()); - if (session == null) { - throw new UnauthorizedException(); - } - - try { - final int count = answerRepository.deleteByContentId(contentId); - contentRepository.delete(contentId); - dbLogger.log("delete", "type", "content", "answerCount", count); - } catch (final IllegalArgumentException e) { - logger.error("Could not delete content {}.", contentId, e); - } - - final DeleteQuestionEvent event = new DeleteQuestionEvent(this, session, content); - this.publisher.publishEvent(event); - } - - @PreAuthorize("hasPermission(#session, 'owner')") - @Caching(evict = { - @CacheEvict(value = "contents", allEntries = true), - @CacheEvict(value = "contentlists", key = "#session.getId()"), - @CacheEvict(value = "lecturecontentlists", key = "#session.getId()", condition = "'lecture'.equals(#variant)"), - @CacheEvict(value = "preparationcontentlists", key = "#session.getId()", condition = "'preparation'.equals(#variant)"), - @CacheEvict(value = "flashcardcontentlists", key = "#session.getId()", condition = "'flashcard'.equals(#variant)") }) - private void deleteBySessionAndVariant(final Session session, final String variant) { - final List<String> contentIds; - if ("all".equals(variant)) { - contentIds = contentRepository.findIdsBySessionId(session.getId()); - } else { - contentIds = contentRepository.findIdsBySessionIdAndVariant(session.getId(), variant); - } - - final int answerCount = answerRepository.deleteByContentIds(contentIds); - final int contentCount = contentRepository.deleteBySessionId(session.getId()); - dbLogger.log("delete", "type", "question", "questionCount", contentCount); - dbLogger.log("delete", "type", "answer", "answerCount", answerCount); - - final DeleteAllQuestionsEvent event = new DeleteAllQuestionsEvent(this, session); - this.publisher.publishEvent(event); - } - - @Override - @PreAuthorize("isAuthenticated()") - public void deleteAllContent(final String sessionkey) { - final Session session = getSessionWithAuthCheck(sessionkey); - deleteBySessionAndVariant(session, "all"); - } - - @Override - @PreAuthorize("isAuthenticated()") - public void deleteLectureQuestions(final String sessionkey) { - final Session session = getSessionWithAuthCheck(sessionkey); - deleteBySessionAndVariant(session, "lecture"); - } - - @Override - @PreAuthorize("isAuthenticated()") - public void deletePreparationQuestions(final String sessionkey) { - final Session session = getSessionWithAuthCheck(sessionkey); - deleteBySessionAndVariant(session, "preparation"); - } - - @Override - @PreAuthorize("isAuthenticated()") - public void deleteFlashcards(final String sessionkey) { - final Session session = getSessionWithAuthCheck(sessionkey); - deleteBySessionAndVariant(session, "flashcard"); - } - - @Override - @PreAuthorize("isAuthenticated() and hasPermission(#questionId, 'content', 'owner')") - public void startNewPiRound(final String questionId, User user) { - final Content content = contentRepository.findOne(questionId); - final Session session = sessionRepository.findOne(content.getSessionId()); - - if (null == user) { - user = userService.getCurrentUser(); - } - - cancelDelayedPiRoundChange(questionId); - - content.setPiRoundEndTime(0); - content.setVotingDisabled(true); - content.updateRoundManagementState(); - update(content); - - this.publisher.publishEvent(new PiRoundEndEvent(this, session, content)); - } - - @Override - @PreAuthorize("hasPermission(#questionId, 'content', 'owner')") - public void startNewPiRoundDelayed(final String questionId, final int time) { - final ContentService contentService = this; - final User user = userService.getCurrentUser(); - final Content content = contentRepository.findOne(questionId); - final Session session = sessionRepository.findOne(content.getSessionId()); - - final Date date = new Date(); - final Timer timer = new Timer(); - final Date endDate = new Date(date.getTime() + (time * 1000)); - content.updateRoundStartVariables(date, endDate); - update(content); - - this.publisher.publishEvent(new PiRoundDelayedStartEvent(this, session, content)); - timerList.put(questionId, timer); - - timer.schedule(new TimerTask() { - @Override - public void run() { - contentService.startNewPiRound(questionId, user); - } - }, endDate); - } - - @Override - @PreAuthorize("hasPermission(#questionId, 'content', 'owner')") - public void cancelPiRoundChange(final String questionId) { - final Content content = contentRepository.findOne(questionId); - final Session session = sessionRepository.findOne(content.getSessionId()); - - cancelDelayedPiRoundChange(questionId); - content.resetRoundManagementState(); - - if (0 == content.getPiRound() || 1 == content.getPiRound()) { - content.setPiRoundFinished(false); - } else { - content.setPiRound(1); - content.setPiRoundFinished(true); - } - - update(content); - this.publisher.publishEvent(new PiRoundCancelEvent(this, session, content)); - } - - @Override - public void cancelDelayedPiRoundChange(final String questionId) { - Timer timer = timerList.get(questionId); - - if (null != timer) { - timer.cancel(); - timerList.remove(questionId); - timer.purge(); - } - } - - @Override - @PreAuthorize("hasPermission(#questionId, 'content', 'owner')") - @CacheEvict("answerlists") - public void resetPiRoundState(final String questionId) { - final Content content = contentRepository.findOne(questionId); - final Session session = sessionRepository.findOne(content.getSessionId()); - cancelDelayedPiRoundChange(questionId); - - if ("freetext".equals(content.getQuestionType())) { - content.setPiRound(0); - } else { - content.setPiRound(1); - } - - content.resetRoundManagementState(); - answerRepository.deleteByContentId(content.getId()); - update(content); - this.publisher.publishEvent(new PiRoundResetEvent(this, session, content)); - } - - @Override - @PreAuthorize("hasPermission(#questionId, 'content', 'owner')") - public void setVotingAdmission(final String questionId, final boolean disableVoting) { - final Content content = contentRepository.findOne(questionId); - final Session session = sessionRepository.findOne(content.getSessionId()); - content.setVotingDisabled(disableVoting); - - if (!disableVoting && !content.isActive()) { - content.setActive(true); - update(content); - } else { - update(content); - } - ArsnovaEvent event; - if (disableVoting) { - event = new LockVoteEvent(this, session, content); - } else { - event = new UnlockVoteEvent(this, session, content); - } - this.publisher.publishEvent(event); - } - - @Override - @PreAuthorize("isAuthenticated()") - @Caching(evict = { @CacheEvict(value = "contents", allEntries = true), - @CacheEvict(value = "contentlists", key = "#sessionId"), - @CacheEvict(value = "lecturecontentlists", key = "#sessionId"), - @CacheEvict(value = "preparationcontentlists", key = "#sessionId"), - @CacheEvict(value = "flashcardcontentlists", key = "#sessionId") }) - public void setVotingAdmissions(final String sessionkey, final boolean disableVoting, List<Content> contents) { - final User user = getCurrentUser(); - final Session session = getSession(sessionkey); - if (!session.isCreator(user)) { - throw new UnauthorizedException(); - } - for (final Content q : contents) { - if (!"flashcard".equals(q.getQuestionType())) { - q.setVotingDisabled(disableVoting); - } - } - ArsnovaEvent event; - if (disableVoting) { - event = new LockVotesEvent(this, session, contents); - } else { - event = new UnlockVotesEvent(this, session, contents); - } - this.publisher.publishEvent(event); - } - - @Override - @PreAuthorize("isAuthenticated()") - public void setVotingAdmissionForAllQuestions(final String sessionkey, final boolean disableVoting) { - final User user = getCurrentUser(); - final Session session = getSession(sessionkey); - if (!session.isCreator(user)) { - throw new UnauthorizedException(); - } - final List<Content> contents = contentRepository.findBySessionId(session.getId()); - setVotingAdmissionForAllQuestions(session.getId(), disableVoting); - } - - private Session getSessionWithAuthCheck(final String sessionKeyword) { - final User user = userService.getCurrentUser(); - final Session session = sessionRepository.findByKeyword(sessionKeyword); - if (user == null || session == null || !session.isCreator(user)) { - throw new UnauthorizedException(); - } - return session; - } - - @Override - @PreAuthorize("hasPermission(#questionId, 'content', 'owner')") - public void deleteAnswers(final String questionId) { - final Content content = contentRepository.findOne(questionId); - content.resetQuestionState(); - update(content); - answerRepository.deleteByContentId(content.getId()); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<String> getUnAnsweredQuestionIds(final String sessionKey) { - final User user = getCurrentUser(); - final Session session = getSession(sessionKey); - return contentRepository.findUnansweredIdsBySessionIdAndUser(session.getId(), user); - } - - private User getCurrentUser() { - final User user = userService.getCurrentUser(); - if (user == null) { - throw new UnauthorizedException(); - } - return user; - } - - @Override - @PreAuthorize("isAuthenticated()") - public Answer getMyAnswer(final String questionId) { - final Content content = get(questionId); - if (content == null) { - throw new NotFoundException(); - } - return answerRepository.findByQuestionIdUserPiRound(questionId, userService.getCurrentUser(), content.getPiRound()); - } - - @Override - public void getFreetextAnswerAndMarkRead(final String answerId, final User user) { - final Answer answer = answerRepository.findOne(answerId); - if (answer == null) { - throw new NotFoundException(); - } - if (answer.isRead()) { - return; - } - final Session session = sessionRepository.findOne(answer.getSessionId()); - if (session.isCreator(user)) { - answer.setRead(true); - answerRepository.save(answer); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Answer> getAnswers(final String questionId, final int piRound, final int offset, final int limit) { - final Content content = contentRepository.findOne(questionId); - if (content == null) { - throw new NotFoundException(); - } - return "freetext".equals(content.getQuestionType()) - ? getFreetextAnswersByQuestionId(questionId, offset, limit) - : answerRepository.findByContentIdPiRound(content.getId(), piRound); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Answer> getAnswers(final String questionId, final int offset, final int limit) { - final Content content = get(questionId); - if (content == null) { - throw new NotFoundException(); - } - if ("freetext".equals(content.getQuestionType())) { - return getFreetextAnswersByQuestionId(questionId, offset, limit); - } else { - return answerRepository.findByContentIdPiRound(content.getId(), content.getPiRound()); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Answer> getAllAnswers(final String questionId, final int offset, final int limit) { - final Content content = get(questionId); - if (content == null) { - throw new NotFoundException(); - } - if ("freetext".equals(content.getQuestionType())) { - return getFreetextAnswersByQuestionId(questionId, offset, limit); - } else { - return answerRepository.findByContentId(content.getId()); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countAnswersByQuestionIdAndRound(final String questionId) { - final Content content = get(questionId); - if (content == null) { - return 0; - } - - if ("freetext".equals(content.getQuestionType())) { - return answerRepository.countByContentId(content.getId()); - } else { - return answerRepository.countByContentIdRound(content.getId(), content.getPiRound()); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countAnswersByQuestionIdAndRound(final String questionId, final int piRound) { - final Content content = get(questionId); - if (content == null) { - return 0; - } - - return answerRepository.countByContentIdRound(content.getId(), piRound); - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countTotalAbstentionsByQuestionId(final String questionId) { - final Content content = get(questionId); - if (content == null) { - return 0; - } - - return answerRepository.countByContentId(questionId); - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countTotalAnswersByQuestionId(final String questionId) { - final Content content = get(questionId); - if (content == null) { - return 0; - } - - return answerRepository.countByContentId(content.getId()); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Answer> getFreetextAnswersByQuestionId(final String questionId, final int offset, final int limit) { - final List<Answer> answers = answerRepository.findByContentId(questionId, offset, limit); - if (answers == null) { - throw new NotFoundException(); - } - /* Remove user for privacy concerns */ - for (Answer answer : answers) { - answer.setUser(null); - } - - return answers; - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Answer> getMyAnswersBySessionKey(final String sessionKey) { - final Session session = getSession(sessionKey); - // Load contents first because we are only interested in answers of the latest piRound. - final List<Content> contents = getBySessionKey(sessionKey); - final Map<String, Content> questionIdToQuestion = new HashMap<>(); - for (final Content content : contents) { - questionIdToQuestion.put(content.getId(), content); - } - - /* filter answers by active piRound per question */ - final List<Answer> answers = answerRepository.findByUserSessionId(userService.getCurrentUser(), session.getId()); - final List<Answer> filteredAnswers = new ArrayList<>(); - for (final Answer answer : answers) { - final Content content = questionIdToQuestion.get(answer.getQuestionId()); - if (content == null) { - // Content is not present. Most likely it has been locked by the - // Session's creator. Locked Questions do not appear in this list. - continue; - } - if (0 == answer.getPiRound() && !"freetext".equals(content.getQuestionType())) { - answer.setPiRound(1); - } - - // discard all answers that aren't in the same piRound as the content - if (answer.getPiRound() == content.getPiRound()) { - filteredAnswers.add(answer); - } - } - - return filteredAnswers; - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countTotalAnswersBySessionKey(final String sessionKey) { - return answerRepository.countBySessionKey(sessionKey); - } - - @Override - @PreAuthorize("isAuthenticated()") - @CacheEvict(value = "answerlists", key = "#contentId") - public Answer saveAnswer(final String contentId, final Answer answer) { - final User user = getCurrentUser(); - final Content content = get(contentId); - if (content == null) { - throw new NotFoundException(); - } - final Session session = sessionRepository.findOne(content.getSessionId()); - - answer.setUser(user.getUsername()); - answer.setQuestionId(content.getId()); - answer.setSessionId(session.getId()); - answer.setQuestionVariant(content.getQuestionVariant()); - answer.setQuestionValue(content.calculateValue(answer)); - answer.setTimestamp(new Date().getTime()); - - if ("freetext".equals(content.getQuestionType())) { - answer.setPiRound(0); - imageUtils.generateThumbnailImage(answer); - if (content.isFixedAnswer() && content.getText() != null) { - answer.setAnswerTextRaw(answer.getAnswerText()); - - if (content.isStrictMode()) { - content.checkTextStrictOptions(answer); - } - answer.setQuestionValue(content.evaluateCorrectAnswerFixedText(answer.getAnswerTextRaw())); - answer.setSuccessfulFreeTextAnswer(content.isSuccessfulFreeTextAnswer(answer.getAnswerTextRaw())); - } - } else { - answer.setPiRound(content.getPiRound()); - } - - this.answerQueue.offer(new AnswerQueueElement(session, content, answer, user)); - - return answer; - } - - @Override - @PreAuthorize("isAuthenticated()") - @CacheEvict(value = "answerlists", allEntries = true) - public Answer updateAnswer(final Answer answer) { - final User user = userService.getCurrentUser(); - final Answer realAnswer = this.getMyAnswer(answer.getQuestionId()); - if (user == null || realAnswer == null || !user.getUsername().equals(realAnswer.getUser())) { - throw new UnauthorizedException(); - } - - final Content content = get(answer.getQuestionId()); - if ("freetext".equals(content.getQuestionType())) { - imageUtils.generateThumbnailImage(realAnswer); - content.checkTextStrictOptions(realAnswer); - } - final Session session = sessionRepository.findOne(content.getSessionId()); - answer.setUser(user.getUsername()); - answer.setQuestionId(content.getId()); - answer.setSessionId(session.getId()); - answerRepository.save(realAnswer); - this.publisher.publishEvent(new NewAnswerEvent(this, session, answer, user, content)); - - return answer; - } - - @Override - @PreAuthorize("isAuthenticated()") - @CacheEvict(value = "answerlists", allEntries = true) - public void deleteAnswer(final String questionId, final String answerId) { - final Content content = contentRepository.findOne(questionId); - if (content == null) { - throw new NotFoundException(); - } - final User user = userService.getCurrentUser(); - final Session session = sessionRepository.findOne(content.getSessionId()); - if (user == null || session == null || !session.isCreator(user)) { - throw new UnauthorizedException(); - } - answerRepository.delete(answerId); - - this.publisher.publishEvent(new DeleteAnswerEvent(this, session, content)); - } - - /* FIXME: caching */ - @Override - @PreAuthorize("isAuthenticated()") - //@Cacheable("lecturecontentlists") - public List<Content> getLectureQuestions(final String sessionkey) { - final Session session = getSession(sessionkey); - final User user = userService.getCurrentUser(); - if (session.isCreator(user)) { - return contentRepository.findBySessionIdOnlyLectureVariant(session.getId()); - } else { - return contentRepository.findBySessionIdOnlyLectureVariantAndActive(session.getId()); - } - } - - /* FIXME: caching */ - @Override - @PreAuthorize("isAuthenticated()") - //@Cacheable("flashcardcontentlists") - public List<Content> getFlashcards(final String sessionkey) { - final Session session = getSession(sessionkey); - final User user = userService.getCurrentUser(); - if (session.isCreator(user)) { - return contentRepository.findBySessionIdOnlyFlashcardVariant(session.getId()); - } else { - return contentRepository.findBySessionIdOnlyFlashcardVariantAndActive(session.getId()); - } - } - - /* FIXME: caching */ - @Override - @PreAuthorize("isAuthenticated()") - //@Cacheable("preparationcontentlists") - public List<Content> getPreparationQuestions(final String sessionkey) { - final Session session = getSession(sessionkey); - final User user = userService.getCurrentUser(); - if (session.isCreator(user)) { - return contentRepository.findBySessionIdOnlyPreparationVariant(session.getId()); - } else { - return contentRepository.findBySessionIdOnlyPreparationVariantAndActive(session.getId()); - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Content> replaceImageData(final List<Content> contents) { - for (Content q : contents) { - if (q.getImage() != null && q.getImage().startsWith("data:image/")) { - q.setImage("true"); - } - } - - return contents; - } - - private Session getSession(final String sessionkey) { - final Session session = sessionRepository.findByKeyword(sessionkey); - if (session == null) { - throw new NotFoundException(); - } - return session; - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countLectureQuestions(final String sessionkey) { - return contentRepository.countLectureVariantBySessionId(getSession(sessionkey).getId()); - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countFlashcards(final String sessionkey) { - return contentRepository.countFlashcardVariantBySessionId(getSession(sessionkey).getId()); - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countPreparationQuestions(final String sessionkey) { - return contentRepository.countPreparationVariantBySessionId(getSession(sessionkey).getId()); - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countLectureQuestionAnswers(final String sessionkey) { - return this.countLectureQuestionAnswersInternal(sessionkey); - } - - /* - * The "internal" suffix means it is called by internal services that have no authentication! - * TODO: Find a better way of doing this... - */ - @Override - public int countLectureQuestionAnswersInternal(final String sessionkey) { - return answerRepository.countBySessionIdLectureVariant(getSession(sessionkey).getId()); - } - - @Override - public Map<String, Object> countAnswersAndAbstentionsInternal(final String questionId) { - final Content content = get(questionId); - HashMap<String, Object> map = new HashMap<>(); - - if (content == null) { - return null; - } - - map.put("_id", questionId); - map.put("answers", answerRepository.countByContentIdRound(content.getId(), content.getPiRound())); - map.put("abstentions", answerRepository.countByContentId(questionId)); - - return map; - } - - @Override - @PreAuthorize("isAuthenticated()") - public int countPreparationQuestionAnswers(final String sessionkey) { - return this.countPreparationQuestionAnswersInternal(sessionkey); - } - - /* - * The "internal" suffix means it is called by internal services that have no authentication! - * TODO: Find a better way of doing this... - */ - @Override - public int countPreparationQuestionAnswersInternal(final String sessionkey) { - return answerRepository.countBySessionIdPreparationVariant(getSession(sessionkey).getId()); - } - - /* - * The "internal" suffix means it is called by internal services that have no authentication! - * TODO: Find a better way of doing this... - */ - @Override - public int countFlashcardsForUserInternal(final String sessionkey) { - return contentRepository.findBySessionIdOnlyFlashcardVariantAndActive(getSession(sessionkey).getId()).size(); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<String> getUnAnsweredLectureQuestionIds(final String sessionkey) { - final User user = getCurrentUser(); - return this.getUnAnsweredLectureQuestionIds(sessionkey, user); - } - - @Override - public List<String> getUnAnsweredLectureQuestionIds(final String sessionkey, final User user) { - final Session session = getSession(sessionkey); - return contentRepository.findUnansweredIdsBySessionIdAndUserOnlyLectureVariant(session.getId(), user); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<String> getUnAnsweredPreparationQuestionIds(final String sessionkey) { - final User user = getCurrentUser(); - return this.getUnAnsweredPreparationQuestionIds(sessionkey, user); - } - - @Override - public List<String> getUnAnsweredPreparationQuestionIds(final String sessionkey, final User user) { - final Session session = getSession(sessionkey); - return contentRepository.findUnansweredIdsBySessionIdAndUserOnlyPreparationVariant(session.getId(), user); - } - - @Override - @PreAuthorize("isAuthenticated()") - public void publishAll(final String sessionkey, final boolean publish) { - /* TODO: resolve redundancies */ - final User user = getCurrentUser(); - final Session session = getSession(sessionkey); - if (!session.isCreator(user)) { - throw new UnauthorizedException(); - } - final List<Content> contents = contentRepository.findBySessionId(session.getId()); - publishQuestions(sessionkey, publish, contents); - } - - @Override - @PreAuthorize("isAuthenticated()") - @Caching(evict = { @CacheEvict(value = "contents", allEntries = true), - @CacheEvict(value = "contentlists", key = "#sessionId"), - @CacheEvict(value = "lecturecontentlists", key = "#sessionId"), - @CacheEvict(value = "preparationcontentlists", key = "#sessionId"), - @CacheEvict(value = "flashcardcontentlists", key = "#sessionId") }) - public void publishQuestions(final String sessionkey, final boolean publish, List<Content> contents) { - final User user = getCurrentUser(); - final Session session = getSession(sessionkey); - if (!session.isCreator(user)) { - throw new UnauthorizedException(); - } - for (final Content content : contents) { - content.setActive(publish); - } - contentRepository.save(contents); - ArsnovaEvent event; - if (publish) { - event = new UnlockQuestionsEvent(this, session, contents); - } else { - event = new LockQuestionsEvent(this, session, contents); - } - this.publisher.publishEvent(event); - } - - @Override - @PreAuthorize("isAuthenticated()") - @CacheEvict(value = "answerlists", allEntries = true) - public void deleteAllQuestionsAnswers(final String sessionkey) { - final User user = getCurrentUser(); - final Session session = getSession(sessionkey); - if (!session.isCreator(user)) { - throw new UnauthorizedException(); - } - - final List<Content> contents = contentRepository.findBySessionIdAndVariantAndActive(session.getId()); - resetContentsRoundState(session.getId(), contents); - final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); - answerRepository.deleteAllAnswersForQuestions(contentIds); - - this.publisher.publishEvent(new DeleteAllQuestionsAnswersEvent(this, session)); - } - - /* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */ - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - @CacheEvict(value = "answerlists", allEntries = true) - public void deleteAllPreparationAnswers(String sessionkey) { - final Session session = getSession(sessionkey); - - final List<Content> contents = contentRepository.findBySessionIdAndVariantAndActive(session.getId(), "preparation"); - resetContentsRoundState(session.getId(), contents); - final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); - answerRepository.deleteAllAnswersForQuestions(contentIds); - - this.publisher.publishEvent(new DeleteAllPreparationAnswersEvent(this, session)); - } - - /* TODO: Only evict cache entry for the answer's question. This requires some refactoring. */ - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - @CacheEvict(value = "answerlists", allEntries = true) - public void deleteAllLectureAnswers(String sessionkey) { - final Session session = getSession(sessionkey); - - final List<Content> contents = contentRepository.findBySessionIdAndVariantAndActive(session.getId(), "lecture"); - resetContentsRoundState(session.getId(), contents); - final List<String> contentIds = contents.stream().map(Content::getId).collect(Collectors.toList()); - answerRepository.deleteAllAnswersForQuestions(contentIds); - - this.publisher.publishEvent(new DeleteAllLectureAnswersEvent(this, session)); - } - - @Caching(evict = { - @CacheEvict(value = "contents", allEntries = true), - @CacheEvict(value = "contentlists", key = "#sessionId"), - @CacheEvict(value = "lecturecontentlists", key = "#sessionId"), - @CacheEvict(value = "preparationcontentlists", key = "#sessionId"), - @CacheEvict(value = "flashcardcontentlists", key = "#sessionId") }) - private void resetContentsRoundState(final String sessionId, final List<Content> contents) { - for (final Content q : contents) { - /* TODO: Check if setting the sessionId is necessary. */ - q.setSessionId(sessionId); - q.resetQuestionState(); - } - contentRepository.save(contents); - } - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { - this.publisher = publisher; - } - - @Override - public String getImage(String questionId, String answerId) { - final List<Answer> answers = getAnswers(questionId, -1, -1); - Answer answer = null; - - for (Answer a : answers) { - if (answerId.equals(a.getId())) { - answer = a; - break; - } - } - - if (answer == null) { - throw new NotFoundException(); - } - - return answer.getAnswerImage(); - } - - @Override - public String getQuestionImage(String questionId) { - Content content = contentRepository.findOne(questionId); - String imageData = content.getImage(); - - if (imageData == null) { - imageData = ""; - } - - return imageData; - } - - @Override - public String getQuestionFcImage(String questionId) { - Content content = contentRepository.findOne(questionId); - String imageData = content.getFcImage(); - - if (imageData == null) { - imageData = ""; - } - - return imageData; - } -} diff --git a/src/main/java/de/thm/arsnova/services/FeedbackStorageService.java b/src/main/java/de/thm/arsnova/services/FeedbackStorageService.java deleted file mode 100644 index 279f703e778036684eb9c965594f642ca55bd506..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/FeedbackStorageService.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Feedback; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; - -import java.util.List; -import java.util.Map; - -public interface FeedbackStorageService { - Feedback getBySession(Session session); - Integer getBySessionAndUser(Session session, User u); - void save(Session session, int value, User user); - Map<Session, List<User>> cleanVotes(int cleanupFeedbackDelay); - List<User> cleanVotesBySession(Session session, int cleanupFeedbackDelayInMins); -} diff --git a/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java b/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java deleted file mode 100644 index b0465a031028cddd6f3178ffe11f00bb46fde75a..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/MotdServiceImpl.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Motd; -import de.thm.arsnova.entities.MotdList; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.persistance.MotdListRepository; -import de.thm.arsnova.persistance.MotdRepository; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Cacheable; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Service; - -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.StringTokenizer; -/** - * Performs all question, interposed question, and answer related operations. - */ -@Service -public class MotdServiceImpl extends DefaultEntityServiceImpl<Motd> implements MotdService { - private UserService userService; - - private SessionService sessionService; - - private MotdRepository motdRepository; - - private MotdListRepository motdListRepository; - - public MotdServiceImpl( - MotdRepository repository, - MotdListRepository motdListRepository, - UserService userService, - SessionService sessionService, - @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { - super(Motd.class, repository, jackson2HttpMessageConverter.getObjectMapper()); - this.motdRepository = repository; - this.motdListRepository = motdListRepository; - this.userService = userService; - this.sessionService = sessionService; - } - - @Override - @PreAuthorize("isAuthenticated()") - public Motd getByKey(final String key) { - return motdRepository.findByKey(key); - } - - @Override - @PreAuthorize("hasPermission('', 'motd', 'admin')") - public List<Motd> getAdminMotds() { - return motdRepository.findGlobalForAdmin(); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public List<Motd> getAllSessionMotds(final String sessionkey) { - return motdRepository.findBySessionKey(sessionkey); - } - - @Override - @Cacheable(cacheNames = "motds", key = "('session').concat(#sessionkey)") - public List<Motd> getCurrentSessionMotds(final Date clientdate, final String sessionkey) { - final List<Motd> motds = motdRepository.findBySessionKey(sessionkey); - return filterMotdsByDate(motds, clientdate); - } - - @Override - @Cacheable(cacheNames = "motds", key = "#audience") - public List<Motd> getCurrentMotds(final Date clientdate, final String audience) { - final List<Motd> motds; - switch (audience) { - case "all": motds = motdRepository.findGlobalForAll(); break; - case "loggedIn": motds = motdRepository.findGlobalForLoggedIn(); break; - case "students": motds = motdRepository.findForStudents(); break; - case "tutors": motds = motdRepository.findGlobalForTutors(); break; - default: throw new IllegalArgumentException("Invalid audience."); - } - - return filterMotdsByDate(motds, clientdate); - } - - @Override - public List<Motd> filterMotdsByDate(List<Motd> list, Date clientdate) { - List<Motd> returns = new ArrayList<>(); - for (Motd motd : list) { - if (motd.getStartdate().before(clientdate) && motd.getEnddate().after(clientdate)) { - returns.add(motd); - } - } - return returns; - } - - @Override - public List<Motd> filterMotdsByList(List<Motd> list, MotdList motdlist) { - if (motdlist != null && motdlist.getMotdkeys() != null && !motdlist.getMotdkeys().isEmpty()) { - List<Motd> returns = new ArrayList<>(); - HashSet<String> keys = new HashSet<>(500); // Or a more realistic size - StringTokenizer st = new StringTokenizer(motdlist.getMotdkeys(), ","); - while (st.hasMoreTokens()) { - keys.add(st.nextToken()); - } - for (Motd motd : list) { - if (!keys.contains(motd.getMotdkey())) { - returns.add(motd); - } - } - return returns; - } else { - return list; - } - } - - @Override - @PreAuthorize("hasPermission('', 'motd', 'admin')") - public Motd save(final Motd motd) { - return createOrUpdateMotd(motd); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public Motd save(final String sessionkey, final Motd motd) { - Session session = sessionService.getByKey(sessionkey); - motd.setSessionId(session.getId()); - - return createOrUpdateMotd(motd); - } - - @Override - @PreAuthorize("hasPermission(1,'motd','admin')") - public Motd update(final Motd motd) { - return createOrUpdateMotd(motd); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public Motd update(final String sessionkey, final Motd motd) { - return createOrUpdateMotd(motd); - } - - @CacheEvict(cacheNames = "motds", key = "#motd.audience.concat(#motd.sessionkey)") - private Motd createOrUpdateMotd(final Motd motd) { - if (motd.getMotdkey() != null) { - Motd oldMotd = motdRepository.findByKey(motd.getMotdkey()); - if (!(motd.getId().equals(oldMotd.getId()) && motd.getSessionkey().equals(oldMotd.getSessionkey()) - && motd.getAudience().equals(oldMotd.getAudience()))) { - throw new BadRequestException(); - } - } - - if (null != motd.getId()) { - Motd oldMotd = get(motd.getId()); - motd.setMotdkey(oldMotd.getMotdkey()); - } else { - motd.setMotdkey(sessionService.generateKey()); - } - save(motd); - - return motdRepository.save(motd); - } - - @Override - @PreAuthorize("hasPermission('', 'motd', 'admin')") - @CacheEvict(cacheNames = "motds", key = "#motd.audience.concat(#motd.sessionkey)") - public void delete(Motd motd) { - motdRepository.delete(motd); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public void deleteBySessionKey(final String sessionkey, Motd motd) { - motdRepository.delete(motd); - } - - @Override - @PreAuthorize("isAuthenticated()") - @Cacheable(cacheNames = "motdlist", key = "#username") - public MotdList getMotdListByUsername(final String username) { - final User user = userService.getCurrentUser(); - if (username.equals(user.getUsername()) && !"guest".equals(user.getType())) { - return motdListRepository.findByUsername(username); - } - return null; - } - - @Override - @PreAuthorize("isAuthenticated()") - @CachePut(cacheNames = "motdlist", key = "#motdList.username") - public MotdList saveMotdList(MotdList motdList) { - final User user = userService.getCurrentUser(); - if (user.getUsername().equals(motdList.getUsername())) { - return motdListRepository.save(motdList); - } - return null; - } - - @Override - @PreAuthorize("isAuthenticated()") - public MotdList updateMotdList(MotdList motdList) { - final User user = userService.getCurrentUser(); - if (user.getUsername().equals(motdList.getUsername())) { - return motdListRepository.save(motdList); - } - return null; - } -} diff --git a/src/main/java/de/thm/arsnova/services/SessionService.java b/src/main/java/de/thm/arsnova/services/SessionService.java deleted file mode 100644 index 8b8f650405c0073bf9e40bead5b54199f14f4303..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/SessionService.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.connector.model.Course; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.SessionFeature; -import de.thm.arsnova.entities.SessionInfo; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ImportExportSession; -import de.thm.arsnova.entities.transport.ScoreStatistics; - -import java.util.List; -import java.util.UUID; - -/** - * The functionality the session service should provide. - */ -public interface SessionService extends EntityService<Session> { - Session getByKey(String keyword); - - Session getForAdmin(final String keyword); - - Session getInternal(String keyword, User user); - - Session save(Session session); - - boolean isKeyAvailable(String keyword); - - String generateKey(); - - List<Session> getUserSessions(String username); - - List<Session> getUserVisitedSessions(String username); - - List<Session> getMySessions(int offset, int limit); - - List<Session> getMyVisitedSessions(int offset, int limit); - - int countSessionsByCourses(List<Course> courses); - - int activeUsers(String sessionkey); - - Session setActive(String sessionkey, Boolean lock); - - Session join(String keyword, UUID socketId); - - Session update(String sessionkey, Session session); - - Session updateCreator(String sessionkey, String newCreator); - - Session updateInternal(Session session, User user); - - int[] deleteCascading(Session session); - - ScoreStatistics getLearningProgress(String sessionkey, String type, String questionVariant); - - ScoreStatistics getMyLearningProgress(String sessionkey, String type, String questionVariant); - - List<SessionInfo> getMySessionsInfo(int offset, int limit); - - List<SessionInfo> getPublicPoolSessionsInfo(); - - List<SessionInfo> getMyPublicPoolSessionsInfo(); - - List<SessionInfo> getMyVisitedSessionsInfo(int offset, int limit); - - SessionInfo importSession(ImportExportSession session); - - ImportExportSession exportSession(String sessionkey, Boolean withAnswerStatistics, Boolean withFeedbackQuestions); - - SessionInfo copySessionToPublicPool(String sessionkey, de.thm.arsnova.entities.transport.ImportExportSession.PublicPool pp); - - SessionFeature getFeatures(String sessionkey); - - SessionFeature updateFeatures(String sessionkey, SessionFeature features); - - boolean lockFeedbackInput(String sessionkey, Boolean lock); - - boolean flipFlashcards(String sessionkey, Boolean flip); - - void deleteInactiveSessions(); - - void deleteInactiveVisitedSessionLists(); -} diff --git a/src/main/java/de/thm/arsnova/services/SessionServiceImpl.java b/src/main/java/de/thm/arsnova/services/SessionServiceImpl.java deleted file mode 100644 index d2a0ee6a1280277a324a18b020b768fc1b23a2c5..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/SessionServiceImpl.java +++ /dev/null @@ -1,612 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.persistance.AnswerRepository; -import de.thm.arsnova.persistance.CommentRepository; -import de.thm.arsnova.persistance.ContentRepository; -import de.thm.arsnova.persistance.LogEntryRepository; -import de.thm.arsnova.util.ImageUtils; -import de.thm.arsnova.connector.client.ConnectorClient; -import de.thm.arsnova.connector.model.Course; -import de.thm.arsnova.services.score.ScoreCalculatorFactory; -import de.thm.arsnova.services.score.ScoreCalculator; -import de.thm.arsnova.entities.ScoreOptions; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.SessionFeature; -import de.thm.arsnova.entities.SessionInfo; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ImportExportSession; -import de.thm.arsnova.entities.transport.ScoreStatistics; -import de.thm.arsnova.events.DeleteSessionEvent; -import de.thm.arsnova.events.FeatureChangeEvent; -import de.thm.arsnova.events.FlipFlashcardsEvent; -import de.thm.arsnova.events.LockFeedbackEvent; -import de.thm.arsnova.events.NewSessionEvent; -import de.thm.arsnova.events.StatusSessionEvent; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.exceptions.ForbiddenException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.exceptions.PayloadTooLargeException; -import de.thm.arsnova.persistance.SessionRepository; -import de.thm.arsnova.persistance.VisitedSessionRepository; -import org.ektorp.UpdateConflictException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.CachePut; -import org.springframework.cache.annotation.Caching; -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.stereotype.Service; - -import java.io.Serializable; -import java.util.Comparator; -import java.util.List; -import java.util.UUID; - -/** - * Performs all session related operations. - */ -@Service -public class SessionServiceImpl extends DefaultEntityServiceImpl<Session> implements SessionService, ApplicationEventPublisherAware { - private static final long SESSION_INACTIVITY_CHECK_INTERVAL_MS = 30 * 60 * 1000L; - - private static final Logger logger = LoggerFactory.getLogger(SessionServiceImpl.class); - - private LogEntryRepository dbLogger; - - private SessionRepository sessionRepository; - - private ContentRepository contentRepository; - - private AnswerRepository answerRepository; - - private CommentRepository commentRepository; - - private VisitedSessionRepository visitedSessionRepository; - - private UserService userService; - - private FeedbackService feedbackService; - - private ScoreCalculatorFactory scoreCalculatorFactory; - - private ConnectorClient connectorClient; - - private ImageUtils imageUtils; - - @Value("${session.guest-session.cleanup-days:0}") - private int guestSessionInactivityThresholdDays; - - @Value("${pp.logofilesize_b}") - private int uploadFileSizeByte; - - private ApplicationEventPublisher publisher; - - public SessionServiceImpl( - SessionRepository repository, - ContentRepository contentRepository, - AnswerRepository answerRepository, - CommentRepository commentRepository, - VisitedSessionRepository visitedSessionRepository, - LogEntryRepository dbLogger, - UserService userService, - FeedbackService feedbackService, - ScoreCalculatorFactory scoreCalculatorFactory, - ImageUtils imageUtils, - @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { - super(Session.class, repository, jackson2HttpMessageConverter.getObjectMapper()); - this.sessionRepository = repository; - this.contentRepository = contentRepository; - this.answerRepository = answerRepository; - this.commentRepository = commentRepository; - this.visitedSessionRepository = visitedSessionRepository; - this.dbLogger = dbLogger; - this.userService = userService; - this.feedbackService = feedbackService; - this.scoreCalculatorFactory = scoreCalculatorFactory; - this.imageUtils = imageUtils; - } - - public static class SessionNameComparator implements Comparator<Session>, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public int compare(final Session session1, final Session session2) { - return session1.getName().compareToIgnoreCase(session2.getName()); - } - } - - public static class SessionInfoNameComparator implements Comparator<SessionInfo>, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public int compare(final SessionInfo session1, final SessionInfo session2) { - return session1.getName().compareToIgnoreCase(session2.getName()); - } - } - - public static class SessionShortNameComparator implements Comparator<Session>, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public int compare(final Session session1, final Session session2) { - return session1.getShortName().compareToIgnoreCase(session2.getShortName()); - } - } - - public static class SessionInfoShortNameComparator implements Comparator<SessionInfo>, Serializable { - private static final long serialVersionUID = 1L; - - @Override - public int compare(final SessionInfo session1, final SessionInfo session2) { - return session1.getShortName().compareToIgnoreCase(session2.getShortName()); - } - } - - @Autowired(required = false) - public void setConnectorClient(ConnectorClient connectorClient) { - this.connectorClient = connectorClient; - } - - @Scheduled(fixedDelay = SESSION_INACTIVITY_CHECK_INTERVAL_MS) - public void deleteInactiveSessions() { - if (guestSessionInactivityThresholdDays > 0) { - logger.info("Delete inactive sessions."); - long unixTime = System.currentTimeMillis(); - long lastActivityBefore = unixTime - guestSessionInactivityThresholdDays * 24 * 60 * 60 * 1000L; - int totalCount[] = new int[] {0, 0, 0}; - List<Session> inactiveSessions = sessionRepository.findInactiveGuestSessionsMetadata(lastActivityBefore); - for (Session session : inactiveSessions) { - int[] count = deleteCascading(session); - totalCount[0] += count[0]; - totalCount[1] += count[1]; - totalCount[2] += count[2]; - } - - if (!inactiveSessions.isEmpty()) { - logger.info("Deleted {} inactive guest sessions.", inactiveSessions.size()); - dbLogger.log("cleanup", "type", "session", - "sessionCount", inactiveSessions.size(), - "questionCount", totalCount[0], - "answerCount", totalCount[1], - "commentCount", totalCount[2]); - } - } - } - - @Scheduled(fixedDelay = SESSION_INACTIVITY_CHECK_INTERVAL_MS) - public void deleteInactiveVisitedSessionLists() { - if (guestSessionInactivityThresholdDays > 0) { - logger.info("Delete lists of visited session for inactive users."); - long unixTime = System.currentTimeMillis(); - long lastActivityBefore = unixTime - guestSessionInactivityThresholdDays * 24 * 60 * 60 * 1000L; - visitedSessionRepository.deleteInactiveGuestVisitedSessionLists(lastActivityBefore); - } - } - - @Override - public Session join(final String keyword, final UUID socketId) { - /* Socket.IO solution */ - - Session session = null != keyword ? sessionRepository.findByKeyword(keyword) : null; - - if (null == session) { - userService.removeUserFromSessionBySocketId(socketId); - return null; - } - final User user = userService.getUser2SocketId(socketId); - - userService.addUserToSessionBySocketId(socketId, keyword); - - if (session.getCreator().equals(user.getUsername())) { - updateSessionOwnerActivity(session); - } - sessionRepository.registerAsOnlineUser(user, session); - - if (connectorClient != null && session.isCourseSession()) { - final String courseid = session.getCourseId(); - if (!connectorClient.getMembership(user.getUsername(), courseid).isMember()) { - throw new ForbiddenException("User is no course member."); - } - } - - return session; - } - - @CachePut(value = "sessions") - private Session updateSessionOwnerActivity(final Session session) { - try { - /* Do not clutter CouchDB. Only update once every 3 hours. */ - if (session.getLastOwnerActivity() > System.currentTimeMillis() - 3 * 3600000) { - return session; - } - - session.setLastOwnerActivity(System.currentTimeMillis()); - save(session); - - return session; - } catch (final UpdateConflictException e) { - logger.error("Failed to update lastOwnerActivity for session {}.", session, e); - return session; - } - } - - @Override - @PreAuthorize("isAuthenticated()") - public Session getByKey(final String keyword) { - final User user = userService.getCurrentUser(); - return this.getInternal(keyword, user); - } - - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public Session getForAdmin(final String keyword) { - return sessionRepository.findByKeyword(keyword); - } - - /* - * The "internal" suffix means it is called by internal services that have no authentication! - * TODO: Find a better way of doing this... - */ - @Override - public Session getInternal(final String keyword, final User user) { - final Session session = sessionRepository.findByKeyword(keyword); - if (session == null) { - throw new NotFoundException(); - } - if (!session.isActive()) { - if (user.hasRole(UserSessionService.Role.STUDENT)) { - throw new ForbiddenException("User is not session creator."); - } else if (user.hasRole(UserSessionService.Role.SPEAKER) && !session.isCreator(user)) { - throw new ForbiddenException("User is not session creator."); - } - } - if (connectorClient != null && session.isCourseSession()) { - final String courseid = session.getCourseId(); - if (!connectorClient.getMembership(user.getUsername(), courseid).isMember()) { - throw new ForbiddenException("User is no course member."); - } - } - return session; - } - - @Override - @PreAuthorize("isAuthenticated() and hasPermission(#sessionkey, 'session', 'owner')") - public List<Session> getUserSessions(String username) { - return sessionRepository.findByUsername(username, 0, 0); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Session> getMySessions(final int offset, final int limit) { - return sessionRepository.findByUser(userService.getCurrentUser(), offset, limit); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<SessionInfo> getPublicPoolSessionsInfo() { - return sessionRepository.findInfosForPublicPool(); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<SessionInfo> getMyPublicPoolSessionsInfo() { - return sessionRepository.findInfosForPublicPoolByUser(userService.getCurrentUser()); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<SessionInfo> getMySessionsInfo(final int offset, final int limit) { - final User user = userService.getCurrentUser(); - return sessionRepository.getMySessionsInfo(user, offset, limit); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<Session> getMyVisitedSessions(final int offset, final int limit) { - return sessionRepository.findVisitedByUsername(userService.getCurrentUser().getUsername(), offset, limit); - } - - @Override - @PreAuthorize("hasPermission('', 'motd', 'admin')") - public List<Session> getUserVisitedSessions(String username) { - return sessionRepository.findVisitedByUsername(username, 0, 0); - } - - @Override - @PreAuthorize("isAuthenticated()") - public List<SessionInfo> getMyVisitedSessionsInfo(final int offset, final int limit) { - return sessionRepository.findInfoForVisitedByUser(userService.getCurrentUser(), offset, limit); - } - - @Override - @PreAuthorize("hasPermission('', 'session', 'create')") - @Caching(evict = @CacheEvict(cacheNames = "sessions", key = "#result.keyword")) - public Session save(final Session session) { - if (connectorClient != null && session.getCourseId() != null) { - if (!connectorClient.getMembership( - userService.getCurrentUser().getUsername(), session.getCourseId()).isMember() - ) { - throw new ForbiddenException(); - } - } - handleLogo(session); - - // set some default values - ScoreOptions lpo = new ScoreOptions(); - lpo.setType("questions"); - session.setLearningProgressOptions(lpo); - - SessionFeature sf = new SessionFeature(); - sf.setLecture(true); - sf.setFeedback(true); - sf.setInterposed(true); - sf.setJitt(true); - sf.setLearningProgress(true); - sf.setPi(true); - session.setFeatures(sf); - - session.setKeyword(generateKey()); - session.setCreationTime(System.currentTimeMillis()); - session.setCreator(userService.getCurrentUser().getUsername()); - session.setActive(true); - session.setFeedbackLock(false); - - final Session result = save(session); - this.publisher.publishEvent(new NewSessionEvent(this, result)); - return result; - } - - @Override - public boolean isKeyAvailable(final String keyword) { - return getByKey(keyword) == null; - } - - @Override - public String generateKey() { - final int low = 10000000; - final int high = 100000000; - final String keyword = String - .valueOf((int) (Math.random() * (high - low) + low)); - - if (isKeyAvailable(keyword)) { - return keyword; - } - return generateKey(); - } - - @Override - public int countSessionsByCourses(final List<Course> courses) { - final List<Session> sessions = sessionRepository.findSessionsByCourses(courses); - if (sessions == null) { - return 0; - } - return sessions.size(); - } - - @Override - public int activeUsers(final String sessionkey) { - return userService.getUsersBySessionKey(sessionkey).size(); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public Session setActive(final String sessionkey, final Boolean lock) { - final Session session = sessionRepository.findByKeyword(sessionkey); - session.setActive(lock); - this.publisher.publishEvent(new StatusSessionEvent(this, session)); - sessionRepository.save(session); - - return session; - } - - @Override - @PreAuthorize("hasPermission(#session, 'owner')") - @CachePut(value = "sessions", key = "#session") - public Session update(final String sessionkey, final Session session) { - final Session existingSession = sessionRepository.findByKeyword(sessionkey); - - existingSession.setActive(session.isActive()); - existingSession.setShortName(session.getShortName()); - existingSession.setPpAuthorName(session.getPpAuthorName()); - existingSession.setPpAuthorMail(session.getPpAuthorMail()); - existingSession.setShortName(session.getShortName()); - existingSession.setPpAuthorName(session.getPpAuthorName()); - existingSession.setPpFaculty(session.getPpFaculty()); - existingSession.setName(session.getName()); - existingSession.setPpUniversity(session.getPpUniversity()); - existingSession.setPpDescription(session.getPpDescription()); - existingSession.setPpLevel(session.getPpLevel()); - existingSession.setPpLicense(session.getPpLicense()); - existingSession.setPpSubject(session.getPpSubject()); - existingSession.setFeedbackLock(session.getFeedbackLock()); - - handleLogo(session); - existingSession.setPpLogo(session.getPpLogo()); - - sessionRepository.save(existingSession); - - return session; - } - - @Override - @PreAuthorize("hasPermission('', 'motd', 'admin')") - @Caching(evict = { @CacheEvict("sessions"), @CacheEvict(cacheNames = "sessions", key = "#sessionkey.keyword") }) - public Session updateCreator(String sessionkey, String newCreator) { - final Session session = sessionRepository.findByKeyword(sessionkey); - if (session == null) { - throw new NullPointerException("Could not load session " + sessionkey + "."); - } - - session.setCreator(newCreator); - save(session); - - return save(session); - } - - /* - * The "internal" suffix means it is called by internal services that have no authentication! - * TODO: Find a better way of doing this... - */ - @Override - public Session updateInternal(final Session session, final User user) { - if (session.isCreator(user)) { - sessionRepository.save(session); - return session; - } - return null; - } - - @Override - @PreAuthorize("hasPermission(#session, 'owner')") - @CacheEvict("sessions") - public int[] deleteCascading(final Session session) { - int[] count = new int[] {0, 0, 0}; - List<String> contentIds = contentRepository.findIdsBySessionId(session.getId()); - count[2] = commentRepository.deleteBySessionId(session.getId()); - count[1] = answerRepository.deleteByContentIds(contentIds); - count[0] = contentRepository.deleteBySessionId(session.getId()); - sessionRepository.delete(session); - logger.debug("Deleted session document {} and related data.", session.getId()); - dbLogger.log("delete", "type", "session", "id", session.getId()); - - this.publisher.publishEvent(new DeleteSessionEvent(this, session)); - - return count; - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'read')") - public ScoreStatistics getLearningProgress(final String sessionkey, final String type, final String questionVariant) { - final Session session = sessionRepository.findByKeyword(sessionkey); - ScoreCalculator scoreCalculator = scoreCalculatorFactory.create(type, questionVariant); - return scoreCalculator.getCourseProgress(session); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'read')") - public ScoreStatistics getMyLearningProgress(final String sessionkey, final String type, final String questionVariant) { - final Session session = sessionRepository.findByKeyword(sessionkey); - final User user = userService.getCurrentUser(); - ScoreCalculator scoreCalculator = scoreCalculatorFactory.create(type, questionVariant); - return scoreCalculator.getMyProgress(session, user); - } - - @Override - @PreAuthorize("hasPermission('', 'session', 'create')") - public SessionInfo importSession(ImportExportSession importSession) { - final User user = userService.getCurrentUser(); - final SessionInfo info = sessionRepository.importSession(user, importSession); - if (info == null) { - throw new NullPointerException("Could not import session."); - } - return info; - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public ImportExportSession exportSession(String sessionkey, Boolean withAnswerStatistics, Boolean withFeedbackQuestions) { - return sessionRepository.exportSession(sessionkey, withAnswerStatistics, withFeedbackQuestions); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public SessionInfo copySessionToPublicPool(String sessionkey, de.thm.arsnova.entities.transport.ImportExportSession.PublicPool pp) { - ImportExportSession temp = sessionRepository.exportSession(sessionkey, false, false); - temp.getSession().setPublicPool(pp); - temp.getSession().setSessionType("public_pool"); - final User user = userService.getCurrentUser(); - return sessionRepository.importSession(user, temp); - } - - @Override - public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { - this.publisher = publisher; - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'read')") - public SessionFeature getFeatures(String sessionkey) { - return sessionRepository.findByKeyword(sessionkey).getFeatures(); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public SessionFeature updateFeatures(String sessionkey, SessionFeature features) { - final Session session = sessionRepository.findByKeyword(sessionkey); - final User user = userService.getCurrentUser(); - session.setFeatures(features); - this.publisher.publishEvent(new FeatureChangeEvent(this, session)); - sessionRepository.save(session); - - return session.getFeatures(); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public boolean lockFeedbackInput(String sessionkey, Boolean lock) { - final Session session = sessionRepository.findByKeyword(sessionkey); - final User user = userService.getCurrentUser(); - if (!lock) { - feedbackService.cleanFeedbackVotesBySessionKey(sessionkey, 0); - } - - session.setFeedbackLock(lock); - this.publisher.publishEvent(new LockFeedbackEvent(this, session)); - sessionRepository.save(session); - - return session.getFeedbackLock(); - } - - @Override - @PreAuthorize("hasPermission(#sessionkey, 'session', 'owner')") - public boolean flipFlashcards(String sessionkey, Boolean flip) { - final Session session = sessionRepository.findByKeyword(sessionkey); - final User user = userService.getCurrentUser(); - session.setFlipFlashcards(flip); - this.publisher.publishEvent(new FlipFlashcardsEvent(this, session)); - sessionRepository.save(session); - - return session.getFlipFlashcards(); - } - - private void handleLogo(Session session) { - if (session.getPpLogo() != null) { - if (session.getPpLogo().startsWith("http")) { - final String base64ImageString = imageUtils.encodeImageToString(session.getPpLogo()); - if (base64ImageString == null) { - throw new BadRequestException("Could not encode image."); - } - session.setPpLogo(base64ImageString); - } - - // base64 adds offset to filesize, formula taken from: http://en.wikipedia.org/wiki/Base64#MIME - final int fileSize = (int) ((session.getPpLogo().length() - 814) / 1.37); - if (fileSize > uploadFileSizeByte) { - throw new PayloadTooLargeException("Could not save file. File is too large with " + fileSize + " Byte."); - } - } - } -} diff --git a/src/main/java/de/thm/arsnova/services/UserService.java b/src/main/java/de/thm/arsnova/services/UserService.java deleted file mode 100644 index 0b037274fde7271d86d050fb85e74c14a3572347..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/UserService.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.DbUser; -import de.thm.arsnova.entities.User; - -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -/** - * The functionality the user service should provide. - */ -public interface UserService { - User getCurrentUser(); - - boolean isAdmin(String username); - - boolean isBannedFromLogin(String addr); - - void increaseFailedLoginCount(String addr); - - User getUser2SocketId(UUID socketId); - - void putUser2SocketId(UUID socketId, User user); - - void removeUser2SocketId(UUID socketId); - - Set<Map.Entry<UUID, User>> socketId2User(); - - boolean isUserInSession(User user, String keyword); - - Set<User> getUsersBySessionKey(String keyword); - - String getSessionByUsername(String username); - - void addUserToSessionBySocketId(UUID socketId, String keyword); - - void removeUserFromSessionBySocketId(UUID socketId); - - void removeUserFromMaps(User user); - - int loggedInUsers(); - - DbUser getByUsername(String username); - - DbUser create(String username, String password); - - DbUser update(DbUser dbUser); - - DbUser deleteByUsername(String username); - - void initiatePasswordReset(String username); - - boolean resetPassword(DbUser dbUser, String key, String password); -} diff --git a/src/main/java/de/thm/arsnova/services/UserServiceImpl.java b/src/main/java/de/thm/arsnova/services/UserServiceImpl.java deleted file mode 100644 index 1d44b3f8848ca282e75bbfefffac50cf660491df..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/UserServiceImpl.java +++ /dev/null @@ -1,552 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import com.codahale.metrics.annotation.Gauge; -import de.thm.arsnova.entities.DbUser; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.exceptions.BadRequestException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.persistance.UserRepository; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.commons.lang.StringUtils; -import org.pac4j.oauth.profile.facebook.FacebookProfile; -import org.pac4j.oauth.profile.google2.Google2Profile; -import org.pac4j.oauth.profile.twitter.TwitterProfile; -import org.pac4j.springframework.security.authentication.Pac4jAuthenticationToken; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.mail.MailException; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.mail.javamail.MimeMessageHelper; -import org.springframework.scheduling.annotation.Scheduled; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; -import org.springframework.security.cas.authentication.CasAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; -import org.springframework.security.crypto.keygen.BytesKeyGenerator; -import org.springframework.security.crypto.keygen.KeyGenerators; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Isolation; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.util.UriUtils; -import org.stagemonitor.core.metrics.MonitorGauges; - -import javax.annotation.PreDestroy; -import javax.mail.MessagingException; -import javax.mail.internet.MimeMessage; -import java.io.UnsupportedEncodingException; -import java.text.MessageFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.regex.Pattern; - -/** - * Performs all user related operations. - */ -@Service -@MonitorGauges -public class UserServiceImpl implements UserService { - - private static final int LOGIN_TRY_RESET_DELAY_MS = 30 * 1000; - - private static final int LOGIN_BAN_RESET_DELAY_MS = 2 * 60 * 1000; - - private static final int REPEATED_PASSWORD_RESET_DELAY_MS = 3 * 60 * 1000; - - private static final int PASSWORD_RESET_KEY_DURABILITY_MS = 2 * 60 * 60 * 1000; - - private static final long ACTIVATION_KEY_CHECK_INTERVAL_MS = 30 * 60 * 1000L; - private static final long ACTIVATION_KEY_DURABILITY_MS = 6 * 60 * 60 * 1000L; - - private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class); - - private static final ConcurrentHashMap<UUID, User> socketid2user = new ConcurrentHashMap<>(); - - /* used for Socket.IO online check solution (new) */ - private static final ConcurrentHashMap<User, String> user2session = new ConcurrentHashMap<>(); - - private UserRepository userRepository; - - private JavaMailSender mailSender; - - @Value("${root-url}") - private String rootUrl; - - @Value("${customization.path}") - private String customizationPath; - - @Value("${security.user-db.allowed-email-domains}") - private String allowedEmailDomains; - - @Value("${security.user-db.activation-path}") - private String activationPath; - - @Value("${security.user-db.reset-password-path}") - private String resetPasswordPath; - - @Value("${mail.sender.address}") - private String mailSenderAddress; - - @Value("${mail.sender.name}") - private String mailSenderName; - - @Value("${security.user-db.registration-mail.subject}") - private String regMailSubject; - - @Value("${security.user-db.registration-mail.body}") - private String regMailBody; - - @Value("${security.user-db.reset-password-mail.subject}") - private String resetPasswordMailSubject; - - @Value("${security.user-db.reset-password-mail.body}") - private String resetPasswordMailBody; - - @Value("${security.authentication.login-try-limit}") - private int loginTryLimit; - - @Value("${security.admin-accounts}") - private String[] adminAccounts; - - private Pattern mailPattern; - private BytesKeyGenerator keygen; - private BCryptPasswordEncoder encoder; - private ConcurrentHashMap<String, Byte> loginTries; - private Set<String> loginBans; - - { - loginTries = new ConcurrentHashMap<>(); - loginBans = Collections.synchronizedSet(new HashSet<String>()); - } - - public UserServiceImpl(UserRepository repository, JavaMailSender mailSender) { - this.userRepository = repository; - this.mailSender = mailSender; - } - - @Scheduled(fixedDelay = LOGIN_TRY_RESET_DELAY_MS) - public void resetLoginTries() { - if (!loginTries.isEmpty()) { - logger.debug("Reset failed login counters."); - loginTries.clear(); - } - } - - @Scheduled(fixedDelay = LOGIN_BAN_RESET_DELAY_MS) - public void resetLoginBans() { - if (!loginBans.isEmpty()) { - logger.info("Reset temporary login bans."); - loginBans.clear(); - } - } - - @Scheduled(fixedDelay = ACTIVATION_KEY_CHECK_INTERVAL_MS) - public void deleteInactiveUsers() { - logger.info("Delete inactive users."); - long unixTime = System.currentTimeMillis(); - long lastActivityBefore = unixTime - ACTIVATION_KEY_DURABILITY_MS; - userRepository.deleteInactiveUsers(lastActivityBefore); - } - - @Override - public User getCurrentUser() { - final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || authentication.getPrincipal() == null) { - return null; - } - - User user = null; - - if (authentication instanceof Pac4jAuthenticationToken) { - user = getOAuthUser(authentication); - } else if (authentication instanceof CasAuthenticationToken) { - final CasAuthenticationToken token = (CasAuthenticationToken) authentication; - user = new User(token.getAssertion().getPrincipal()); - } else if (authentication instanceof AnonymousAuthenticationToken) { - final AnonymousAuthenticationToken token = (AnonymousAuthenticationToken) authentication; - user = new User(token); - } else if (authentication instanceof UsernamePasswordAuthenticationToken) { - final UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication; - user = new User(token); - if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_GUEST"))) { - user.setType(User.GUEST); - } else if (authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_DB_USER"))) { - user.setType(User.ARSNOVA); - } - } - - if (user == null || "anonymous".equals(user.getUsername())) { - throw new UnauthorizedException(); - } - - user.setAdmin(Arrays.asList(adminAccounts).contains(user.getUsername())); - - return user; - } - - private User getOAuthUser(final Authentication authentication) { - User user = null; - final Pac4jAuthenticationToken token = (Pac4jAuthenticationToken) authentication; - if (token.getProfile() instanceof Google2Profile) { - final Google2Profile profile = (Google2Profile) token.getProfile(); - user = new User(profile); - } else if (token.getProfile() instanceof TwitterProfile) { - final TwitterProfile profile = (TwitterProfile) token.getProfile(); - user = new User(profile); - } else if (token.getProfile() instanceof FacebookProfile) { - final FacebookProfile profile = (FacebookProfile) token.getProfile(); - user = new User(profile); - } - return user; - } - - @Override - public boolean isAdmin(final String username) { - return Arrays.asList(adminAccounts).contains(username); - } - - @Override - public boolean isBannedFromLogin(String addr) { - return loginBans.contains(addr); - } - - @Override - public void increaseFailedLoginCount(String addr) { - Byte tries = loginTries.get(addr); - if (null == tries) { - tries = 0; - } - if (tries < loginTryLimit) { - loginTries.put(addr, ++tries); - if (loginTryLimit == tries) { - logger.info("Temporarily banned {} from login.", addr); - loginBans.add(addr); - } - } - } - - @Override - public User getUser2SocketId(final UUID socketId) { - return socketid2user.get(socketId); - } - - @Override - public void putUser2SocketId(final UUID socketId, final User user) { - socketid2user.put(socketId, user); - } - - @Override - public Set<Map.Entry<UUID, User>> socketId2User() { - return socketid2user.entrySet(); - } - - @Override - public void removeUser2SocketId(final UUID socketId) { - socketid2user.remove(socketId); - } - - @Override - public boolean isUserInSession(final User user, final String keyword) { - if (keyword == null) { - return false; - } - String session = user2session.get(user); - - return session != null && keyword.equals(session); - } - - @Override - public Set<User> getUsersBySessionKey(final String keyword) { - final Set<User> result = new HashSet<>(); - for (final Entry<User, String> e : user2session.entrySet()) { - if (e.getValue().equals(keyword)) { - result.add(e.getKey()); - } - } - - return result; - } - - @Override - @Transactional(isolation = Isolation.READ_COMMITTED) - public void addUserToSessionBySocketId(final UUID socketId, final String keyword) { - final User user = socketid2user.get(socketId); - user2session.put(user, keyword); - } - - @Override - @Transactional(isolation = Isolation.READ_COMMITTED) - public void removeUserFromSessionBySocketId(final UUID socketId) { - final User user = socketid2user.get(socketId); - if (null == user) { - logger.warn("No user exists for socket {}.", socketId); - - return; - } - user2session.remove(user); - } - - @Override - public String getSessionByUsername(final String username) { - for (final Entry<User, String> entry : user2session.entrySet()) { - if (entry.getKey().getUsername().equals(username)) { - return entry.getValue(); - } - } - - return null; - } - - @PreDestroy - public void destroy() { - logger.error("Destroy UserServiceImpl"); - } - - @Override - public void removeUserFromMaps(final User user) { - if (user != null) { - user2session.remove(user); - } - } - - @Override - @Gauge - public int loggedInUsers() { - return user2session.size(); - } - - @Override - public DbUser getByUsername(String username) { - return userRepository.findByUsername(username.toLowerCase()); - } - - @Override - public DbUser create(String username, String password) { - String lcUsername = username.toLowerCase(); - - if (null == keygen) { - keygen = KeyGenerators.secureRandom(32); - } - - if (null == mailPattern) { - parseMailAddressPattern(); - } - - if (null == mailPattern || !mailPattern.matcher(lcUsername).matches()) { - logger.info("User registration failed. {} does not match pattern.", lcUsername); - - return null; - } - - if (null != userRepository.findByUsername(lcUsername)) { - logger.info("User registration failed. {} already exists.", lcUsername); - - return null; - } - - DbUser dbUser = new DbUser(); - dbUser.setUsername(lcUsername); - dbUser.setPassword(encodePassword(password)); - dbUser.setActivationKey(RandomStringUtils.randomAlphanumeric(32)); - dbUser.setCreation(System.currentTimeMillis()); - - DbUser result = userRepository.save(dbUser); - if (null != result) { - sendActivationEmail(result); - } else { - logger.error("User registration failed. {} could not be created.", lcUsername); - } - - return result; - } - - private String encodePassword(String password) { - if (null == encoder) { - encoder = new BCryptPasswordEncoder(12); - } - - return encoder.encode(password); - } - - private void sendActivationEmail(DbUser dbUser) { - String activationUrl; - try { - activationUrl = MessageFormat.format( - "{0}{1}/{2}?action=activate&username={3}&key={4}", - rootUrl, - customizationPath, - activationPath, - UriUtils.encodeQueryParam(dbUser.getUsername(), "UTF-8"), - dbUser.getActivationKey() - ); - } catch (UnsupportedEncodingException e) { - logger.error("Sending of activation mail failed.", e); - - return; - } - - sendEmail(dbUser, regMailSubject, MessageFormat.format(regMailBody, activationUrl)); - } - - private void parseMailAddressPattern() { - /* TODO: Add Unicode support */ - - List<String> domainList = Arrays.asList(allowedEmailDomains.split(",")); - - if (!domainList.isEmpty()) { - List<String> patterns = new ArrayList<>(); - if (domainList.contains("*")) { - patterns.add("([a-z0-9-]+\\.)+[a-z0-9-]+"); - } else { - Pattern patternPattern = Pattern.compile("[a-z0-9.*-]+", Pattern.CASE_INSENSITIVE); - for (String patternStr : domainList) { - if (patternPattern.matcher(patternStr).matches()) { - patterns.add(patternStr.replaceAll("[.]", "[.]").replaceAll("[*]", "[a-z0-9-]+?")); - } - } - } - - mailPattern = Pattern.compile("[a-z0-9._-]+?@(" + StringUtils.join(patterns, "|") + ")", Pattern.CASE_INSENSITIVE); - logger.info("Allowed e-mail addresses (pattern) for registration: '{}'.", mailPattern.pattern()); - } - } - - @Override - public DbUser update(DbUser dbUser) { - if (null != dbUser.getId()) { - return userRepository.save(dbUser); - } - - return null; - } - - @Override - public DbUser deleteByUsername(String username) { - User user = getCurrentUser(); - if (!user.getUsername().equals(username.toLowerCase()) - && !SecurityContextHolder.getContext().getAuthentication().getAuthorities() - .contains(new SimpleGrantedAuthority("ROLE_ADMIN"))) { - throw new UnauthorizedException(); - } - - DbUser dbUser = getByUsername(username); - if (null == dbUser) { - throw new NotFoundException(); - } - - userRepository.delete(dbUser); - - return dbUser; - } - - @Override - public void initiatePasswordReset(String username) { - DbUser dbUser = getByUsername(username); - if (null == dbUser) { - logger.info("Password reset failed. User {} does not exist.", username); - - throw new NotFoundException(); - } - if (System.currentTimeMillis() < dbUser.getPasswordResetTime() + REPEATED_PASSWORD_RESET_DELAY_MS) { - logger.info("Password reset failed. The reset delay for User {} is still active.", username); - - throw new BadRequestException(); - } - - dbUser.setPasswordResetKey(RandomStringUtils.randomAlphanumeric(32)); - dbUser.setPasswordResetTime(System.currentTimeMillis()); - - if (null == userRepository.save(dbUser)) { - logger.error("Password reset failed. {} could not be updated.", username); - } - - String resetPasswordUrl; - try { - resetPasswordUrl = MessageFormat.format( - "{0}{1}/{2}?action=resetpassword&username={3}&key={4}", - rootUrl, - customizationPath, - resetPasswordPath, - UriUtils.encodeQueryParam(dbUser.getUsername(), "UTF-8"), - dbUser.getPasswordResetKey() - ); - } catch (UnsupportedEncodingException e) { - logger.error("Sending of password reset mail failed.", e); - - return; - } - - sendEmail(dbUser, resetPasswordMailSubject, MessageFormat.format(resetPasswordMailBody, resetPasswordUrl)); - } - - @Override - public boolean resetPassword(DbUser dbUser, String key, String password) { - if (null == key || "".equals(key) || !key.equals(dbUser.getPasswordResetKey())) { - logger.info("Password reset failed. Invalid key provided for User {}.", dbUser.getUsername()); - - return false; - } - if (System.currentTimeMillis() > dbUser.getPasswordResetTime() + PASSWORD_RESET_KEY_DURABILITY_MS) { - logger.info("Password reset failed. Key provided for User {} is no longer valid.", dbUser.getUsername()); - - dbUser.setPasswordResetKey(null); - dbUser.setPasswordResetTime(0); - update(dbUser); - - return false; - } - - dbUser.setPassword(encodePassword(password)); - dbUser.setPasswordResetKey(null); - if (null == update(dbUser)) { - logger.error("Password reset failed. {} could not be updated.", dbUser.getUsername()); - } - - return true; - } - - private void sendEmail(DbUser dbUser, String subject, String body) { - MimeMessage msg = mailSender.createMimeMessage(); - MimeMessageHelper helper = new MimeMessageHelper(msg, "UTF-8"); - try { - helper.setFrom(mailSenderName + "<" + mailSenderAddress + ">"); - helper.setTo(dbUser.getUsername()); - helper.setSubject(subject); - helper.setText(body); - - logger.info("Sending mail \"{}\" from \"{}\" to \"{}\"", subject, msg.getFrom(), dbUser.getUsername()); - mailSender.send(msg); - } catch (MailException | MessagingException e) { - logger.warn("Mail \"{}\" could not be sent.", subject, e); - } - } -} diff --git a/src/main/java/de/thm/arsnova/services/UserSessionService.java b/src/main/java/de/thm/arsnova/services/UserSessionService.java deleted file mode 100644 index 79ab4f84a8c7865a532153748dabd24addfcc91c..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/UserSessionService.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; - -import java.util.UUID; - -/** - * The functionality the user-session service should provide. - */ -public interface UserSessionService { - - enum Role { - STUDENT, - SPEAKER - } - - void setUser(User user); - User getUser(); - - void setSession(Session session); - Session getSession(); - - void setSocketId(UUID socketId); - UUID getSocketId(); - - void setRole(Role role); - Role getRole(); - - boolean inSession(); - boolean isAuthenticated(); -} diff --git a/src/main/java/de/thm/arsnova/services/UserSessionServiceImpl.java b/src/main/java/de/thm/arsnova/services/UserSessionServiceImpl.java deleted file mode 100644 index da33beaadd8c0146a36558500471d9e91eeb2f70..0000000000000000000000000000000000000000 --- a/src/main/java/de/thm/arsnova/services/UserSessionServiceImpl.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.entities.User; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; -import org.springframework.stereotype.Component; - -import java.io.Serializable; -import java.util.UUID; - -/** - * This service is used to assign and check for a specific role. - */ -@Component -@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS) -public class UserSessionServiceImpl implements UserSessionService, Serializable { - private static final long serialVersionUID = 1L; - - private User user; - private Session session; - private UUID socketId; - private Role role; - - @Override - public void setUser(final User u) { - user = u; - user.setRole(role); - } - - @Override - public User getUser() { - return user; - } - - @Override - public void setSession(final Session s) { - session = s; - } - - @Override - public Session getSession() { - return session; - } - - @Override - public void setSocketId(final UUID sId) { - socketId = sId; - } - - @Override - public UUID getSocketId() { - return socketId; - } - - @Override - public boolean inSession() { - return isAuthenticated() - && getSession() != null; - } - - @Override - public boolean isAuthenticated() { - return getUser() != null - && getRole() != null; - } - - @Override - public void setRole(final Role r) { - role = r; - if (user != null) { - user.setRole(role); - } - } - - @Override - public Role getRole() { - return role; - } -} diff --git a/src/main/java/de/thm/arsnova/util/ImageUtils.java b/src/main/java/de/thm/arsnova/util/ImageUtils.java index 9b79795b547bd0e1c916c3c1cf102141dcc91b17..5ac24d889fb176f84ebfa3114582e884889f2596 100644 --- a/src/main/java/de/thm/arsnova/util/ImageUtils.java +++ b/src/main/java/de/thm/arsnova/util/ImageUtils.java @@ -17,7 +17,7 @@ */ package de.thm.arsnova.util; -import de.thm.arsnova.entities.Answer; +import de.thm.arsnova.model.migration.v2.Answer; import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/de/thm/arsnova/web/CorsFilter.java b/src/main/java/de/thm/arsnova/web/CorsFilter.java index e77520080568b5bab254c7514a623bf217f2e4a0..6c2c9158144c548720b94b84c6487d6f7e3c229a 100644 --- a/src/main/java/de/thm/arsnova/web/CorsFilter.java +++ b/src/main/java/de/thm/arsnova/web/CorsFilter.java @@ -25,6 +25,7 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; public class CorsFilter extends org.springframework.web.filter.CorsFilter { + private static final String TOKEN_HEADER_NAME = "Arsnova-Auth-Token"; private final Logger logger = LoggerFactory.getLogger(CorsFilter.class); public CorsFilter(List<String> origins) { @@ -43,9 +44,11 @@ public class CorsFilter extends org.springframework.web.filter.CorsFilter { config.addAllowedHeader("Accept"); config.addAllowedHeader("Content-Type"); config.addAllowedHeader("X-Requested-With"); + config.addAllowedHeader(TOKEN_HEADER_NAME); config.addAllowedMethod("GET"); config.addAllowedMethod("POST"); config.addAllowedMethod("PUT"); + config.addAllowedMethod("PATCH"); config.addAllowedMethod("DELETE"); config.setAllowCredentials(true); source.registerCorsConfiguration("/**", config); diff --git a/src/main/java/de/thm/arsnova/web/InternalEntityAspect.java b/src/main/java/de/thm/arsnova/web/InternalEntityAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..3a59bedc35335f711e3dd95180197007895522fb --- /dev/null +++ b/src/main/java/de/thm/arsnova/web/InternalEntityAspect.java @@ -0,0 +1,26 @@ +package de.thm.arsnova.web; + +import de.thm.arsnova.model.Entity; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This aspect ensures that entities marked for internal use are not serialized for the public API. + * + * @author Daniel Gerhardt + */ +@Aspect +public class InternalEntityAspect { + private static final Logger logger = LoggerFactory.getLogger(InternalEntityAspect.class); + + @AfterReturning(pointcut = "execution(de.thm.arsnova.model.Entity+ de.thm.arsnova.controller.*.*(..))", returning = "entity") + public void prohibitInternalEntitySerialization(final Entity entity) { + logger.debug("Executing InternalEntityAspect for entity: {}", entity); + + if (entity.isInternal()) { + throw new SecurityException("Serialization of internal entities is not allowed."); + } + } +} diff --git a/src/main/java/de/thm/arsnova/web/MaintenanceModeFilter.java b/src/main/java/de/thm/arsnova/web/MaintenanceModeFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..7d2e3bc6f1768a628f79bd8f537db61b590b4cb5 --- /dev/null +++ b/src/main/java/de/thm/arsnova/web/MaintenanceModeFilter.java @@ -0,0 +1,51 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.web; + +import de.thm.arsnova.service.StatusService; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Blocks API requests while maintenance reasons are active. + * + * @author Daniel Gerhardt + */ +@Component +public class MaintenanceModeFilter extends OncePerRequestFilter { + private StatusService statusService; + + public MaintenanceModeFilter(final StatusService statusService) { + this.statusService = statusService; + } + + @Override + protected void doFilterInternal(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse, final FilterChain filterChain) throws ServletException, IOException { + if (statusService.isMaintenanceActive()) { + httpServletResponse.setStatus(503); + return; + } + filterChain.doFilter(httpServletRequest, httpServletResponse); + } +} diff --git a/src/main/java/de/thm/arsnova/web/PathApiVersionContentNegotiationStrategy.java b/src/main/java/de/thm/arsnova/web/PathApiVersionContentNegotiationStrategy.java new file mode 100644 index 0000000000000000000000000000000000000000..3ded812830af479caf5ba08548f7e99ed221dbc1 --- /dev/null +++ b/src/main/java/de/thm/arsnova/web/PathApiVersionContentNegotiationStrategy.java @@ -0,0 +1,58 @@ +package de.thm.arsnova.web; + +import de.thm.arsnova.config.AppConfig; +import de.thm.arsnova.controller.AbstractEntityController; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.context.request.NativeWebRequest; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * This {@link ContentNegotiationStrategy} selects the media type based on request path. It allows to set the correct + * media type for old clients which do not set the 'Accept' header. + * + * @author Daniel Gerhardt + */ +public class PathApiVersionContentNegotiationStrategy implements ContentNegotiationStrategy { + private static final Logger logger = LoggerFactory.getLogger(PathApiVersionContentNegotiationStrategy.class); + + private MediaType fallback; + private MediaType empty = MediaType.valueOf(AbstractEntityController.MEDIATYPE_EMPTY); + + public PathApiVersionContentNegotiationStrategy(MediaType fallback) { + this.fallback = fallback; + } + + @Override + public List<MediaType> resolveMediaTypes(final NativeWebRequest webRequest) + throws HttpMediaTypeNotAcceptableException { + final HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + final List<MediaType> mediaTypes = new ArrayList<>(); + if (servletRequest.getServletPath().startsWith("/v2/")) { + logger.trace("Negotiating content based on path for API v2"); + mediaTypes.add(AppConfig.API_V2_MEDIA_TYPE); + mediaTypes.add(MediaType.TEXT_PLAIN); + } else { + logger.trace("Content negotiation falling back to {}", fallback); + if (servletRequest.getHeader(HttpHeaders.ACCEPT) == null + && Arrays.asList("POST", "PUT", "PATCH").contains(servletRequest.getMethod())) { + /* This allows AbstractEntityController to send an empty response if no Accept header is set */ + logger.debug("No Accept header present for {} request. Entity will not be sent in response", + servletRequest.getMethod()); + mediaTypes.add(empty); + } else { + mediaTypes.add(fallback); + } + } + + return mediaTypes; + } +} diff --git a/src/main/java/de/thm/arsnova/aop/RangeAspect.java b/src/main/java/de/thm/arsnova/web/RangeAspect.java similarity index 97% rename from src/main/java/de/thm/arsnova/aop/RangeAspect.java rename to src/main/java/de/thm/arsnova/web/RangeAspect.java index 522d568e7f8af9b7ba44064b193562c81a87b920..9959fab9cd884c9ee25973cb96ca4d10cdb6d757 100644 --- a/src/main/java/de/thm/arsnova/aop/RangeAspect.java +++ b/src/main/java/de/thm/arsnova/web/RangeAspect.java @@ -15,11 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.aop; +package de.thm.arsnova.web; import de.thm.arsnova.util.PaginationListDecorator; import de.thm.arsnova.controller.PaginationController; -import de.thm.arsnova.services.ResponseProviderService; +import de.thm.arsnova.service.ResponseProviderService; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; diff --git a/src/main/java/de/thm/arsnova/web/ResponseInterceptorHandler.java b/src/main/java/de/thm/arsnova/web/ResponseInterceptorHandler.java index 3edfc47be5208171e9b10228f57c25a2fb014990..da4afef5e2493d97c8a14876111faaba0f736ebe 100644 --- a/src/main/java/de/thm/arsnova/web/ResponseInterceptorHandler.java +++ b/src/main/java/de/thm/arsnova/web/ResponseInterceptorHandler.java @@ -17,7 +17,7 @@ */ package de.thm.arsnova.web; -import de.thm.arsnova.services.ResponseProviderService; +import de.thm.arsnova.service.ResponseProviderService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; diff --git a/src/main/java/de/thm/arsnova/web/V2ContentTypeOverrideFilter.java b/src/main/java/de/thm/arsnova/web/V2ContentTypeOverrideFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..b31f747b3693c331f32a4c3a678bcf8db452a1a3 --- /dev/null +++ b/src/main/java/de/thm/arsnova/web/V2ContentTypeOverrideFilter.java @@ -0,0 +1,73 @@ +package de.thm.arsnova.web; + +import de.thm.arsnova.config.AppConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; + +/** + * This {@link javax.servlet.Filter} overrides the Content-Type for JSON data sent by the client with the ARSnova API v2 + * JSON Content-Type. + * + * @author Daniel Gerhardt + */ +@Component +public class V2ContentTypeOverrideFilter extends OncePerRequestFilter { + private static final Logger logger = LoggerFactory.getLogger(V2ContentTypeOverrideFilter.class); + private final List<String> contentTypeHeaders; + + { + contentTypeHeaders = new ArrayList<>(); + contentTypeHeaders.add(AppConfig.API_V2_MEDIA_TYPE_VALUE); + } + + @Override + protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, + final FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest requestWrapper = new HttpServletRequestWrapper(request) { + @Override + public String getHeader(final String name) { + String header = super.getHeader(name); + if (header != null && HttpHeaders.CONTENT_TYPE.equals(name) + && MediaType.APPLICATION_JSON.includes(MediaType.valueOf(header))) { + logger.debug("Overriding {} header: {}", HttpHeaders.CONTENT_TYPE, AppConfig.API_V2_MEDIA_TYPE_VALUE); + return AppConfig.API_V2_MEDIA_TYPE_VALUE; + } + + return header; + } + + @Override + public String getContentType() { + return getHeader(HttpHeaders.CONTENT_TYPE); + } + + @Override + public Enumeration<String> getHeaders(final String name) { + String firstHeader = super.getHeaders(name).nextElement(); + if (firstHeader != null && HttpHeaders.CONTENT_TYPE.equals(name) + && MediaType.APPLICATION_JSON.includes(MediaType.valueOf(firstHeader))) { + logger.debug("Overriding {} header: {}", HttpHeaders.CONTENT_TYPE, AppConfig.API_V2_MEDIA_TYPE_VALUE); + return Collections.enumeration(contentTypeHeaders); + } + + return super.getHeaders(name); + } + }; + filterChain.doFilter(requestWrapper, response); + } +} diff --git a/src/main/java/de/thm/arsnova/exceptions/BadRequestException.java b/src/main/java/de/thm/arsnova/web/exceptions/BadRequestException.java similarity index 91% rename from src/main/java/de/thm/arsnova/exceptions/BadRequestException.java rename to src/main/java/de/thm/arsnova/web/exceptions/BadRequestException.java index b7e80b833cb0f6053e9d0c1f77fe63813272cdd9..7f417aca25d9f61a91e4254365b7e6948ba0d4f6 100644 --- a/src/main/java/de/thm/arsnova/exceptions/BadRequestException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/BadRequestException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * Bad Request means status code 400. diff --git a/src/main/java/de/thm/arsnova/exceptions/ForbiddenException.java b/src/main/java/de/thm/arsnova/web/exceptions/ForbiddenException.java similarity index 91% rename from src/main/java/de/thm/arsnova/exceptions/ForbiddenException.java rename to src/main/java/de/thm/arsnova/web/exceptions/ForbiddenException.java index 05b21dab60ba76f181a0343c4420d7129814e966..aefe91f3383d8e85e78c40bce1126ad221b23b72 100644 --- a/src/main/java/de/thm/arsnova/exceptions/ForbiddenException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/ForbiddenException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * Forbidden means status code 403. diff --git a/src/main/java/de/thm/arsnova/exceptions/NoContentException.java b/src/main/java/de/thm/arsnova/web/exceptions/NoContentException.java similarity index 91% rename from src/main/java/de/thm/arsnova/exceptions/NoContentException.java rename to src/main/java/de/thm/arsnova/web/exceptions/NoContentException.java index f3f7e81649b77d04bf4431b45aeebd857b71eff8..0177ac4acd3a1b02f3f4bda77b98adc3f809e3f1 100644 --- a/src/main/java/de/thm/arsnova/exceptions/NoContentException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/NoContentException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * No Content means status code 204. diff --git a/src/main/java/de/thm/arsnova/exceptions/NotFoundException.java b/src/main/java/de/thm/arsnova/web/exceptions/NotFoundException.java similarity index 91% rename from src/main/java/de/thm/arsnova/exceptions/NotFoundException.java rename to src/main/java/de/thm/arsnova/web/exceptions/NotFoundException.java index a0a12b25add162e74557c33502cea8dcc3697b90..faa58e11db4da59ad02496d8bcbfde169f6eb2a3 100644 --- a/src/main/java/de/thm/arsnova/exceptions/NotFoundException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/NotFoundException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * Not Found means status code 404. diff --git a/src/main/java/de/thm/arsnova/exceptions/NotImplementedException.java b/src/main/java/de/thm/arsnova/web/exceptions/NotImplementedException.java similarity index 91% rename from src/main/java/de/thm/arsnova/exceptions/NotImplementedException.java rename to src/main/java/de/thm/arsnova/web/exceptions/NotImplementedException.java index 93f7ed795537fbd2a263059c7add9bc3c3b1f3b7..7941effb45549ff2563e8473152f59dfb30d66de 100644 --- a/src/main/java/de/thm/arsnova/exceptions/NotImplementedException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/NotImplementedException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * Not Implemented means status code 501. diff --git a/src/main/java/de/thm/arsnova/exceptions/PayloadTooLargeException.java b/src/main/java/de/thm/arsnova/web/exceptions/PayloadTooLargeException.java similarity index 91% rename from src/main/java/de/thm/arsnova/exceptions/PayloadTooLargeException.java rename to src/main/java/de/thm/arsnova/web/exceptions/PayloadTooLargeException.java index 9233b58f25fd465979943a782c9a9d4fced27e25..5dff0716a522ef6b77ef9d6a7ea9aee3813f230b 100644 --- a/src/main/java/de/thm/arsnova/exceptions/PayloadTooLargeException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/PayloadTooLargeException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * Payload Too Large means status code 413. diff --git a/src/main/java/de/thm/arsnova/exceptions/PreconditionFailedException.java b/src/main/java/de/thm/arsnova/web/exceptions/PreconditionFailedException.java similarity index 92% rename from src/main/java/de/thm/arsnova/exceptions/PreconditionFailedException.java rename to src/main/java/de/thm/arsnova/web/exceptions/PreconditionFailedException.java index e14d30c7edd66c8bc7dbd669144eda48a4a64d32..31c1de8b2dbb3d370f1ce0819a7b269cc5c5512d 100644 --- a/src/main/java/de/thm/arsnova/exceptions/PreconditionFailedException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/PreconditionFailedException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * Precondition Failed means status code 412. diff --git a/src/main/java/de/thm/arsnova/exceptions/UnauthorizedException.java b/src/main/java/de/thm/arsnova/web/exceptions/UnauthorizedException.java similarity index 91% rename from src/main/java/de/thm/arsnova/exceptions/UnauthorizedException.java rename to src/main/java/de/thm/arsnova/web/exceptions/UnauthorizedException.java index f6a91249b2681fea1bd816b0b9dadde1ab86748a..e889a0f42c21c2b1bc019429250e16be6a645bf4 100644 --- a/src/main/java/de/thm/arsnova/exceptions/UnauthorizedException.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/UnauthorizedException.java @@ -1,4 +1,4 @@ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; /** * Unauthorized means status code 401. diff --git a/src/main/java/de/thm/arsnova/exceptions/package-info.java b/src/main/java/de/thm/arsnova/web/exceptions/package-info.java similarity index 62% rename from src/main/java/de/thm/arsnova/exceptions/package-info.java rename to src/main/java/de/thm/arsnova/web/exceptions/package-info.java index ea831c868e3cc413cf3d9f4dba472116c3ed569d..03df89db51f1e4c190d1858814eabb70ed526558 100644 --- a/src/main/java/de/thm/arsnova/exceptions/package-info.java +++ b/src/main/java/de/thm/arsnova/web/exceptions/package-info.java @@ -1,4 +1,4 @@ /** * Contains exceptions used to return HTTP status codes */ -package de.thm.arsnova.exceptions; +package de.thm.arsnova.web.exceptions; diff --git a/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java b/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java index 6ceddaa77218ea4ff839f655e900638b3e05de74..be4809c12dece3bd439218a0e59bedea505712cb 100644 --- a/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java +++ b/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerImpl.java @@ -28,36 +28,34 @@ import com.corundumstudio.socketio.listener.DataListener; import com.corundumstudio.socketio.listener.DisconnectListener; import com.corundumstudio.socketio.protocol.Packet; import com.corundumstudio.socketio.protocol.PacketType; -import de.thm.arsnova.entities.Comment; -import de.thm.arsnova.entities.ScoreOptions; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.events.*; -import de.thm.arsnova.exceptions.NoContentException; -import de.thm.arsnova.exceptions.NotFoundException; -import de.thm.arsnova.exceptions.UnauthorizedException; -import de.thm.arsnova.services.CommentService; -import de.thm.arsnova.services.FeedbackService; -import de.thm.arsnova.services.ContentService; -import de.thm.arsnova.services.SessionService; -import de.thm.arsnova.services.UserService; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.Comment; +import de.thm.arsnova.model.ScoreOptions; +import de.thm.arsnova.model.migration.ToV2Migrator; +import de.thm.arsnova.event.*; +import de.thm.arsnova.web.exceptions.NoContentException; +import de.thm.arsnova.web.exceptions.NotFoundException; +import de.thm.arsnova.web.exceptions.UnauthorizedException; +import de.thm.arsnova.service.AnswerService; +import de.thm.arsnova.service.CommentService; +import de.thm.arsnova.service.FeedbackService; +import de.thm.arsnova.service.ContentService; +import de.thm.arsnova.service.RoomService; +import de.thm.arsnova.service.UserService; import de.thm.arsnova.websocket.message.Feedback; import de.thm.arsnova.websocket.message.Content; -import de.thm.arsnova.websocket.message.Session; +import de.thm.arsnova.websocket.message.Room; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Required; import org.springframework.scheduling.annotation.Async; -import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.authority.AuthorityUtils; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; import javax.annotation.PreDestroy; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; @@ -79,14 +77,20 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova private UserService userService; @Autowired - private SessionService sessionService; + private RoomService roomService; @Autowired private ContentService contentService; + @Autowired + private AnswerService answerService; + @Autowired private CommentService commentService; + @Autowired + private ToV2Migrator toV2Migrator; + private static final Logger logger = LoggerFactory.getLogger(ArsnovaSocketioServerImpl.class); private int portNumber; @@ -96,7 +100,6 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova private String storepass; private final Configuration config; private SocketIOServer server; - private boolean securityInitialized; public ArsnovaSocketioServerImpl() { config = new Configuration(); @@ -142,49 +145,52 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova @Override @Timed(name = "setFeedbackEvent.onData") public void onData(final SocketIOClient client, final Feedback data, final AckRequest ackSender) { - final User u = userService.getUser2SocketId(client.getSessionId()); + final ClientAuthentication u = userService.getUserToSocketId(client.getSessionId()); if (u == null) { logger.info("Client {} tried to send feedback but is not mapped to a user", client.getSessionId()); return; } - final String sessionKey = userService.getSessionByUsername(u.getUsername()); - final de.thm.arsnova.entities.Session session = sessionService.getInternal(sessionKey, u); + final String roomId = userService.getRoomIdByUserId(u.getId()); + final de.thm.arsnova.model.Room room = roomService.getInternal(roomId, u); - if (session.getFeedbackLock()) { - logger.debug("Feedback save blocked: {}", u, sessionKey, data.getValue()); + if (room.getSettings().isFeedbackLocked()) { + logger.debug("Feedback ignored: User: {}, Room Id: {}, Feedback: {}", u, roomId, data.getValue()); } else { - logger.debug("Feedback recieved: {}", u, sessionKey, data.getValue()); - if (null != sessionKey) { - feedbackService.save(sessionKey, data.getValue(), u); + logger.debug("Feedback received: User: {}, Room Id: {}, Feedback: {}", u, roomId, data.getValue()); + if (null != roomId) { + feedbackService.save(roomId, data.getValue(), u); } } } }); - server.addEventListener("setSession", Session.class, new DataListener<Session>() { + server.addEventListener("setSession", Room.class, new DataListener<Room>() { @Override @Timed(name = "setSessionEvent.onData") - public void onData(final SocketIOClient client, final Session session, final AckRequest ackSender) { - final User u = userService.getUser2SocketId(client.getSessionId()); + public void onData(final SocketIOClient client, final Room room, final AckRequest ackSender) { + final ClientAuthentication u = userService.getUserToSocketId(client.getSessionId()); if (null == u) { - logger.info("Client {} requested to join session but is not mapped to a user", client.getSessionId()); + logger.info("Client {} requested to join room but is not mapped to a user", client.getSessionId()); return; } - final String oldSessionKey = userService.getSessionByUsername(u.getUsername()); - if (null != session.getKeyword() && session.getKeyword().equals(oldSessionKey)) { - return; - } + final String oldRoomId = userService.getRoomIdByUserId(u.getId()); + if (null != room.getKeyword()) { + if (room.getKeyword().equals(oldRoomId)) { + return; + } + final String roomId = roomService.getIdByShortId(room.getKeyword()); - if (null != sessionService.join(session.getKeyword(), client.getSessionId())) { - /* active user count has to be sent to the client since the broadcast is - * not always sent as long as the polling solution is active simultaneously */ - reportActiveUserCountForSession(session.getKeyword()); - reportSessionDataToClient(session.getKeyword(), u, client); + if (null != roomId && null != roomService.join(roomId, client.getSessionId())) { + /* active user count has to be sent to the client since the broadcast is + * not always sent as long as the polling solution is active simultaneously */ + reportActiveUserCountForRoom(roomId); + reportRoomDataToClient(roomId, u, client); + } } - if (null != oldSessionKey) { - reportActiveUserCountForSession(oldSessionKey); + if (null != oldRoomId) { + reportActiveUserCountForRoom(oldRoomId); } } }); @@ -200,10 +206,10 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova SocketIOClient client, Comment comment, AckRequest ackRequest) { - final User user = userService.getUser2SocketId(client.getSessionId()); + final ClientAuthentication user = userService.getUserToSocketId(client.getSessionId()); try { - commentService.getAndMarkReadInternal(comment.getId(), user); - } catch (NotFoundException | UnauthorizedException e) { + commentService.getAndMarkRead(comment.getId()); + } catch (IOException | NotFoundException | UnauthorizedException e) { logger.error("Loading of comment {} failed for user {} with exception {}", comment.getId(), user, e.getMessage()); } } @@ -212,9 +218,9 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova server.addEventListener("readFreetextAnswer", String.class, new DataListener<String>() { @Override public void onData(SocketIOClient client, String answerId, AckRequest ackRequest) { - final User user = userService.getUser2SocketId(client.getSessionId()); + final ClientAuthentication user = userService.getUserToSocketId(client.getSessionId()); try { - contentService.getFreetextAnswerAndMarkRead(answerId, user); + answerService.getFreetextAnswerAndMarkRead(answerId, user); } catch (NotFoundException | UnauthorizedException e) { logger.error("Marking answer {} as read failed for user {} with exception {}", answerId, user, e.getMessage()); } @@ -228,14 +234,15 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova @Override @Timed(name = "setLearningProgressOptionsEvent.onData") public void onData(SocketIOClient client, ScoreOptions scoreOptions, AckRequest ack) { - final User user = userService.getUser2SocketId(client.getSessionId()); - final String sessionKey = userService.getSessionByUsername(user.getUsername()); - final de.thm.arsnova.entities.Session session = sessionService.getInternal(sessionKey, user); - if (session.isCreator(user)) { - session.setLearningProgressOptions(scoreOptions); - sessionService.updateInternal(session, user); - broadcastInSession(session.getKeyword(), "learningProgressOptions", scoreOptions); - } + throw new UnsupportedOperationException("Not implemented."); +// final ClientAuthentication user = userService.getUserToSocketId(client.getSessionId()); +// final String shortRoomId = userService.getSessionByUsername(user.getUsername()); +// final de.thm.arsnova.entities.Room room = roomService.getInternal(shortRoomId, user); +// if (room.getOwnerId().equals(user.getId())) { +// room.setLearningProgressOptions(scoreOptions); +// roomService.updateInternal(room, user); +// broadcastInRoom(room.getShortId(), "learningProgressOptions", scoreOptions); +// } } }); @@ -243,9 +250,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova @Override @Timed public void onConnect(final SocketIOClient client) { - if (!securityInitialized) { - initializeSecurity(); - } + } }); @@ -256,17 +261,17 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova if ( userService == null || client.getSessionId() == null - || userService.getUser2SocketId(client.getSessionId()) == null + || userService.getUserToSocketId(client.getSessionId()) == null ) { return; } - final String username = userService.getUser2SocketId(client.getSessionId()).getUsername(); - final String sessionKey = userService.getSessionByUsername(username); - userService.removeUserFromSessionBySocketId(client.getSessionId()); - userService.removeUser2SocketId(client.getSessionId()); - if (null != sessionKey) { + final String userId = userService.getUserToSocketId(client.getSessionId()).getId(); + final String roomId = userService.getRoomIdByUserId(userId); + userService.removeUserFromRoomBySocketId(client.getSessionId()); + userService.removeUserToSocketId(client.getSessionId()); + if (null != roomId) { /* user disconnected before joining a session */ - reportActiveUserCountForSession(sessionKey); + reportActiveUserCountForRoom(roomId); } } }); @@ -334,19 +339,19 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova this.useSSL = useSSL; } - public void reportDeletedFeedback(final User user, final Set<de.thm.arsnova.entities.Session> arsSessions) { - final List<String> keywords = new ArrayList<>(); - for (final de.thm.arsnova.entities.Session session : arsSessions) { - keywords.add(session.getKeyword()); + public void reportDeletedFeedback(final ClientAuthentication user, final Set<de.thm.arsnova.model.Room> rooms) { + final List<String> roomShortIds = new ArrayList<>(); + for (final de.thm.arsnova.model.Room room : rooms) { + roomShortIds.add(room.getShortId()); } - this.sendToUser(user, "feedbackReset", keywords); + this.sendToUser(user, "feedbackReset", roomShortIds); } - private List<UUID> findConnectionIdForUser(final User user) { + private List<UUID> findConnectionIdForUser(final ClientAuthentication user) { final List<UUID> result = new ArrayList<>(); - for (final Entry<UUID, User> e : userService.socketId2User()) { + for (final Entry<UUID, ClientAuthentication> e : userService.getSocketIdToUser()) { final UUID someUsersConnectionId = e.getKey(); - final User someUser = e.getValue(); + final ClientAuthentication someUser = e.getValue(); if (someUser.equals(user)) { result.add(someUsersConnectionId); } @@ -354,7 +359,7 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova return result; } - private void sendToUser(final User user, final String event, Object data) { + private void sendToUser(final ClientAuthentication user, final String event, Object data) { final List<UUID> connectionIds = findConnectionIdForUser(user); if (connectionIds.isEmpty()) { return; @@ -370,26 +375,27 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova * Currently only sends the feedback data to the client. Should be used for all * relevant Socket.IO data, the client needs to know after joining a session. */ - public void reportSessionDataToClient(final String sessionKey, final User user, final SocketIOClient client) { - final de.thm.arsnova.entities.Session session = sessionService.getInternal(sessionKey, user); - final de.thm.arsnova.entities.SessionFeature features = sessionService.getFeatures(sessionKey); - - client.sendEvent("unansweredLecturerQuestions", contentService.getUnAnsweredLectureQuestionIds(sessionKey, user)); - client.sendEvent("unansweredPreparationQuestions", contentService.getUnAnsweredPreparationQuestionIds(sessionKey, user)); - client.sendEvent("countLectureQuestionAnswers", contentService.countLectureQuestionAnswersInternal(sessionKey)); - client.sendEvent("countPreparationQuestionAnswers", contentService.countPreparationQuestionAnswersInternal(sessionKey)); - client.sendEvent("activeUserCountData", sessionService.activeUsers(sessionKey)); - client.sendEvent("learningProgressOptions", session.getLearningProgressOptions()); - final de.thm.arsnova.entities.Feedback fb = feedbackService.getBySessionKey(sessionKey); + public void reportRoomDataToClient(final String roomId, final ClientAuthentication user, final SocketIOClient client) { + final de.thm.arsnova.model.Room room = roomService.getInternal(roomId, user); + final de.thm.arsnova.model.Room.Settings settings = room.getSettings(); + + client.sendEvent("unansweredLecturerQuestions", contentService.getUnAnsweredLectureContentIds(roomId, user)); + client.sendEvent("unansweredPreparationQuestions", contentService.getUnAnsweredPreparationContentIds(roomId, user)); + /* FIXME: Content variant is ignored for now */ + client.sendEvent("countLectureQuestionAnswers", answerService.countTotalAnswersByRoomId(roomId)); + client.sendEvent("countPreparationQuestionAnswers", answerService.countTotalAnswersByRoomId(roomId)); + client.sendEvent("activeUserCountData", roomService.activeUsers(roomId)); +// client.sendEvent("learningProgressOptions", room.getLearningProgressOptions()); + final de.thm.arsnova.model.Feedback fb = feedbackService.getByRoomId(roomId); client.sendEvent("feedbackData", fb.getValues()); - if (features.isFlashcard() || features.isFlashcardFeature()) { - client.sendEvent("countFlashcards", contentService.countFlashcardsForUserInternal(sessionKey)); - client.sendEvent("flipFlashcards", session.getFlipFlashcards()); + if (settings.isFlashcardsEnabled()) { + client.sendEvent("countFlashcards", contentService.countFlashcardsForUserInternal(roomId)); +// client.sendEvent("flipFlashcards", room.getFlipFlashcards()); } try { - final long averageFeedback = feedbackService.calculateRoundedAverageFeedback(sessionKey); + final long averageFeedback = feedbackService.calculateRoundedAverageFeedback(roomId); client.sendEvent("feedbackDataRoundedAverage", averageFeedback); } catch (final NoContentException e) { final Object object = null; // can't directly use "null". @@ -397,22 +403,22 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova } } - public void reportUpdatedFeedbackForSession(final de.thm.arsnova.entities.Session session) { - final de.thm.arsnova.entities.Feedback fb = feedbackService.getBySessionKey(session.getKeyword()); - broadcastInSession(session.getKeyword(), "feedbackData", fb.getValues()); + public void reportUpdatedFeedbackForRoom(final de.thm.arsnova.model.Room room) { + final de.thm.arsnova.model.Feedback fb = feedbackService.getByRoomId(room.getId()); + broadcastInRoom(room.getId(), "feedbackData", fb.getValues()); try { - final long averageFeedback = feedbackService.calculateRoundedAverageFeedback(session.getKeyword()); - broadcastInSession(session.getKeyword(), "feedbackDataRoundedAverage", averageFeedback); + final long averageFeedback = feedbackService.calculateRoundedAverageFeedback(room.getId()); + broadcastInRoom(room.getId(), "feedbackDataRoundedAverage", averageFeedback); } catch (final NoContentException e) { - broadcastInSession(session.getKeyword(), "feedbackDataRoundedAverage", null); + broadcastInRoom(room.getId(), "feedbackDataRoundedAverage", null); } } - public void reportFeedbackForUserInSession(final de.thm.arsnova.entities.Session session, final User user) { - final de.thm.arsnova.entities.Feedback fb = feedbackService.getBySessionKey(session.getKeyword()); + public void reportFeedbackForUserInRoom(final Room room, final ClientAuthentication user) { + final de.thm.arsnova.model.Feedback fb = feedbackService.getByRoomId(room.getKeyword()); Long averageFeedback; try { - averageFeedback = feedbackService.calculateRoundedAverageFeedback(session.getKeyword()); + averageFeedback = feedbackService.calculateRoundedAverageFeedback(room.getKeyword()); } catch (final NoContentException e) { averageFeedback = null; } @@ -429,55 +435,55 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova } } - public void reportActiveUserCountForSession(final String sessionKey) { - final int count = userService.getUsersBySessionKey(sessionKey).size(); + public void reportActiveUserCountForRoom(final String roomId) { + final int count = userService.getUsersByRoomId(roomId).size(); - broadcastInSession(sessionKey, "activeUserCountData", count); + broadcastInRoom(roomId, "activeUserCountData", count); } - public void reportAnswersToLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session, final Content content) { - broadcastInSession(session.getKeyword(), "answersToLecQuestionAvail", content.get_id()); + public void reportAnswersToContentAvailable(final de.thm.arsnova.model.Room room, final Content content) { + broadcastInRoom(room.getId(), "answersToLecQuestionAvail", content.get_id()); } - public void reportAudienceQuestionAvailable(final de.thm.arsnova.entities.Session session, final Comment audienceQuestion) { + public void reportCommentAvailable(final de.thm.arsnova.model.Room room, final Comment comment) { /* TODO role handling implementation, send this only to users with role lecturer */ - broadcastInSession(session.getKeyword(), "audQuestionAvail", audienceQuestion.getId()); + broadcastInRoom(room.getId(), "audQuestionAvail", comment.getId()); } - public void reportLecturerQuestionAvailable(final de.thm.arsnova.entities.Session session, final List<de.thm.arsnova.entities.Content> qs) { + public void reportContentAvailable(final de.thm.arsnova.model.Room room, final List<de.thm.arsnova.model.Content> qs) { List<Content> contents = new ArrayList<>(); - for (de.thm.arsnova.entities.Content q : qs) { + for (de.thm.arsnova.model.Content q : qs) { contents.add(new Content(q)); } /* TODO role handling implementation, send this only to users with role audience */ if (!qs.isEmpty()) { - broadcastInSession(session.getKeyword(), "lecQuestionAvail", contents.get(0).get_id()); // deprecated! + broadcastInRoom(room.getId(), "lecQuestionAvail", contents.get(0).get_id()); // deprecated! } - broadcastInSession(session.getKeyword(), "lecturerQuestionAvailable", contents); + broadcastInRoom(room.getId(), "lecturerQuestionAvailable", contents); } - public void reportLecturerQuestionsLocked(final de.thm.arsnova.entities.Session session, final List<de.thm.arsnova.entities.Content> qs) { + public void reportContentsLocked(final de.thm.arsnova.model.Room room, final List<de.thm.arsnova.model.Content> qs) { List<Content> contents = new ArrayList<>(); - for (de.thm.arsnova.entities.Content q : qs) { + for (de.thm.arsnova.model.Content q : qs) { contents.add(new Content(q)); } - broadcastInSession(session.getKeyword(), "lecturerQuestionLocked", contents); + broadcastInRoom(room.getId(), "lecturerQuestionLocked", contents); } - public void reportSessionStatus(final String sessionKey, final boolean active) { - broadcastInSession(sessionKey, "setSessionActive", active); + public void reportRoomStatus(final String roomId, final boolean active) { + broadcastInRoom(roomId, "setSessionActive", active); } - public void broadcastInSession(final String sessionKey, final String eventName, final Object data) { - /* collect a list of users which are in the current session iterate over + public void broadcastInRoom(final String roomId, final String eventName, final Object data) { + /* collect a list of users which are in the current room iterate over * all connected clients and if send feedback, if user is in current - * session + * room */ - final Set<User> users = userService.getUsersBySessionKey(sessionKey); + final Set<ClientAuthentication> users = userService.getUsersByRoomId(roomId); for (final SocketIOClient c : server.getAllClients()) { - final User u = userService.getUser2SocketId(c.getSessionId()); + final ClientAuthentication u = userService.getUserToSocketId(c.getSessionId()); if (u != null && users.contains(u)) { c.sendEvent(eventName, data); } @@ -486,50 +492,51 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova @Override public void visit(NewQuestionEvent event) { - this.reportLecturerQuestionAvailable(event.getSession(), Collections.singletonList(event.getQuestion())); + this.reportContentAvailable(event.getRoom(), Collections.singletonList(event.getQuestion())); } @Override public void visit(UnlockQuestionEvent event) { - this.reportLecturerQuestionAvailable(event.getSession(), Collections.singletonList(event.getQuestion())); + this.reportContentAvailable(event.getRoom(), Collections.singletonList(event.getQuestion())); } @Override public void visit(LockQuestionEvent event) { - this.reportLecturerQuestionsLocked(event.getSession(), Collections.singletonList(event.getQuestion())); + this.reportContentsLocked(event.getRoom(), Collections.singletonList(event.getQuestion())); } @Override public void visit(UnlockQuestionsEvent event) { - this.reportLecturerQuestionAvailable(event.getSession(), event.getQuestions()); + this.reportContentAvailable(event.getRoom(), event.getQuestions()); } @Override public void visit(LockQuestionsEvent event) { - this.reportLecturerQuestionsLocked(event.getSession(), event.getQuestions()); + this.reportContentsLocked(event.getRoom(), event.getQuestions()); } @Override public void visit(NewCommentEvent event) { - this.reportAudienceQuestionAvailable(event.getSession(), event.getQuestion()); + this.reportCommentAvailable(event.getRoom(), event.getQuestion()); } @Async @Override @Timed(name = "visit.NewAnswerEvent") public void visit(NewAnswerEvent event) { - final String sessionKey = event.getSession().getKeyword(); - this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Content(event.getContent())); - broadcastInSession(sessionKey, "countQuestionAnswersByQuestionId", contentService.countAnswersAndAbstentionsInternal(event.getContent().getId())); - broadcastInSession(sessionKey, "countLectureQuestionAnswers", contentService.countLectureQuestionAnswersInternal(sessionKey)); - broadcastInSession(sessionKey, "countPreparationQuestionAnswers", contentService.countPreparationQuestionAnswersInternal(sessionKey)); + final String roomId = event.getRoom().getId(); + this.reportAnswersToContentAvailable(event.getRoom(), new Content(event.getContent())); + broadcastInRoom(roomId, "countQuestionAnswersByQuestionId", answerService.countAnswersAndAbstentionsInternal(event.getContent().getId())); + /* FIXME: Content variant is ignored for now */ + broadcastInRoom(roomId, "countLectureQuestionAnswers", answerService.countTotalAnswersByRoomId(roomId)); + broadcastInRoom(roomId, "countPreparationQuestionAnswers", answerService.countTotalAnswersByRoomId(roomId)); // Update the unanswered count for the content variant that was answered. - final de.thm.arsnova.entities.Content content = event.getContent(); - if ("lecture".equals(content.getQuestionVariant())) { - sendToUser(event.getUser(), "unansweredLecturerQuestions", contentService.getUnAnsweredLectureQuestionIds(sessionKey, event.getUser())); - } else if ("preparation".equals(content.getQuestionVariant())) { - sendToUser(event.getUser(), "unansweredPreparationQuestions", contentService.getUnAnsweredPreparationQuestionIds(sessionKey, event.getUser())); + final de.thm.arsnova.model.Content content = event.getContent(); + if ("lecture".equals(content.getGroups())) { + sendToUser(event.getUser(), "unansweredLecturerQuestions", contentService.getUnAnsweredLectureContentIds(roomId, event.getUser())); + } else if ("preparation".equals(content.getGroups())) { + sendToUser(event.getUser(), "unansweredPreparationQuestions", contentService.getUnAnsweredPreparationContentIds(roomId, event.getUser())); } } @@ -537,93 +544,94 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova @Override @Timed(name = "visit.DeleteAnswerEvent") public void visit(DeleteAnswerEvent event) { - final String sessionKey = event.getSession().getKeyword(); - this.reportAnswersToLecturerQuestionAvailable(event.getSession(), new Content(event.getQuestion())); + final String roomId = event.getRoom().getId(); + this.reportAnswersToContentAvailable(event.getRoom(), new Content(event.getQuestion())); // We do not know which user's answer was deleted, so we can't update his 'unanswered' list of questions... - broadcastInSession(sessionKey, "countLectureQuestionAnswers", contentService.countLectureQuestionAnswersInternal(sessionKey)); - broadcastInSession(sessionKey, "countPreparationQuestionAnswers", contentService.countPreparationQuestionAnswersInternal(sessionKey)); + /* FIXME: Content variant is ignored for now */ + broadcastInRoom(roomId, "countLectureQuestionAnswers", answerService.countTotalAnswersByRoomId(roomId)); + broadcastInRoom(roomId, "countPreparationQuestionAnswers", answerService.countTotalAnswersByRoomId(roomId)); } @Async @Override @Timed(name = "visit.PiRoundDelayedStartEvent") public void visit(PiRoundDelayedStartEvent event) { - final String sessionKey = event.getSession().getKeyword(); - broadcastInSession(sessionKey, "startDelayedPiRound", event.getPiRoundInformations()); + final String roomId = event.getRoom().getId(); + broadcastInRoom(roomId, "startDelayedPiRound", event.getPiRoundInformations()); } @Async @Override @Timed(name = "visit.PiRoundEndEvent") public void visit(PiRoundEndEvent event) { - final String sessionKey = event.getSession().getKeyword(); - broadcastInSession(sessionKey, "endPiRound", event.getPiRoundEndInformations()); + final String roomId = event.getRoom().getId(); + broadcastInRoom(roomId, "endPiRound", event.getPiRoundEndInformations()); } @Async @Override @Timed(name = "visit.PiRoundCancelEvent") public void visit(PiRoundCancelEvent event) { - final String sessionKey = event.getSession().getKeyword(); - broadcastInSession(sessionKey, "cancelPiRound", event.getQuestionId()); + final String roomId = event.getRoom().getId(); + broadcastInRoom(roomId, "cancelPiRound", event.getContentId()); } @Override public void visit(PiRoundResetEvent event) { - final String sessionKey = event.getSession().getKeyword(); - broadcastInSession(sessionKey, "resetPiRound", event.getPiRoundResetInformations()); + final String roomId = event.getRoom().getId(); + broadcastInRoom(roomId, "resetPiRound", event.getPiRoundResetInformations()); } @Override public void visit(LockVoteEvent event) { - final String sessionKey = event.getSession().getKeyword(); - broadcastInSession(sessionKey, "lockVote", event.getVotingAdmission()); + final String roomId = event.getRoom().getId(); + broadcastInRoom(roomId, "lockVote", event.getVotingAdmission()); } @Override public void visit(UnlockVoteEvent event) { - final String sessionKey = event.getSession().getKeyword(); - broadcastInSession(sessionKey, "unlockVote", event.getVotingAdmission()); + final String roomId = event.getRoom().getId(); + broadcastInRoom(roomId, "unlockVote", event.getVotingAdmission()); } @Override public void visit(LockVotesEvent event) { List<Content> contents = new ArrayList<>(); - for (de.thm.arsnova.entities.Content q : event.getQuestions()) { + for (de.thm.arsnova.model.Content q : event.getQuestions()) { contents.add(new Content(q)); } - broadcastInSession(event.getSession().getKeyword(), "lockVotes", contents); + broadcastInRoom(event.getRoom().getId(), "lockVotes", contents); } @Override public void visit(UnlockVotesEvent event) { List<Content> contents = new ArrayList<>(); - for (de.thm.arsnova.entities.Content q : event.getQuestions()) { + for (de.thm.arsnova.model.Content q : event.getQuestions()) { contents.add(new Content(q)); } - broadcastInSession(event.getSession().getKeyword(), "unlockVotes", contents); + broadcastInRoom(event.getRoom().getId(), "unlockVotes", contents); } @Override public void visit(FeatureChangeEvent event) { - final String sessionKey = event.getSession().getKeyword(); - final de.thm.arsnova.entities.SessionFeature features = event.getSession().getFeatures(); - broadcastInSession(sessionKey, "featureChange", features); + final String roomId = event.getRoom().getId(); + final de.thm.arsnova.model.Room.Settings settings = event.getRoom().getSettings(); + broadcastInRoom(roomId, "featureChange", toV2Migrator.migrate(settings)); - if (features.isFlashcard() || features.isFlashcardFeature()) { - broadcastInSession(sessionKey, "countFlashcards", contentService.countFlashcardsForUserInternal(sessionKey)); - broadcastInSession(sessionKey, "flipFlashcards", event.getSession().getFlipFlashcards()); + if (settings.isFlashcardsEnabled()) { + broadcastInRoom(roomId, "countFlashcards", contentService.countFlashcardsForUserInternal(roomId)); +// broadcastInRoom(roomId, "flipFlashcards", event.getRoom().getFlipFlashcards()); } } @Override public void visit(LockFeedbackEvent event) { - broadcastInSession(event.getSession().getKeyword(), "lockFeedback", event.getSession().getFeedbackLock()); + broadcastInRoom(event.getRoom().getId(), "lockFeedback", event.getRoom().getSettings().isFeedbackLocked()); } @Override public void visit(FlipFlashcardsEvent event) { - broadcastInSession(event.getSession().getKeyword(), "flipFlashcards", event.getSession().getFlipFlashcards()); +// broadcastInRoom(event.getRoom().getId(), "flipFlashcards", event.getRoom().getFlipFlashcards()); } @Override @@ -664,37 +672,28 @@ public class ArsnovaSocketioServerImpl implements ArsnovaSocketioServer, Arsnova @Override public void visit(NewFeedbackEvent event) { - this.reportUpdatedFeedbackForSession(event.getSession()); + this.reportUpdatedFeedbackForRoom(event.getRoom()); } @Override - public void visit(DeleteFeedbackForSessionsEvent event) { + public void visit(DeleteFeedbackForRoomsEvent event) { this.reportDeletedFeedback(event.getUser(), event.getSessions()); } @Override - public void visit(StatusSessionEvent event) { - this.reportSessionStatus(event.getSession().getKeyword(), event.getSession().isActive()); + public void visit(StatusRoomEvent event) { + this.reportRoomStatus(event.getRoom().getId(), !event.getRoom().isClosed()); } @Override public void visit(ChangeScoreEvent event) { - broadcastInSession(event.getSession().getKeyword(), "learningProgressChange", null); + broadcastInRoom(event.getRoom().getId(), "learningProgressChange", null); } @Override - public void visit(NewSessionEvent event) { } + public void visit(NewRoomEvent event) { } @Override - public void visit(DeleteSessionEvent event) { } - - private void initializeSecurity() { - Authentication auth = new AnonymousAuthenticationToken("websocket", "websocket", - AuthorityUtils.createAuthorityList("ROLE_WEBSOCKET_ACCESS")); - SecurityContext context = SecurityContextHolder.createEmptyContext(); - context.setAuthentication(auth); - SecurityContextHolder.setContext(context); - securityInitialized = true; - } + public void visit(DeleteRoomEvent event) { } } diff --git a/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerListener.java b/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerListener.java index bbb98296581c45334e8399ad91ea2d84a7e1fd5a..1d69bc792375c57b2d5a87b9e7bb16c196d2ff1a 100644 --- a/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerListener.java +++ b/src/main/java/de/thm/arsnova/websocket/ArsnovaSocketioServerListener.java @@ -17,8 +17,8 @@ */ package de.thm.arsnova.websocket; -import de.thm.arsnova.events.ArsnovaEvent; -import de.thm.arsnova.events.ArsnovaEventVisitor; +import de.thm.arsnova.event.ArsnovaEvent; +import de.thm.arsnova.event.ArsnovaEventVisitor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationListener; import org.springframework.stereotype.Component; diff --git a/src/main/java/de/thm/arsnova/websocket/WebsocketAuthenticationAspect.java b/src/main/java/de/thm/arsnova/websocket/WebsocketAuthenticationAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..18ef7807352a9af0fe4099936fc3b6334b287680 --- /dev/null +++ b/src/main/java/de/thm/arsnova/websocket/WebsocketAuthenticationAspect.java @@ -0,0 +1,92 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.websocket; + +import com.corundumstudio.socketio.SocketIOClient; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.security.User; +import de.thm.arsnova.service.UserService; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Configurable; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * This aspect populates the SecurityContextHolder of Spring Security when data are received via WebSockets. + * It allows WebSocket listeners to access service methods which are secured by Spring Security annotations. + * + * @author Daniel Gerhardt + */ +@Aspect +@Configurable +public class WebsocketAuthenticationAspect { + private static final Logger logger = LoggerFactory.getLogger(WebsocketAuthenticationAspect.class); + private static final GrantedAuthority WEBSOCKET_AUTHORITY = new SimpleGrantedAuthority("ROLE_WEBSOCKET_ACCESS"); + + private UserService userService; + + @Around("execution(void com.corundumstudio.socketio.listener.DataListener+.onData(..)) && args(client, message, ..)") + public <T> void handleWebsocketAuthentication(final ProceedingJoinPoint pjp, + final SocketIOClient client, final T message) throws Throwable { + logger.debug("Executing WebsocketAuthenticationAspect for onData event: Session Id: {}, Message Class: {}", + client.getSessionId(), message.getClass()); + try { + populateSecurityContext(client.getSessionId()); + pjp.proceed(); + } finally { + clearSecurityContext(); + } + } + + private void populateSecurityContext(final UUID socketId) { + ClientAuthentication userAuth = userService.getUserToSocketId(socketId); + if (userAuth == null) { + throw new AccessDeniedException("No user authenticated for WebSocket connection"); + } + SecurityContext context = SecurityContextHolder.getContext(); + Set<GrantedAuthority> authorities = new HashSet<>(); + authorities.add(WEBSOCKET_AUTHORITY); + User user = new User(userAuth, authorities); + Authentication auth = new UsernamePasswordAuthenticationToken(user, null, authorities); + context.setAuthentication(auth); + SecurityContextHolder.setContext(context); + } + + private void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + + @Autowired + public void setUserService(final UserService userService) { + this.userService = userService; + } +} diff --git a/src/main/java/de/thm/arsnova/websocket/message/Content.java b/src/main/java/de/thm/arsnova/websocket/message/Content.java index 6344dd3ebf506a5c33270ba96c13d8df0e621956..c95f6a2b699a608b3f2eb30672989102eea8e606 100644 --- a/src/main/java/de/thm/arsnova/websocket/message/Content.java +++ b/src/main/java/de/thm/arsnova/websocket/message/Content.java @@ -25,9 +25,10 @@ public class Content { private final String _id; private final String variant; - public Content(de.thm.arsnova.entities.Content content) { + public Content(de.thm.arsnova.model.Content content) { this._id = content.getId(); - this.variant = content.getQuestionVariant(); + /* FIXME: Message does not support content with multiple groups */ + this.variant = content.getGroups().toArray(new String[1])[0]; } public String get_id() { diff --git a/src/main/java/de/thm/arsnova/websocket/message/Session.java b/src/main/java/de/thm/arsnova/websocket/message/Room.java similarity index 97% rename from src/main/java/de/thm/arsnova/websocket/message/Session.java rename to src/main/java/de/thm/arsnova/websocket/message/Room.java index 8afccd951e5b6053b9f482fd644e1abbf291264d..efa2216483046d9293dda532e3abdc7e4ed934be 100644 --- a/src/main/java/de/thm/arsnova/websocket/message/Session.java +++ b/src/main/java/de/thm/arsnova/websocket/message/Room.java @@ -20,7 +20,7 @@ package de.thm.arsnova.websocket.message; /** * Represents a session. */ -public class Session { +public class Room { private String keyword; public String getKeyword() { diff --git a/src/main/resources/META-INF/aop.xml b/src/main/resources/META-INF/aop.xml index 6681890d4b954ff74c1b496d1e21c49f65c1472c..fe4871b30ed1a8777f4caccb3c138cc8747a843c 100644 --- a/src/main/resources/META-INF/aop.xml +++ b/src/main/resources/META-INF/aop.xml @@ -1,12 +1,13 @@ <?xml version="1.0" encoding="UTF-8"?> <aspectj> <weaver options="-verbose -showWeaveInfo"> - <!-- only weave classes in our application-specific packages --> <include within="de.thm.arsnova..*"/> + <include within="com.corundumstudio.socketio.listener.*"/> </weaver> <aspects> - <aspect name="de.thm.arsnova.aop.RangeAspect"/> - <aspect name="de.thm.arsnova.aop.UserSessionAspect"/> + <aspect name="de.thm.arsnova.web.RangeAspect"/> + <aspect name="de.thm.arsnova.web.InternalEntityAspect"/> + <aspect name="de.thm.arsnova.websocket.WebsocketAuthenticationAspect"/> </aspects> </aspectj> diff --git a/src/main/resources/arsnova.properties.example b/src/main/resources/arsnova.properties.example index 34fa9b0a1d80f6a0a91fa1bbd5e3d77c239494e8..0eda0cadfc51d0d68c578ab51db68bcb30b2a68f 100644 --- a/src/main/resources/arsnova.properties.example +++ b/src/main/resources/arsnova.properties.example @@ -38,9 +38,10 @@ security.admin-accounts= ################################################################################ couchdb.host=localhost couchdb.port=5984 -couchdb.name=arsnova +couchdb.name=arsnova3 couchdb.username=admin couchdb.password= +#couchdb.migrate-from=arsnova ################################################################################ @@ -58,6 +59,12 @@ mail.host= # several minutes security.authentication.login-try-limit=50 +# JSON Web Tokens +# +security.jwt.serverId=arsnova.backend.v3:com.example +security.jwt.secret= +security.jwt.validity-period=T6H + # Configuration parameters for authentication services: # enabled: enable or disable the service # allowed-roles: enable/disable service for a specific role (valid roles: speaker, student) diff --git a/src/main/resources/couchdb/Answer.design.js b/src/main/resources/couchdb/Answer.design.js index 44d0a92f3b4ecf99a9bcfb878668bb2421f19847..3e89d47b19c6d1875d7b4d134198f78259ad42c4 100644 --- a/src/main/resources/couchdb/Answer.design.js +++ b/src/main/resources/couchdb/Answer.design.js @@ -2,69 +2,85 @@ var designDoc = { "_id": "_design/Answer", "language": "javascript", "views": { - "by_questionid": { + "by_id": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit(doc.questionId, {_rev: doc._rev}); + if (doc.type === "Answer") { + emit(doc._id, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_contentid": { + "map": function (doc) { + if (doc.type === "Answer") { + emit(doc.contentId, {_rev: doc._rev}); } } }, - "by_questionid_piround_text_subject": { + "by_contentid_round_body_subject": { + "map": function (doc) { + if (doc.type === "Answer") { + emit([doc.contentId, doc.round, doc.abstention, doc.body, doc.subject, doc.successfulFreeTextAnswer], {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_contentid_round_selectedchoiceindexes": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit([doc.questionId, doc.piRound, doc.abstention, doc.answerText, doc.answerSubject, doc.successfulFreeTextAnswer], {_rev: doc._rev}); + if (doc.type === "Answer") { + emit([doc.contentId, doc.round, doc.selectedChoiceIndexes], {_rev: doc._rev}); } }, "reduce": "_count" }, - "by_questionid_timestamp": { + "by_contentid_creationtimestamp": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit([doc.questionId, doc.timestamp], {_rev: doc._rev}); + if (doc.type === "Answer") { + emit([doc.contentId, doc.creationTimestamp], {_rev: doc._rev}); } } }, - "by_questionid_user_piround": { + "by_contentid_creatorid_round": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit([doc.questionId, doc.user, doc.piRound], {_rev: doc._rev}); + if (doc.type === "Answer") { + emit([doc.contentId, doc.creatorId, doc.round], {_rev: doc._rev}); } } }, - "by_sessionid": { + "by_roomid": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit(doc.sessionId, {_rev: doc._rev}); + if (doc.type === "Answer") { + emit(doc.roomId, {_rev: doc._rev}); } }, "reduce": "_count" }, - "by_sessionid_variant": { + "by_roomid_variant": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit([doc.sessionId, doc.questionVariant], {_rev: doc._rev}); + if (doc.type === "Answer") { + emit([doc.roomId, doc.questionVariant], {_rev: doc._rev}); } }, "reduce": "_count" }, - "by_user_sessionid": { + "by_creatorid_roomid": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit([doc.user, doc.sessionId], {_rev: doc._rev}); + if (doc.type === "Answer") { + emit([doc.creatorId, doc.roomId], {_rev: doc._rev}); } } }, - "questionid_by_user_sessionid_variant": { + "contentid_by_creatorid_roomid_variant": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit([doc.user, doc.sessionId, doc.questionVariant], doc.questionId); + if (doc.type === "Answer") { + emit([doc.user, doc.roomId, doc.questionVariant], doc.contentId); } } }, - "questionid_piround_by_user_sessionid_variant": { + "contentid_round_by_creatorid_roomid_variant": { "map": function (doc) { - if (doc.type === "skill_question_answer") { - emit([doc.user, doc.sessionId, doc.questionVariant], [doc.questionId, doc.piRound]); + if (doc.type === "Answer") { + emit([doc.creatorId, doc.roomId, doc.questionVariant], [doc.contentId, doc.round]); } } } diff --git a/src/main/resources/couchdb/Attachment.design.js b/src/main/resources/couchdb/Attachment.design.js new file mode 100644 index 0000000000000000000000000000000000000000..5fc0678f11b4f022dcd205e120caeb62f9e2de52 --- /dev/null +++ b/src/main/resources/couchdb/Attachment.design.js @@ -0,0 +1,22 @@ +var designDoc = { + "_id": "_design/Attachment", + "language": "javascript", + "views": { + "by_id": { + "map": function (doc) { + if (doc.type === "Attachment") { + emit(doc._id, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_creatorid": { + "map": function (doc) { + if (doc.type === "Attachment") { + emit(doc.creatorId, {_rev: doc._rev}); + } + }, + "reduce": "_count" + } + } +} diff --git a/src/main/resources/couchdb/Comment.design.js b/src/main/resources/couchdb/Comment.design.js index c407a2eb551f8c45f8f87c8efcdc69443d1affc1..9a5cb79f1d6631ea728c71e48e6e9c701838347a 100644 --- a/src/main/resources/couchdb/Comment.design.js +++ b/src/main/resources/couchdb/Comment.design.js @@ -2,41 +2,49 @@ var designDoc = { "_id": "_design/Comment", "language": "javascript", "views": { - "by_sessionid": { + "by_id": { "map": function (doc) { - if (doc.type === "interposed_question") { - emit(doc.sessionId, {_rev: doc._rev}); + if (doc.type === "Comment") { + emit(doc._id, {_rev: doc._rev}); } }, "reduce": "_count" }, - "by_sessionid_creator_read": { + "by_roomid": { "map": function (doc) { - if (doc.type === "interposed_question") { - emit([doc.sessionId, doc.creator, doc.read], {_rev: doc._rev}) + if (doc.type === "Comment") { + emit(doc.roomId, {_rev: doc._rev}); } }, "reduce": "_count" }, - "by_sessionid_creator_timestamp": { + "by_roomid_creatorid_read": { "map": function (doc) { - if (doc.type === "interposed_question") { - emit([doc.sessionId, doc.creator, doc.timestamp], {_rev: doc._rev}); + if (doc.type === "Comment") { + emit([doc.roomId, doc.creatorId, doc.read], {_rev: doc._rev}) + } + }, + "reduce": "_count" + }, + "by_roomid_creatorid_creationtimestamp": { + "map": function (doc) { + if (doc.type === "Comment") { + emit([doc.roomId, doc.creatorId, doc.creationTimestamp], {_rev: doc._rev}); } } }, - "by_sessionid_read": { + "by_roomid_read": { "map": function (doc) { - if (doc.type === "interposed_question") { - emit([doc.sessionId, doc.read], {_rev: doc._rev}); + if (doc.type === "Comment") { + emit([doc.roomId, doc.read], {_rev: doc._rev}); } }, "reduce": "_count" }, - "by_sessionid_timestamp": { + "by_roomid_creationtimestamp": { "map": function (doc) { - if (doc.type === "interposed_question") { - emit([doc.sessionId, doc.timestamp], {_rev: doc._rev}); + if (doc.type === "Comment") { + emit([doc.roomId, doc.creationTimestamp], {_rev: doc._rev}); } } } diff --git a/src/main/resources/couchdb/Content.design.js b/src/main/resources/couchdb/Content.design.js index cb3f2f98208cb83b87d7d5b1183f43c969629c1f..5b09c23f6fe15cb03a481f2c737ee23d54f8a0d3 100644 --- a/src/main/resources/couchdb/Content.design.js +++ b/src/main/resources/couchdb/Content.design.js @@ -2,18 +2,26 @@ var designDoc = { "_id": "_design/Content", "language": "javascript", "views": { - "by_sessionid": { + "by_id": { "map": function (doc) { - if (doc.type === "skill_question") { - emit(doc.sessionId, {_rev: doc._rev}); + if (doc.type === "Content") { + emit(doc._id, {_rev: doc._rev}); } }, "reduce": "_count" }, - "by_sessionid_variant_active": { + "by_roomid": { "map": function (doc) { - if (doc.type === "skill_question") { - emit([doc.sessionId, doc.questionVariant, doc.active, doc.subject, doc.text.substr(0, 16)], {_rev: doc._rev}); + if (doc.type === "Content") { + emit(doc.roomId, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_roomid_group_locked": { + "map": function (doc) { + if (doc.type === "Content") { + emit([doc.roomId, doc.group, doc.locked, doc.subject, doc.body.substr(0, 16)], {_rev: doc._rev}); } }, "reduce": "_count" diff --git a/src/main/resources/couchdb/DbUser.design.js b/src/main/resources/couchdb/DbUser.design.js deleted file mode 100644 index aa63058d15eb951257a7e076ba2f8cb99a25bb15..0000000000000000000000000000000000000000 --- a/src/main/resources/couchdb/DbUser.design.js +++ /dev/null @@ -1,18 +0,0 @@ -var designDoc = { - "_id": "_design/DbUser", - "language": "javascript", - "views": { - "by_creation_for_inactive": { - "map": function (doc) { - if (doc.type === "userdetails" && doc.activationKey) { - emit(doc.creation, {_rev: doc._rev}); - } - } - }, - "by_username": { - "map": function (doc) { - if (doc.type === "userdetails") emit(doc.username, {_rev: doc._rev}); - } - } - } -}; diff --git a/src/main/resources/couchdb/LoggedIn.design.js b/src/main/resources/couchdb/LoggedIn.design.js deleted file mode 100644 index aadec2d1e364f8ef9b15defa43c75dae29f4ba68..0000000000000000000000000000000000000000 --- a/src/main/resources/couchdb/LoggedIn.design.js +++ /dev/null @@ -1,27 +0,0 @@ -var designDoc = { - "_id": "_design/LoggedIn", - "language": "javascript", - "views": { - "all": { - "map": function (doc) { - if (doc.type === "logged_in"){ - emit(doc.user, doc); - } - } - }, - "by_last_activity_for_guests": { - "map": function (doc) { - if (doc.type === "logged_in" && doc.user.indexOf("Guest") === 0) { - emit(doc.timestamp || 0, {_rev: doc._rev}); - } - } - }, - "visited_sessions_by_user": { - "map": function (doc) { - if (doc.type === "logged_in") { - emit(doc.user, doc.visitedSessions); - } - } - } - } -}; diff --git a/src/main/resources/couchdb/Motd.design.js b/src/main/resources/couchdb/Motd.design.js index 8e932510dc5ee7fddcae328058b636d1ae7ca2ac..08fd577284cafb4b243300df7cd0f632635bbaa2 100644 --- a/src/main/resources/couchdb/Motd.design.js +++ b/src/main/resources/couchdb/Motd.design.js @@ -2,24 +2,32 @@ var designDoc = { "_id": "_design/Motd", "language": "javascript", "views": { + "by_id": { + "map": function (doc) { + if (doc.type === "Motd") { + emit(doc._id, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, "by_audience_for_global": { "map": function (doc) { - if (doc.type === "motd" && doc.audience !== "session") { + if (doc.type === "Motd" && doc.audience !== "ROOM") { emit(doc.audience, {_rev: doc._rev}); } } }, - "by_motdkey": { + "by_id": { "map": function (doc) { - if (doc.type === "motd") { - emit(doc.motdkey, {_rev: doc._rev}); + if (doc.type === "Motd") { + emit(doc.id, {_rev: doc._rev}); } } }, - "by_sessionkey": { + "by_roomid": { "map": function (doc) { - if (doc.type === "motd" && doc.audience === "session") { - emit(doc.sessionkey, {_rev: doc._rev}); + if (doc.type === "Motd" && doc.audience === "ROOM") { + emit(doc.roomId, {_rev: doc._rev}); } } } diff --git a/src/main/resources/couchdb/MotdList.design.js b/src/main/resources/couchdb/MotdList.design.js deleted file mode 100644 index e7ddbe7621d93c89cb4b5ea2784369756452afe6..0000000000000000000000000000000000000000 --- a/src/main/resources/couchdb/MotdList.design.js +++ /dev/null @@ -1,13 +0,0 @@ -var designDoc = { - "_id": "_design/MotdList", - "language": "javascript", - "views": { - "doc_by_username": { - "map": function (doc) { - if (doc.type === "motdlist") { - emit(doc.username, doc); - } - } - } - } -}; diff --git a/src/main/resources/couchdb/Room.design.js b/src/main/resources/couchdb/Room.design.js new file mode 100644 index 0000000000000000000000000000000000000000..1ccb77baff7989fb330d5514b1fbf82c0f4afdac --- /dev/null +++ b/src/main/resources/couchdb/Room.design.js @@ -0,0 +1,66 @@ +var designDoc = { + "_id": "_design/Room", + "language": "javascript", + "views": { + "by_id": { + "map": function (doc) { + if (doc.type === "Room") { + emit(doc._id, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_courseid": { + "map": function (doc) { + if (doc.type === "Room" && doc.courseId && !doc.poolProperties) { + emit(doc.courseId, {_rev: doc._rev}); + } + } + }, + "by_shortid": { + "map": function (doc) { + if (doc.type === "Room") { + emit(doc.shortId, {_rev: doc._rev}); + } + } + }, + "by_ownerid": { + "map": function (doc) { + if (doc.type === "Room") { + emit(doc.ownerId, {_rev: doc._rev}); + } + } + }, + "by_lastactivity_for_guests": { /* needs rewrite */ + "map": function (doc) { + if (doc.type === "Room" && !doc.poolProperties && doc.creator.indexOf("Guest") === 0) { + emit(doc.lastOwnerActivity || doc.creationTimestamp, {_rev: doc._rev}); + } + } + }, + "partial_by_pool_ownerid_name": { + "map": function (doc) { + if (doc.type === "Room") { + emit([!!doc.poolProperties, doc.ownerId, doc.name], { + abbreviation: doc.abbreviation, + shortId: doc.shortId, + locked: doc.locked, + courseType: doc.courseType, + creationTimestamp: doc.creationTimestamp + }); + } + } + }, + "partial_by_category_name_for_pool": { + "map": function (doc) { + if (doc.type === "Room" && doc.poolProperties) { + emit([doc.poolProperties.category, doc.name], { + name: doc.name, + shortId: doc.shortId, + poolProperties: doc.poolProperties + }); + } + } + } + } +}; diff --git a/src/main/resources/couchdb/Session.design.js b/src/main/resources/couchdb/Session.design.js deleted file mode 100644 index 790e3af6afe0829abf1c2d177a6baa94acb2d390..0000000000000000000000000000000000000000 --- a/src/main/resources/couchdb/Session.design.js +++ /dev/null @@ -1,52 +0,0 @@ -var designDoc = { - "_id": "_design/Session", - "language": "javascript", - "views": { - "by_courseid": { - "map": function (doc) { - if (doc.type === "session" && doc.courseId && doc.sessionType !== "public_pool") { - emit(doc.courseId, {_rev: doc._rev}); - } - } - }, - "by_keyword": { - "map": function (doc) { - if (doc.type === "session") { - emit(doc.keyword, {_rev: doc._rev}); - } - } - }, - "by_lastactivity_for_guests": { - "map": function (doc) { - if (doc.type === "session" && doc.sessionType !== "public_pool" && doc.creator.indexOf("Guest") === 0) { - emit(doc.lastOwnerActivity || doc.creationTime, {_rev: doc._rev}); - } - } - }, - "partial_by_sessiontype_creator_name": { - "map": function (doc) { - if (doc.type === "session") { - emit([doc.sessionType, doc.creator, doc.name], { - shortName: doc.shortName, - keyword: doc.keyword, - active: doc.active, - courseType: doc.courseType, - creationTime: doc.creationTime - }); - } - } - }, - "partial_by_subject_name_for_publicpool": { - "map": function (doc) { - if (doc.type === "session" && doc.sessiontype === "public_pool") { - emit([doc.ppSubject, doc.name], { - ppSubject: doc.ppSubject, - name: doc.name, - keyword: doc.keyword, - ppLevel: doc.ppLevel - }); - } - } - } - } -}; diff --git a/src/main/resources/couchdb/UserProfile.design.js b/src/main/resources/couchdb/UserProfile.design.js new file mode 100644 index 0000000000000000000000000000000000000000..ebf910a722f21122691235e2265128a8afcee10b --- /dev/null +++ b/src/main/resources/couchdb/UserProfile.design.js @@ -0,0 +1,35 @@ +var designDoc = { + "_id": "_design/UserProfile", + "language": "javascript", + "views": { + "by_id": { + "map": function (doc) { + if (doc.type === "UserProfile") { + emit(doc._id, {_rev: doc._rev}); + } + }, + "reduce": "_count" + }, + "by_creationtimestamp_for_inactive": { + "map": function (doc) { + if (doc.type === "UserProfile" && doc.authProvider === "ARSNOVA" && doc.account.activationKey) { + emit(doc.creationTimestamp, {_rev: doc._rev}); + } + } + }, + "by_authprovider_loginid": { + "map": function (doc) { + if (doc.type === "UserProfile") { + emit([doc.authProvider, doc.loginId], {_rev: doc._rev}); + } + } + }, + "by_loginid": { + "map": function (doc) { + if (doc.type === "UserProfile") { + emit(doc.loginId, {_rev: doc._rev}); + } + } + } + } +}; diff --git a/src/main/resources/couchdb/lerning_progress.design.js b/src/main/resources/couchdb/lerning_progress.design.js index 5ae5ba8449748e938a7646a1863eb6314acf512f..716c6f78d3880628021b815c07b3c758d3b2a8f7 100644 --- a/src/main/resources/couchdb/lerning_progress.design.js +++ b/src/main/resources/couchdb/lerning_progress.design.js @@ -5,7 +5,7 @@ var designDoc = { "question_value_achieved_for_user": { "comment": "This view returns the points users scored for answered questions.", "map": function (doc) { - if (doc.type === "skill_question_answer" && !doc.abstention) { + if (doc.type === "Answer" && !doc.abstention) { /* The 'questionValue' contains the points scored with this answer, * and this could be negative if a wrong answer was given. * However, we do not want negative values, so we set the lower bound to 0.*/ @@ -28,7 +28,7 @@ var designDoc = { * 1) On any single choice question, the value is the maximum of all possibleAnswer values. * 2) On a multiple choice question, we add up all positive values. */ var value = 0, answers = [], positiveAnswers = [], score = 0; - if (doc.type === "skill_question" && ["school", "flashcard"].indexOf(doc.questionType) === -1) { + if (doc.type === "Content" && ["school", "flashcard"].indexOf(doc.questionType) === -1) { if ("freetext" === doc.questionType && !doc.fixedAnswer) { return; } answers = doc.possibleAnswers.map(function (answer) { return answer.value || 0; }); /* find the maximum value */ diff --git a/src/main/resources/couchdb/statistics.design.js b/src/main/resources/couchdb/statistics.design.js index c9004fd5432a16842d0e1bb25870977aa286daf2..0e33e1499363d9b0160f0905fb1647a4d6c6290a 100644 --- a/src/main/resources/couchdb/statistics.design.js +++ b/src/main/resources/couchdb/statistics.design.js @@ -4,7 +4,7 @@ var designDoc = { "views": { "active_student_users": { "map": function (doc) { - if (doc.type === "skill_question_answer") { + if (doc.type === "Answer") { emit(doc.user, 1); } }, @@ -13,14 +13,14 @@ var designDoc = { "statistics": { "map": function (doc) { switch (doc.type) { - case "session": + case "Room": if (doc.active) { emit("openSessions", 1); } else { emit("closedSessions", 1); } break; - case "skill_question": + case "Content": if (doc.questionType === "flashcard") { emit("flashcards", 1); } else { @@ -34,13 +34,13 @@ var designDoc = { } } break; - case "skill_question_answer": + case "Answer": emit("answers", 1); break; - case "interposed_question": + case "Comment": emit ("interposedQuestions", 1); break; - case "log": + case "LogEntry": if (doc.event === "delete") { switch (doc.payload.type) { case "session": @@ -67,7 +67,7 @@ var designDoc = { }, "unique_session_creators": { "map": function (doc) { - if (doc.type === "session") { + if (doc.type === "Room") { emit(doc.creator, 1); } }, diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 3d024504cc27e6d4f02d7412c8f4d9a9a7766288..3195f1d88612682ef6c57f673c26bedc550fe578 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -11,7 +11,7 @@ <param-name>contextConfigLocation</param-name> <param-value> de.thm.arsnova.config.AppConfig - de.thm.arsnova.config.PersistanceConfig + de.thm.arsnova.config.PersistenceConfig de.thm.arsnova.config.SecurityConfig </param-value> </context-param> @@ -59,9 +59,6 @@ <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> - <listener> - <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> - </listener> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> @@ -79,6 +76,26 @@ <url-pattern>/*</url-pattern> </filter-mapping> + <filter> + <filter-name>maintenanceModeFilter</filter-name> + <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> + <async-supported>true</async-supported> + </filter> + <filter-mapping> + <filter-name>maintenanceModeFilter</filter-name> + <url-pattern>/*</url-pattern> + </filter-mapping> + + <filter> + <filter-name>v2ContentTypeOverrideFilter</filter-name> + <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> + <async-supported>true</async-supported> + </filter> + <filter-mapping> + <filter-name>v2ContentTypeOverrideFilter</filter-name> + <url-pattern>/v2/*</url-pattern> + </filter-mapping> + <session-config> <tracking-mode>COOKIE</tracking-mode> </session-config> diff --git a/src/site/markdown/development.md b/src/site/markdown/development.md index 20e72d0f27fd6be991541a0bc81142ab57a8fead..6c588b68a133eb0666ceee6315f2ece8b9aa1d90 100644 --- a/src/site/markdown/development.md +++ b/src/site/markdown/development.md @@ -64,5 +64,6 @@ The current build status for the master branch: ## Further Documentation +* [Roadmap](development/roadmap.md) * [Caching](development/caching.md) * [Event System](development/event-system.md) diff --git a/src/site/markdown/development/caching.md b/src/site/markdown/development/caching.md index eaa8330213fac226c0128cbbbf5fd648dfbd2b6b..ca4a74e71107b0f4091560bfc582e47edd8327b1 100644 --- a/src/site/markdown/development/caching.md +++ b/src/site/markdown/development/caching.md @@ -16,14 +16,14 @@ Caching should only be used with domain objects where the `hashCode` and `equals ```java @Cacheable(value = "notverycacheable", key = "#p0.concat('-').concat(#p1).concat('-').concat(#p2)") -public ResultObject notVeryCacheable(String sessionId, String questionVariant, String subject) { ... } +public ResultObject notVeryCacheable(String roomId, String questionVariant, String subject) { ... } ``` -Therefore, you should always work with domain objects like `Session`, `Content`, or even your own, newly defined objects: +Therefore, you should always work with domain objects like `Room`, `Content`, or even your own, newly defined objects: ```java @Cacheable("verycacheable") -public ResultObject veryCacheable(Session session) { ... } +public ResultObject veryCacheable(Room room) { ... } ``` Be aware though that you need to carefully choose the fields which should be part of the `equals`/`hashCode`: In case of CouchDB, for example, it is not a good idea to use a document's `rev` field. Every time a document is updated, it gets a new `rev` which will make it _unequal_ to all its previous versions, making cache updates using `@CachePut` impossible. @@ -46,14 +46,14 @@ Here is a list of all caches, their keys, and a short description. Cache name | Key | Description -----------|-----|------------ -`contentlists`| database id of session | Contains all contents for the specified session irrespective of their variant. -`lecturecontentlists` | database id of session | Contains all "lecture" variant contents for the specified session. -`preparationcontentlists` | database id of session | Contains all "preparation" variant contents for the specified session. -`flashcardcontentlists` | database id of session | Contains all "flashcard" variant contents for the specified session. +`contentlists`| database id of room | Contains all contents for the specified room irrespective of their variant. +`lecturecontentlists` | database id of room | Contains all "lecture" variant contents for the specified room. +`preparationcontentlists` | database id of room | Contains all "preparation" variant contents for the specified room. +`flashcardcontentlists` | database id of room | Contains all "flashcard" variant contents for the specified room. `contents` | `Content` entity | Contains single content objects. `contents` | database id of content | Although it shares the name of the previously mentioned cache, it is in essence a different cache because the keys are different. This means that the same `Content` object might be associated with two different keys. `answerlists`| database id of content | Contains single answer objects. -`score` | `Session` entity | Contains `CourseScore` objects to calculate the score values for the specified session. -`sessions` | keyword of session | Contains sessions identified by their keywords. -`sessions` | database id of session | Although it shares the name of the previously mentioned cache, it is in essence a different cache because the keys are different. This means that the same `Session` object might be associated with two different keys. +`score` | `Room` entity | Contains `CourseScore` objects to calculate the score values for the specified room. +`rooms` | keyword of room | Contains rooms identified by their keywords. +`rooms` | database id of room | Although it shares the name of the previously mentioned cache, it is in essence a different cache because the keys are different. This means that the same `Room` object might be associated with two different keys. `statistics` | -- | Contains a single, global statistics object. diff --git a/src/site/markdown/development/event-system.md b/src/site/markdown/development/event-system.md index 71132e71bbc0d34254e47be3f277bf7090ec352e..7bf677bcd1ea5091d09de8cceea97a43c5e14aca 100644 --- a/src/site/markdown/development/event-system.md +++ b/src/site/markdown/development/event-system.md @@ -11,18 +11,18 @@ A class is able to send events by implementing the `ApplicationEventPublisherAwa ```java publisher.publishEvent(theEvent); ``` -where `theEvent` is an object of type `ApplicationEvent`. For ARSnova, the base class `ArsovaEvent` should be used instead. All of ARSnova's internal events are subtypes of `ArsovaEvent`. +where `theEvent` is an object of type `ApplicationEvent`. For ARSnova, the base class `ArsnovaEvent` should be used instead. All of ARSnova's internal events are subtypes of `ArsnovaEvent`. _Note_: Events are sent and received on the same thread, i.e., it is a synchronous operation. ## How to receive events? -Events are received by implementing the `ApplicationListener<ArsovaEvent>` interface. The associated method gets passed in a `ArsovaEvent`, which is the base class of all of ARSnova's events. However, this type itself is not very useful. The real type can be revealed using double dispatch, which is the basis of the Visitor pattern. Therefore, the event should be forwarded to a class that implements the `ArsovaEvent` interface. This could be the same class that received the event. +Events are received by implementing the `ApplicationListener<ArsnovaEvent>` interface. The associated method gets passed in a `ArsnovaEvent`, which is the base class of all of ARSnova's events. However, this type itself is not very useful. The real type can be revealed using double dispatch, which is the basis of the Visitor pattern. Therefore, the event should be forwarded to a class that implements the `ArsnovaEvent` interface. This could be the same class that received the event. _Note_: If the class implementing the Visitor needs to have some of Spring's annotations on the event methods, like, for example, to cache some values using `@Cacheable`, the Listener and the Visitor must be different objects. ## How to create custom events? -Subclass either `ArsovaEvent` or `SessionEvent`. The former is for generic events that are not tied to a specific session, while the latter is for cases where the event only makes sense in the context of a session. +Subclass either `ArsnovaEvent` or `RoomEvent`. The former is for generic events that are not tied to a specific room, while the latter is for cases where the event only makes sense in the context of a room. diff --git a/src/site/markdown/development/roadmap.md b/src/site/markdown/development/roadmap.md new file mode 100644 index 0000000000000000000000000000000000000000..a547e8522c86d7ee59246c18fc12bf7f8b2cd4cb --- /dev/null +++ b/src/site/markdown/development/roadmap.md @@ -0,0 +1,123 @@ +# Roadmap + +## 4.0 + +* Remove API v2 compatibility layer + + +## 3.1 + +TBD + + +## 3.0 + +Version 3.0 is a rewrite of large parts of the code base with the goal of improving its maintainability. +A new streamlined REST API is developed while the legacy API is still supported through a compatibility layer. + + +### Beta 2 + +## Deployment & Operations + +* Implement a monitoring endpoint for Prometheus + + +## Documentation + +* Create/update developer documentation + * REST API + * Architecture + * Coding guidelines +* Update installation and upgrade guide + + +## QA + +* Review critical issues detected by static code analysis + + +### Beta 1 + +## General architecture + +* Implement data validation for entities +* Use better caching implementation (Ehcache, etc.) +* Review and complete caching (annotations, keys, etc.) +* Review and complete authorization handling +* Review event system and unify emitting of events for CRUD operations + * https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2 +* Review and improve error handling + * Exceptions + * HTTP responses + * Logging +* Use modern logging framework (SLF4J with Logback instead of Log4j) + + +## REST API + +* Adjust handling of non-persistent entities for API v3 +* Reimplement export and import + + +## QA + +* Increase code coverage for testing +* Implement performance tests (Gattling) +* Ensure Java 11 (LTS) compatibility +* Review blocker issues detected by static code analysis + + +### Alpha + +## General architecture + +* Minimize code duplication + * Use generic classes for entity related controllers, services and data repositories +* Prefer constructor injection for required dependencies + * Use setter injection for optional dependencies + * Avoid field injection (it's fine in test classes) +* Use Jackson's JsonViews to define property visibility for API and persistence +* Refactor entities + * Introduce abstract Entity class with common properties + * Improve nomenclature for class and property naming + * Avoid use-case specific names (such as "Peer Instruction" for multiple rounds) + * Session -> Room + * Lecture Question -> Content + * Audience Question -> Comment + * Learning Progress -> Score + * Question Type -> Content Format + * Do not reuse one entity type for multiple domain types + * `Question` -> split up into `Content`, `Comment` + * `Answer` -> split up into `Answer`, `AnswerStatistics` + * Use subclasses to implement different formats for Content and Answer + + +## Persistence + +* Abstract and split up persistence layer with help of Spring Data interfaces +* Get rid of JSON-lib (buggy and unmaintained) and use Jackson for all JSON (de-)serialization +* Use Ektorp instead of CouchDB4J (buggy and unmaintained) for CouchDB handling +* Implement view creation and data migration in Java (replace Setup Tool used for 2.x) +* Merge redundant database views +* Avoid emitting full docs and use `include_docs` instead + + +## REST API + +* Implement support for handling multiple API versions +* Implement API v3 + * Stateless: No more `/whoami` or `/myxyz` routes + * Use JWT instead of cookie-based sessions for authentication + * Use generic routes for CRUD + * Implement HTTP PATCH to update properties + * No more `/<entity>/disablexyz` + * Implement a `/<entity type>/find` endpoint + * Provide statistics with non-aggregated combinations of answer choices + * Implement customizable content groups + * They replace predefined content variants + * Relations are stored as part of a room + * They support auto sort (v2 behavior) and user defined ordering +* Implement API live migration layer v2 <-> v3 + * v2 controllers convert between v2 and v3 entity types + * Internally (service and persistence layer) only the v3 entity type is used diff --git a/src/test/java/de/thm/arsnova/config/TestAppConfig.java b/src/test/java/de/thm/arsnova/config/TestAppConfig.java index eea3d564fbee6a679fea09baf9971d1e63e38aa5..2d40e7060ae7a74078e5916defefef680a60c5ec 100644 --- a/src/test/java/de/thm/arsnova/config/TestAppConfig.java +++ b/src/test/java/de/thm/arsnova/config/TestAppConfig.java @@ -1,9 +1,10 @@ package de.thm.arsnova.config; -import de.thm.arsnova.persistance.UserRepository; -import de.thm.arsnova.services.StubUserService; +import de.thm.arsnova.persistence.UserRepository; +import de.thm.arsnova.service.StubUserService; import de.thm.arsnova.websocket.ArsnovaSocketioServer; import de.thm.arsnova.websocket.ArsnovaSocketioServerImpl; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.CustomScopeConfigurer; import org.springframework.cache.annotation.EnableCaching; @@ -16,6 +17,7 @@ import org.springframework.context.annotation.Profile; import org.springframework.context.annotation.PropertySource; import org.springframework.context.annotation.aspectj.EnableSpringConfigured; import org.springframework.context.support.SimpleThreadScope; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mock.web.MockServletContext; import org.springframework.test.context.ContextConfiguration; @@ -38,7 +40,8 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc; @EnableWebMvc @PropertySource( value = {"classpath:arsnova.properties.example", "file:/etc/arsnova/arsnova.properties"}, - ignoreResourceNotFound = true + ignoreResourceNotFound = true, + encoding = "UTF-8" ) @Profile("test") public class TestAppConfig { @@ -72,7 +75,10 @@ public class TestAppConfig { @Bean @Primary - public StubUserService stubUserService(UserRepository repository, JavaMailSender mailSender) { - return new StubUserService(repository, mailSender); + public StubUserService stubUserService( + UserRepository repository, + JavaMailSender mailSender, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + return new StubUserService(repository, mailSender, jackson2HttpMessageConverter); } } diff --git a/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java b/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java index 4801a85488368fff06279def60b4d0cc18230e65..9acd9adfecde08a8a17ded79f7a6624182cdfb74 100644 --- a/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java +++ b/src/test/java/de/thm/arsnova/config/TestPersistanceConfig.java @@ -1,6 +1,6 @@ package de.thm.arsnova.config; -import de.thm.arsnova.persistance.*; +import de.thm.arsnova.persistence.*; import org.mockito.Mockito; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -20,8 +20,8 @@ public class TestPersistanceConfig { } @Bean - public SessionRepository sessionRepository() { - return Mockito.mock(SessionRepository.class); + public RoomRepository sessionRepository() { + return Mockito.mock(RoomRepository.class); } @Bean @@ -40,18 +40,13 @@ public class TestPersistanceConfig { } @Bean - public MotdRepository motdRepository() { - return Mockito.mock(MotdRepository.class); + public AttachmentRepository attachmentRepository() { + return Mockito.mock(AttachmentRepository.class); } @Bean - public MotdListRepository motdListRepository() { - return Mockito.mock(MotdListRepository.class); - } - - @Bean - public VisitedSessionRepository visitedSessionRepository() { - return Mockito.mock(VisitedSessionRepository.class); + public MotdRepository motdRepository() { + return Mockito.mock(MotdRepository.class); } @Bean diff --git a/src/test/java/de/thm/arsnova/config/TestSecurityConfig.java b/src/test/java/de/thm/arsnova/config/TestSecurityConfig.java index f60f9bc2263de01e2d45276d9617982e0ecc2875..ee25ccee92c899a77fc67a755f67ff3a39ff6e19 100644 --- a/src/test/java/de/thm/arsnova/config/TestSecurityConfig.java +++ b/src/test/java/de/thm/arsnova/config/TestSecurityConfig.java @@ -46,21 +46,6 @@ public class TestSecurityConfig extends SecurityConfig { @Override protected void configure(HttpSecurity http) {} - @Override - protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.inMemoryAuthentication() - .withUser("ptsr00") - .password("secret") - .authorities("ROLE_USER") - ; - } - - @Bean - @Override - public AuthenticationManager authenticationManagerBean() throws Exception { - return super.authenticationManager(); - } - @Override @Bean public SessionRegistry sessionRegistry() { diff --git a/src/test/java/de/thm/arsnova/controller/AbstractControllerTest.java b/src/test/java/de/thm/arsnova/controller/AbstractControllerTest.java index 7d7993da484ca106d4f443cd2d7f7ba2108dca03..271b36b4aec48ca088538f8611e33562f1270856 100644 --- a/src/test/java/de/thm/arsnova/controller/AbstractControllerTest.java +++ b/src/test/java/de/thm/arsnova/controller/AbstractControllerTest.java @@ -21,7 +21,7 @@ import de.thm.arsnova.config.AppConfig; import de.thm.arsnova.config.TestAppConfig; import de.thm.arsnova.config.TestPersistanceConfig; import de.thm.arsnova.config.TestSecurityConfig; -import de.thm.arsnova.services.StubUserService; +import de.thm.arsnova.service.StubUserService; import org.junit.After; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; diff --git a/src/test/java/de/thm/arsnova/controller/LoginControllerTest.java b/src/test/java/de/thm/arsnova/controller/v2/AuthenticationControllerTest.java similarity index 71% rename from src/test/java/de/thm/arsnova/controller/LoginControllerTest.java rename to src/test/java/de/thm/arsnova/controller/v2/AuthenticationControllerTest.java index 99c2bfd83915bf036561f4c1559dc5e51b368804..7a7681bdc8dbff17749525dab2954d6016ecb598 100644 --- a/src/test/java/de/thm/arsnova/controller/LoginControllerTest.java +++ b/src/test/java/de/thm/arsnova/controller/v2/AuthenticationControllerTest.java @@ -15,9 +15,10 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.controller; +package de.thm.arsnova.controller.v2; -import de.thm.arsnova.services.StubUserService; +import de.thm.arsnova.controller.AbstractControllerTest; +import de.thm.arsnova.service.StubUserService; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -31,10 +32,11 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; -public class LoginControllerTest extends AbstractControllerTest { +public class AuthenticationControllerTest extends AbstractControllerTest { @Autowired private StubUserService userService; @@ -50,25 +52,32 @@ public class LoginControllerTest extends AbstractControllerTest { } @Test + @Ignore("Mockup needed for DB/Auth") public void testGuestLogin() throws Exception { mockMvc.perform( - get("/doLogin") + get("/v2/auth/doLogin") .param("type", "guest") ).andExpect(status().isOk()); } @Test + @Ignore("Mockup needed for UserService") public void testReuseGuestLogin() throws Exception { mockMvc.perform( - get("/doLogin") - .param("type", "guest").param("user","Guest1234567890") + get("/v2/auth/doLogin") + .param("type", "guest") + ).andExpect(status().isOk()); + final Authentication auth1 = SecurityContextHolder.getContext().getAuthentication(); + cleanup(); + mockMvc.perform( + get("/v2/auth/doLogin") + .param("type", "guest").param("user", auth1.getName()) ).andExpect(status().isOk()); - final Authentication auth = SecurityContextHolder.getContext() - .getAuthentication(); - assertEquals(auth.getClass(), UsernamePasswordAuthenticationToken.class); - assertEquals("Guest1234567890", auth.getName()); - + final Authentication auth2 = SecurityContextHolder.getContext().getAuthentication(); + assertEquals(auth2.getClass(), UsernamePasswordAuthenticationToken.class); + assertNotSame(auth1, auth2); + assertEquals(auth1, auth2); } @Test @@ -76,17 +85,10 @@ public class LoginControllerTest extends AbstractControllerTest { public void testUser() throws Exception { userService.setUserAuthenticated(true); - mockMvc.perform(get("/whoami").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/v2/auth/whoami").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) .andExpect(jsonPath("$.username").value("ptsr00")) .andExpect(jsonPath("$.type").value("ldap")); } - - @Test - public void testLogoutWithoutRedirect() throws Exception { - mockMvc.perform(get("/logout")) - .andExpect(status().is3xxRedirection()) - .andExpect(redirectedUrl("/")); - } } diff --git a/src/test/java/de/thm/arsnova/controller/CourseControllerTest.java b/src/test/java/de/thm/arsnova/controller/v2/CourseControllerTest.java similarity index 88% rename from src/test/java/de/thm/arsnova/controller/CourseControllerTest.java rename to src/test/java/de/thm/arsnova/controller/v2/CourseControllerTest.java index dd3d359f0bc6e45a29d2b193fbaa9b6e89fc32b1..01765d04769164b2d27a66ea079e546581e8153a 100644 --- a/src/test/java/de/thm/arsnova/controller/CourseControllerTest.java +++ b/src/test/java/de/thm/arsnova/controller/v2/CourseControllerTest.java @@ -15,10 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.controller; +package de.thm.arsnova.controller.v2; import de.thm.arsnova.connector.client.ConnectorClient; -import de.thm.arsnova.services.StubUserService; +import de.thm.arsnova.controller.AbstractControllerTest; +import de.thm.arsnova.service.StubUserService; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; @@ -57,7 +58,7 @@ public class CourseControllerTest extends AbstractControllerTest { public void testShouldIndicateNotImplementedIfInactiveClient() throws Exception { setAuthenticated(true, "ptsr00"); - mockMvc.perform(get("/mycourses").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/v2/mycourses").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isNotImplemented()); } @@ -65,7 +66,7 @@ public class CourseControllerTest extends AbstractControllerTest { public void testShouldNotReturnCurrentUsersCoursesIfUnauthorized() throws Exception { setAuthenticated(false, "nobody"); - mockMvc.perform(get("/mycourses").accept(MediaType.APPLICATION_JSON)) + mockMvc.perform(get("/v2/mycourses").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isUnauthorized()); } } diff --git a/src/test/java/de/thm/arsnova/entities/ContentTest.java b/src/test/java/de/thm/arsnova/model/ContentTest.java similarity index 81% rename from src/test/java/de/thm/arsnova/entities/ContentTest.java rename to src/test/java/de/thm/arsnova/model/ContentTest.java index fa3e4dc702c500e31f4a8ed3b6357a5f87077225..26a14227dc61dd67883eb21d874953173b48bfe4 100644 --- a/src/test/java/de/thm/arsnova/entities/ContentTest.java +++ b/src/test/java/de/thm/arsnova/model/ContentTest.java @@ -15,8 +15,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; +import de.thm.arsnova.model.migration.v2.Answer; +import de.thm.arsnova.model.migration.v2.AnswerOption; +import de.thm.arsnova.model.migration.v2.Content; import org.junit.Test; import java.util.ArrayList; @@ -28,17 +31,17 @@ public class ContentTest { @SuppressWarnings("serial") @Test public void shouldComputeBasedOnCorrectAnswerWithExactMatch() { - final PossibleAnswer p1 = new PossibleAnswer(); + final AnswerOption p1 = new AnswerOption(); p1.setText("Yes"); p1.setCorrect(true); p1.setValue(10); - final PossibleAnswer p2 = new PossibleAnswer(); + final AnswerOption p2 = new AnswerOption(); p2.setText("No"); p2.setCorrect(false); p2.setValue(-10); Content q = new Content(); q.setQuestionType("yesno"); - q.setPossibleAnswers(new ArrayList<PossibleAnswer>() {{ + q.setPossibleAnswers(new ArrayList<AnswerOption>() {{ add(p1); add(p2); }}); @@ -54,17 +57,17 @@ public class ContentTest { @SuppressWarnings("serial") @Test public void shouldEqualAbstentionToZeroValue() { - final PossibleAnswer p1 = new PossibleAnswer(); + final AnswerOption p1 = new AnswerOption(); p1.setText("Yes"); p1.setCorrect(true); p1.setValue(10); - final PossibleAnswer p2 = new PossibleAnswer(); + final AnswerOption p2 = new AnswerOption(); p2.setText("No"); p2.setCorrect(false); p2.setValue(-10); Content q = new Content(); q.setAbstention(true); - q.setPossibleAnswers(new ArrayList<PossibleAnswer>() {{ + q.setPossibleAnswers(new ArrayList<AnswerOption>() {{ add(p1); add(p2); }}); @@ -77,21 +80,21 @@ public class ContentTest { @SuppressWarnings("serial") @Test public void shouldCalculateMultipleChoiceAnswers() { - final PossibleAnswer p1 = new PossibleAnswer(); + final AnswerOption p1 = new AnswerOption(); p1.setText("Yes"); p1.setCorrect(true); p1.setValue(10); - final PossibleAnswer p2 = new PossibleAnswer(); + final AnswerOption p2 = new AnswerOption(); p2.setText("No"); p2.setCorrect(false); p2.setValue(-10); - final PossibleAnswer p3 = new PossibleAnswer(); + final AnswerOption p3 = new AnswerOption(); p3.setText("Maybe"); p3.setCorrect(true); p3.setValue(10); Content q = new Content(); q.setQuestionType("mc"); - q.setPossibleAnswers(new ArrayList<PossibleAnswer>() {{ + q.setPossibleAnswers(new ArrayList<AnswerOption>() {{ add(p1); add(p2); add(p3); @@ -118,25 +121,25 @@ public class ContentTest { @SuppressWarnings("serial") @Test public void shouldCalculatePictureQuestionAnswers() { - final PossibleAnswer p1 = new PossibleAnswer(); + final AnswerOption p1 = new AnswerOption(); p1.setText("0;0"); p1.setCorrect(true); p1.setValue(10); - final PossibleAnswer p2 = new PossibleAnswer(); + final AnswerOption p2 = new AnswerOption(); p2.setText("0;1"); p2.setCorrect(false); p2.setValue(-10); - final PossibleAnswer p3 = new PossibleAnswer(); + final AnswerOption p3 = new AnswerOption(); p3.setText("1;0"); p3.setCorrect(true); p3.setValue(10); - final PossibleAnswer p4 = new PossibleAnswer(); + final AnswerOption p4 = new AnswerOption(); p4.setText("1;1"); p4.setCorrect(true); p4.setValue(10); Content q = new Content(); q.setQuestionType("grid"); - q.setPossibleAnswers(new ArrayList<PossibleAnswer>() {{ + q.setPossibleAnswers(new ArrayList<AnswerOption>() {{ add(p1); add(p2); add(p3); diff --git a/src/test/java/de/thm/arsnova/model/EntityTest.java b/src/test/java/de/thm/arsnova/model/EntityTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d5edc7e761db8676d477252e17b0cc3138000226 --- /dev/null +++ b/src/test/java/de/thm/arsnova/model/EntityTest.java @@ -0,0 +1,108 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model; + +import org.junit.Test; +import org.springframework.core.style.ToStringCreator; + +import java.util.Date; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * Tests {@link Entity}'s overrides for {@link Object#hashCode()}, {@link Object#equals(Object)}, and + * {@link Object#toString()}. + * + * @author Daniel Gerhardt + */ +public class EntityTest { + class SomeEntity extends Entity { + private String testA; + + public SomeEntity(String id, String rev, Date creationTimestamp, Date updateTimestamp, String testA) { + this.id = id; + this.rev = rev; + this.creationTimestamp = creationTimestamp; + this.updateTimestamp = updateTimestamp; + this.testA = testA; + } + + @Override + public int hashCode() { + return hashCode(super.hashCode(), testA); + } + + @Override + public ToStringCreator buildToString() { + return super.buildToString().append("testA", testA); + } + } + + class AnotherEntity extends SomeEntity { + private String testB; + + public AnotherEntity(String id, String rev, Date creationTimestamp, Date updateTimestamp, String testA, String testB) { + super(id, rev, creationTimestamp, updateTimestamp, testA); + this.testB = testB; + } + + @Override + public int hashCode() { + return hashCode(super.hashCode(), testB); + } + + @Override + public ToStringCreator buildToString() { + return super.buildToString().append("testB", testB); + } + } + + @Test + public void testHashCode() { + SomeEntity entity1 = new SomeEntity("id", "rev", new Date(0), new Date(0), "test"); + SomeEntity entity2 = new SomeEntity("id", "rev", new Date(0), new Date(0), "test"); + SomeEntity entity3 = new SomeEntity("wrongId", "rev", new Date(0), new Date(0), "test"); + assertEquals(entity1.hashCode(), entity2.hashCode()); + assertNotEquals(entity1.hashCode(), entity3.hashCode()); + AnotherEntity entity4 = new AnotherEntity("id", "rev", new Date(0), new Date(0), "someTest", "anotherTest"); + AnotherEntity entity5 = new AnotherEntity("id", "rev", new Date(0), new Date(0), "someTest", "anotherTest"); + AnotherEntity entity6 = new AnotherEntity("id", "rev", new Date(0), new Date(0), "someTest", "wrong"); + assertEquals(entity4.hashCode(), entity5.hashCode()); + assertNotEquals(entity4.hashCode(), entity6.hashCode()); + } + + @Test + public void testEquals() { + SomeEntity entity1 = new SomeEntity("id", "rev", new Date(0), new Date(0), "test"); + SomeEntity entity2 = new SomeEntity("id", "rev", new Date(0), new Date(0), "test"); + SomeEntity entity3 = new SomeEntity("wrongId", "rev", new Date(0), new Date(0), "test"); + assertEquals(entity1, entity2); + assertNotEquals(entity1, entity3); + } + + @Test + public void testToString() { + SomeEntity entity1 = new SomeEntity("id", "rev", new Date(0), new Date(0), "test"); + assertThat(entity1.toString(), startsWith("[EntityTest.SomeEntity")); + assertThat(entity1.toString(), endsWith("testA = 'test']")); + AnotherEntity entity2 = new AnotherEntity("id", "rev", new Date(0), new Date(0), "someTest", "anotherTest"); + assertThat(entity2.toString(), startsWith("[EntityTest.AnotherEntity")); + assertThat(entity2.toString(), endsWith("testA = 'someTest', testB = 'anotherTest']")); + } +} diff --git a/src/test/java/de/thm/arsnova/entities/FeedbackTest.java b/src/test/java/de/thm/arsnova/model/FeedbackTest.java similarity index 98% rename from src/test/java/de/thm/arsnova/entities/FeedbackTest.java rename to src/test/java/de/thm/arsnova/model/FeedbackTest.java index e0f3c1b6761b0354cb37254dbb0be8873b70b108..01ddd3b1fd9b28ffc5a0ddb448cf72f7a16026b3 100644 --- a/src/test/java/de/thm/arsnova/entities/FeedbackTest.java +++ b/src/test/java/de/thm/arsnova/model/FeedbackTest.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.entities; +package de.thm.arsnova.model; import org.junit.Test; diff --git a/src/test/java/de/thm/arsnova/model/TestClient.java b/src/test/java/de/thm/arsnova/model/TestClient.java new file mode 100644 index 0000000000000000000000000000000000000000..ffb7c9a525aea7158afcf71ddccf63103925a24b --- /dev/null +++ b/src/test/java/de/thm/arsnova/model/TestClient.java @@ -0,0 +1,38 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model; + +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import org.springframework.security.core.GrantedAuthority; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class TestClient extends ClientAuthentication { + private static final long serialVersionUID = 1L; + private final Set<GrantedAuthority> grantedAuthorities; + + public TestClient(String username) { + super(); + grantedAuthorities = new HashSet<>(); + setId(UUID.randomUUID().toString()); + setUsername(username); + setAuthProvider(UserProfile.AuthProvider.ARSNOVA); + } +} diff --git a/src/test/java/de/thm/arsnova/model/migration/FromV2MigratorTest.java b/src/test/java/de/thm/arsnova/model/migration/FromV2MigratorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fec6f4954b14cf12c519d7168a88b9faf34562c1 --- /dev/null +++ b/src/test/java/de/thm/arsnova/model/migration/FromV2MigratorTest.java @@ -0,0 +1,99 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model.migration; + +import de.thm.arsnova.config.AppConfig; +import de.thm.arsnova.config.TestAppConfig; +import de.thm.arsnova.config.TestPersistanceConfig; +import de.thm.arsnova.config.TestSecurityConfig; +import de.thm.arsnova.model.ChoiceAnswer; +import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.migration.v2.Answer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Daniel Gerhardt + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(classes = {AppConfig.class, TestAppConfig.class, TestPersistanceConfig.class, TestSecurityConfig.class}) +@ActiveProfiles("test") +public class FromV2MigratorTest { + private static final String ANSWER_ID = "answerId"; + private static final String CONTENT_ID = "contentId"; + private static final String ROOM_ID = "roomId"; + private static final Content.Format FORMAT = Content.Format.CHOICE; + private static final String OPTION1_LABEL = "option1"; + private static final String OPTION2_LABEL = "option2"; + private static final String OPTION3_LABEL = "option3"; + private static final int ROUND = 1; + + @Autowired + private FromV2Migrator fromV2Migrator; + + @Test + public void testMigrateAnswerMultipleResponse() { + Answer answerV2 = new Answer(); + answerV2.setId(ANSWER_ID); + answerV2.setQuestionId(CONTENT_ID); + answerV2.setSessionId(ROOM_ID); + answerV2.setPiRound(ROUND); + answerV2.setAnswerText("0,1,1"); + + List<ChoiceQuestionContent.AnswerOption> options = new ArrayList<>(); + ChoiceQuestionContent.AnswerOption option1 = new ChoiceQuestionContent.AnswerOption(); + option1.setLabel(OPTION1_LABEL); + options.add(option1); + ChoiceQuestionContent.AnswerOption option2 = new ChoiceQuestionContent.AnswerOption(); + option2.setLabel(OPTION2_LABEL); + options.add(option2); + ChoiceQuestionContent.AnswerOption option3 = new ChoiceQuestionContent.AnswerOption(); + option3.setLabel(OPTION3_LABEL); + options.add(option3); + + ChoiceQuestionContent content = new ChoiceQuestionContent(); + content.setFormat(FORMAT); + content.setRoomId(ROOM_ID); + content.setMultiple(true); + content.setOptions(options); + + List<Integer> selectedChoices = new ArrayList<>(); + selectedChoices.add(1); + selectedChoices.add(2); + + ChoiceAnswer answerV3 = (ChoiceAnswer) fromV2Migrator.migrate(answerV2, content); + + assertEquals(ANSWER_ID, answerV3.getId()); + assertEquals(CONTENT_ID, answerV3.getContentId()); + assertEquals(ROOM_ID, answerV3.getRoomId()); + assertEquals(ROUND, answerV3.getRound()); + assertEquals(selectedChoices, answerV3.getSelectedChoiceIndexes()); + } +} diff --git a/src/test/java/de/thm/arsnova/model/migration/ToV2MigratorTest.java b/src/test/java/de/thm/arsnova/model/migration/ToV2MigratorTest.java new file mode 100644 index 0000000000000000000000000000000000000000..d52d3eb0ce013c19764324ededd5571018127a14 --- /dev/null +++ b/src/test/java/de/thm/arsnova/model/migration/ToV2MigratorTest.java @@ -0,0 +1,147 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.model.migration; + +import de.thm.arsnova.config.AppConfig; +import de.thm.arsnova.config.TestAppConfig; +import de.thm.arsnova.config.TestPersistanceConfig; +import de.thm.arsnova.config.TestSecurityConfig; +import de.thm.arsnova.model.AnswerStatistics; +import de.thm.arsnova.model.ChoiceAnswer; +import de.thm.arsnova.model.ChoiceQuestionContent; +import de.thm.arsnova.model.Content; +import de.thm.arsnova.model.migration.v2.Answer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.Assert.assertEquals; + +/** + * @author Daniel Gerhardt + */ +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(classes = {AppConfig.class, TestAppConfig.class, TestPersistanceConfig.class, TestSecurityConfig.class}) +@ActiveProfiles("test") +public class ToV2MigratorTest { + private static final String ANSWER_ID = "answerId"; + private static final String CONTENT_ID = "contentId"; + private static final String ROOM_ID = "roomId"; + private static final String CREATOR_ID = "creatorId"; + private static final String OPTION1_LABEL = "option1"; + private static final String OPTION2_LABEL = "option2"; + private static final String OPTION3_LABEL = "option3"; + private static final String OPTION4_LABEL = "option4"; + private static final int ROUND = 1; + private static final int ABSTENTION_COUNT = 7; + private static final List<Integer> ANSWER_COUNTS = + Collections.unmodifiableList(Arrays.asList(new Integer[] {3, 2, 4, 1})); + + @Autowired + private ToV2Migrator toV2Migrator; + + @Test + public void testMigrateAnswerMultipleResponse() { + final List<ChoiceQuestionContent.AnswerOption> options = new ArrayList<>(); + final ChoiceQuestionContent.AnswerOption option1 = new ChoiceQuestionContent.AnswerOption(); + option1.setLabel(OPTION1_LABEL); + options.add(option1); + ChoiceQuestionContent.AnswerOption option2 = new ChoiceQuestionContent.AnswerOption(); + option2.setLabel(OPTION2_LABEL); + options.add(option2); + ChoiceQuestionContent.AnswerOption option3 = new ChoiceQuestionContent.AnswerOption(); + option3.setLabel(OPTION3_LABEL); + options.add(option3); + + final ChoiceQuestionContent contentV3 = new ChoiceQuestionContent(); + contentV3.setFormat(Content.Format.CHOICE); + contentV3.setMultiple(true); + contentV3.setRoomId(ROOM_ID); + contentV3.setOptions(options); + + final List<Integer> selectedChoices = new ArrayList<>(); + selectedChoices.add(1); + selectedChoices.add(2); + + final ChoiceAnswer answerV3 = new ChoiceAnswer(); + answerV3.setId(ANSWER_ID); + answerV3.setCreatorId(CREATOR_ID); + answerV3.setRoomId(ROOM_ID); + answerV3.setContentId(CONTENT_ID); + answerV3.setRound(ROUND); + answerV3.setSelectedChoiceIndexes(selectedChoices); + + final Answer answerV2 = toV2Migrator.migrate(answerV3, contentV3); + + assertEquals(ANSWER_ID, answerV2.getId()); + assertEquals(CONTENT_ID, answerV2.getQuestionId()); + assertEquals(ROOM_ID, answerV2.getSessionId()); + assertEquals(ROUND, answerV2.getPiRound()); + assertEquals("0,1,1", answerV2.getAnswerText()); + } + + @Test + public void testMigrateAnswerStatisticsSingleChoice() { + final AnswerStatistics statsV3 = new AnswerStatistics(); + final AnswerStatistics.RoundStatistics roundStatsV3 = new AnswerStatistics.RoundStatistics(); + roundStatsV3.setRound(ROUND); + roundStatsV3.setIndependentCounts(ANSWER_COUNTS); + roundStatsV3.setAbstentionCount(7); + statsV3.setRoundStatistics(Collections.singletonList(roundStatsV3)); + + final ChoiceQuestionContent.AnswerOption option1 = new ChoiceQuestionContent.AnswerOption(); + option1.setLabel(OPTION1_LABEL); + ChoiceQuestionContent.AnswerOption option2 = new ChoiceQuestionContent.AnswerOption(); + option2.setLabel(OPTION2_LABEL); + ChoiceQuestionContent.AnswerOption option3 = new ChoiceQuestionContent.AnswerOption(); + option3.setLabel(OPTION3_LABEL); + ChoiceQuestionContent.AnswerOption option4 = new ChoiceQuestionContent.AnswerOption(); + option3.setLabel(OPTION4_LABEL); + final List<ChoiceQuestionContent.AnswerOption> options = Arrays.asList(new ChoiceQuestionContent.AnswerOption[] { + option1, option2, option3, option4 + }); + + final ChoiceQuestionContent contentV3 = new ChoiceQuestionContent(); + contentV3.getState().setRound(ROUND); + contentV3.setOptions(options); + contentV3.setAbstentionsAllowed(true); + + final List<Answer> statsV2 = toV2Migrator.migrate(statsV3, contentV3, ROUND); + + final Answer abstentionStatsV2 = statsV2.get(0); + assertEquals(ABSTENTION_COUNT, abstentionStatsV2.getAnswerCount()); + assertEquals(ABSTENTION_COUNT, abstentionStatsV2.getAbstentionCount()); + + for (int i = 0; i < ANSWER_COUNTS.size(); i++) { + Answer answerStatsV2 = statsV2.get(i + 1); + assertEquals(ANSWER_COUNTS.get(i).intValue(), answerStatsV2.getAnswerCount()); + assertEquals(ABSTENTION_COUNT, answerStatsV2.getAbstentionCount()); + assertEquals(options.get(i).getLabel(), answerStatsV2.getAnswerText()); + } + } +} diff --git a/src/test/java/de/thm/arsnova/service/DefaultEntityServiceImplTest.java b/src/test/java/de/thm/arsnova/service/DefaultEntityServiceImplTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2a8ccbecc4890f0c71ea4951e215eedb6750f217 --- /dev/null +++ b/src/test/java/de/thm/arsnova/service/DefaultEntityServiceImplTest.java @@ -0,0 +1,125 @@ +package de.thm.arsnova.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.thm.arsnova.config.AppConfig; +import de.thm.arsnova.config.TestAppConfig; +import de.thm.arsnova.config.TestPersistanceConfig; +import de.thm.arsnova.config.TestSecurityConfig; +import de.thm.arsnova.model.Room; +import de.thm.arsnova.persistence.RoomRepository; +import de.thm.arsnova.test.context.support.WithMockUser; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.context.web.WebAppConfiguration; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.AdditionalAnswers.*; +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + + +@RunWith(SpringJUnit4ClassRunner.class) +@WebAppConfiguration +@ContextConfiguration(classes = {AppConfig.class, TestAppConfig.class, TestPersistanceConfig.class, TestSecurityConfig.class}) +@ActiveProfiles("test") +public class DefaultEntityServiceImplTest { + @Autowired + @Qualifier("defaultJsonMessageConverter") + private MappingJackson2HttpMessageConverter jackson2HttpMessageConverter; + + @Autowired + private RoomRepository roomRepository; + + @Test + @WithMockUser("TestUser") + public void testPatch() throws IOException { + final ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper(); + final DefaultEntityServiceImpl<Room> entityService = new DefaultEntityServiceImpl<>(Room.class, roomRepository, objectMapper); + + when(roomRepository.save(any(Room.class))).then(returnsFirstArg()); + + final String originalId = "d8833f0d78964a9487ded02ba2dfbbad"; + final String originalName = "Test Room"; + final String originalOwnerId = "TestUser"; + final boolean originalActive = true; + final Room room = new Room(); + room.setId(originalId); + room.setName(originalName); + room.setClosed(originalActive); + room.setOwnerId(originalOwnerId); + + final String patchedName = "Patched Room"; + final boolean patchedActive = false; + final Map<String, Object> patchedValues = new HashMap<>(); + patchedValues.put("name", patchedName); + patchedValues.put("closed", patchedActive); + patchedValues.put("ownerId", "Should not be changeable."); + + entityService.patch(room, patchedValues); + + assertEquals(originalId, room.getId()); + assertEquals(patchedName, room.getName()); + assertEquals(patchedActive, room.isClosed()); + assertEquals(originalOwnerId, room.getOwnerId()); + } + + @Test + @WithMockUser("TestUser") + public void testPatchWithList() throws IOException { + final ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper(); + final DefaultEntityServiceImpl<Room> entityService = new DefaultEntityServiceImpl<>(Room.class, roomRepository, objectMapper); + + when(roomRepository.save(any(Room.class))).then(returnsFirstArg()); + + List<Room> sessions = new ArrayList<>(); + final String originalId1 = "d8833f0d78964a9487ded02ba2dfbbad"; + final String originalName1 = "Test Room 1"; + final String originalOwnerId1 = "TestUser"; + final boolean originalClosed1 = true; + final Room room1 = new Room(); + room1.setId(originalId1); + room1.setName(originalName1); + room1.setClosed(originalClosed1); + room1.setOwnerId(originalOwnerId1); + sessions.add(room1); + final String originalId2 = "3dc8cbff05da49d5980f6c001a6ea867"; + final String originalName2 = "Test Room 2"; + final String originalOwnerId2 = "TestUser"; + final boolean originalClosed2 = true; + final Room room2 = new Room(); + room2.setId(originalId2); + room2.setName(originalName2); + room2.setClosed(originalClosed2); + room2.setOwnerId(originalOwnerId2); + sessions.add(room2); + + final String patchedName = "Patched Room"; + final boolean patchedClosed = false; + final Map<String, Object> patchedValues = new HashMap<>(); + patchedValues.put("name", patchedName); + patchedValues.put("closed", patchedClosed); + patchedValues.put("ownerId", "Should not be changeable."); + + entityService.patch(sessions, patchedValues); + + assertEquals(originalId1, room1.getId()); + assertEquals(patchedName, room1.getName()); + assertEquals(patchedClosed, room1.isClosed()); + assertEquals(originalOwnerId1, room1.getOwnerId()); + assertEquals(originalId2, room2.getId()); + assertEquals(patchedName, room2.getName()); + assertEquals(patchedClosed, room2.isClosed()); + assertEquals(originalOwnerId2, room2.getOwnerId()); + } +} diff --git a/src/test/java/de/thm/arsnova/service/StubUserService.java b/src/test/java/de/thm/arsnova/service/StubUserService.java new file mode 100644 index 0000000000000000000000000000000000000000..a08514848086c4bd2ca5fac35b572a364563f27b --- /dev/null +++ b/src/test/java/de/thm/arsnova/service/StubUserService.java @@ -0,0 +1,72 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team and Contributors + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.service; + +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.persistence.UserRepository; +import de.thm.arsnova.security.User; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class StubUserService extends UserServiceImpl { + private final Set<GrantedAuthority> grantedAuthorities; + private ClientAuthentication stubUser = null; + + public StubUserService( + UserRepository repository, + JavaMailSender mailSender, + @Qualifier("defaultJsonMessageConverter") MappingJackson2HttpMessageConverter jackson2HttpMessageConverter) { + super(repository, mailSender, jackson2HttpMessageConverter); + grantedAuthorities = new HashSet<>(); + grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER")); + } + + public void setUserAuthenticated(boolean isAuthenticated) { + this.setUserAuthenticated(isAuthenticated, "ptsr00"); + } + + public void setUserAuthenticated(boolean isAuthenticated, String username) { + if (isAuthenticated) { + UserProfile userProfile = new UserProfile(UserProfile.AuthProvider.ARSNOVA, username); + userProfile.setId(UUID.randomUUID().toString()); + User user = new User(userProfile, grantedAuthorities); + stubUser = new ClientAuthentication(user); + } else { + stubUser = null; + } + } + + public void useAnonymousUser() { + stubUser = new ClientAuthentication(new AnonymousAuthenticationToken(UUID.randomUUID().toString(), "anonymous", Collections.emptyList())); + } + + @Override + public ClientAuthentication getCurrentUser() { + return stubUser; + } +} diff --git a/src/test/java/de/thm/arsnova/services/UserServiceTest.java b/src/test/java/de/thm/arsnova/service/UserServiceTest.java similarity index 76% rename from src/test/java/de/thm/arsnova/services/UserServiceTest.java rename to src/test/java/de/thm/arsnova/service/UserServiceTest.java index f609cbcbd0bfb3c496d5195c17382c2bad541569..6794fa424733523bb133d1c2d74ad2dca1bad72d 100644 --- a/src/test/java/de/thm/arsnova/services/UserServiceTest.java +++ b/src/test/java/de/thm/arsnova/service/UserServiceTest.java @@ -15,22 +15,22 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services; +package de.thm.arsnova.service; import de.thm.arsnova.config.AppConfig; import de.thm.arsnova.config.TestAppConfig; import de.thm.arsnova.config.TestPersistanceConfig; import de.thm.arsnova.config.TestSecurityConfig; -import de.thm.arsnova.entities.User; -import org.jasig.cas.client.authentication.AttributePrincipalImpl; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.security.User; +import de.thm.arsnova.security.pac4j.OAuthToken; import org.junit.Test; import org.junit.runner.RunWith; -import org.pac4j.oauth.profile.JsonHelper; import org.pac4j.oauth.profile.google2.Google2Email; import org.pac4j.oauth.profile.google2.Google2Profile; import org.pac4j.oauth.profile.google2.Google2ProfileDefinition; import org.springframework.security.authentication.AnonymousAuthenticationToken; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.test.context.ActiveProfiles; @@ -44,6 +44,7 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.UUID; @@ -57,13 +58,13 @@ import static org.junit.Assert.assertEquals; @ActiveProfiles("test") public class UserServiceTest { - private static final ConcurrentHashMap<UUID, User> socketid2user = new ConcurrentHashMap<>(); + private static final ConcurrentHashMap<UUID, ClientAuthentication> socketid2user = new ConcurrentHashMap<>(); private static final ConcurrentHashMap<String, String> user2session = new ConcurrentHashMap<>(); @Test public void testSocket2UserPersistence() throws IOException, ClassNotFoundException { - socketid2user.put(UUID.randomUUID(), new User(new UsernamePasswordAuthenticationToken("ptsr00", UUID.randomUUID()))); - socketid2user.put(UUID.randomUUID(), new User(new AttributePrincipalImpl("ptstr0"))); + //socketid2user.put(UUID.randomUUID(), new ClientAuthentication(new UsernamePasswordAuthenticationToken("ptsr00", UUID.randomUUID()))); + //socketid2user.put(UUID.randomUUID(), new ClientAuthentication(new AttributePrincipalImpl("ptstr0"))); Google2Email email = new Google2Email(); email.setEmail("mail@host.com"); @@ -72,18 +73,22 @@ public class UserServiceTest { Google2Profile profile = new Google2Profile(); profile.addAttribute(Google2ProfileDefinition.DISPLAY_NAME, "ptsr00"); profile.addAttribute(Google2ProfileDefinition.EMAILS, emails); + UserProfile userProfile = new UserProfile(UserProfile.AuthProvider.GOOGLE, "ptsr00"); + userProfile.setId(UUID.randomUUID().toString()); + User user = new User(userProfile, Collections.emptyList()); + OAuthToken token = new OAuthToken(user, profile, Collections.emptyList()); + socketid2user.put(UUID.randomUUID(), new ClientAuthentication(token)); - socketid2user.put(UUID.randomUUID(), new User(profile)); List<GrantedAuthority> authorities = new ArrayList<>(); authorities.add(new SimpleGrantedAuthority("ROLE_GUEST")); - socketid2user.put(UUID.randomUUID(), new User(new AnonymousAuthenticationToken("ptsr00", UUID.randomUUID(), authorities))); + socketid2user.put(UUID.randomUUID(), new ClientAuthentication(new AnonymousAuthenticationToken("ptsr00", UUID.randomUUID(), authorities))); ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(socketid2user); objOut.close(); ObjectInputStream objIn = new ObjectInputStream(new ByteArrayInputStream(out.toByteArray())); - Map<UUID, User> actual = (Map<UUID, User>) objIn.readObject(); + Map<UUID, ClientAuthentication> actual = (Map<UUID, ClientAuthentication>) objIn.readObject(); assertEquals(actual, socketid2user); } diff --git a/src/test/java/de/thm/arsnova/services/score/QuestionBasedScoreCalculatorTest.java b/src/test/java/de/thm/arsnova/service/score/QuestionBasedScoreCalculatorTest.java similarity index 85% rename from src/test/java/de/thm/arsnova/services/score/QuestionBasedScoreCalculatorTest.java rename to src/test/java/de/thm/arsnova/service/score/QuestionBasedScoreCalculatorTest.java index a84e38c5d2c4ef19194cef449c6c785121989c5c..dad34d7b79ba1eca43964b362b13f4e095374437 100644 --- a/src/test/java/de/thm/arsnova/services/score/QuestionBasedScoreCalculatorTest.java +++ b/src/test/java/de/thm/arsnova/service/score/QuestionBasedScoreCalculatorTest.java @@ -15,15 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; - -import de.thm.arsnova.entities.TestUser; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ScoreStatistics; -import de.thm.arsnova.persistance.SessionStatisticsRepository; -import de.thm.arsnova.services.score.QuestionBasedScoreCalculator; -import de.thm.arsnova.services.score.Score; -import de.thm.arsnova.services.score.VariantScoreCalculator; +package de.thm.arsnova.service.score; + +import de.thm.arsnova.model.TestClient; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ScoreStatistics; +import de.thm.arsnova.persistence.SessionStatisticsRepository; import org.junit.Before; import org.junit.Test; @@ -45,7 +42,7 @@ public class QuestionBasedScoreCalculatorTest { return questionId; } - private void addAnswer(String questionId, User user, int points) { + private void addAnswer(String questionId, ClientAuthentication user, int points) { final int piRound = 1; courseScore.addAnswer(questionId, piRound, user.getUsername(), points); } @@ -65,7 +62,7 @@ public class QuestionBasedScoreCalculatorTest { public void shouldIgnoreQuestionsWithoutCorrectAnswers() { final int questionMaxValue = 0; final int userScore = 0; - User user = new TestUser("username"); + ClientAuthentication user = new TestClient("username"); String questionId = this.addQuestion("lecture", questionMaxValue); this.addAnswer(questionId, user, userScore); @@ -80,7 +77,7 @@ public class QuestionBasedScoreCalculatorTest { @Test public void shouldIgnoreQuestionsWithoutCorrectAnswersInQuestionCount() { - User user = new TestUser("username"); + ClientAuthentication user = new TestClient("username"); courseScore.addQuestion("question-without-correct-answers", "lecture", 1, 0); courseScore.addQuestion("question-with-correct-answers", "lecture", 1, 50); courseScore.addAnswer("question-without-correct-answers", 1, user.getUsername(), 0); @@ -102,9 +99,9 @@ public class QuestionBasedScoreCalculatorTest { public void shouldCalculatePercentageOfOneQuestionWithSomeWrongAnswers() { String questionId = this.addQuestion("lecture", 10); for (int i = 0; i < 99; i++) { - this.addAnswer(questionId, new TestUser("user"+i), 10); + this.addAnswer(questionId, new TestClient("user"+i), 10); } - this.addAnswer(questionId, new TestUser("user-with-a-wrong-answer"), 0); + this.addAnswer(questionId, new TestClient("user-with-a-wrong-answer"), 0); int expected = 99; int actual = lp.getCourseProgress(null).getCourseProgress(); @@ -123,8 +120,8 @@ public class QuestionBasedScoreCalculatorTest { String q1 = this.addQuestion("lecture", 10); String q2 = this.addQuestion("lecture", 10); // two users - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); // four answers, last one is wrong this.addAnswer(q1, u1, 10); this.addAnswer(q1, u2, 10); @@ -144,10 +141,10 @@ public class QuestionBasedScoreCalculatorTest { String q2 = this.addQuestion("lecture", 1); // first question has many answers, all of them correct for (int i = 0; i < 100; i++) { - this.addAnswer(q1, new TestUser("user"+i), 1000); + this.addAnswer(q1, new TestClient("user"+i), 1000); } // second question has one wrong answer - this.addAnswer(q2, new TestUser("another-user"), 0); + this.addAnswer(q2, new TestClient("another-user"), 0); int expected = 50; int actual = lp.getCourseProgress(null).getCourseProgress(); @@ -159,8 +156,8 @@ public class QuestionBasedScoreCalculatorTest { public void shouldFilterBasedOnQuestionVariant() { String q1 = this.addQuestion("lecture", 100); String q2 = this.addQuestion("preparation", 100); - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); // first question is answered correctly, second one is not this.addAnswer(q1, u1, 100); this.addAnswer(q1, u2, 100); @@ -182,8 +179,8 @@ public class QuestionBasedScoreCalculatorTest { @Test public void shouldConsiderAnswersOfSamePiRound() { - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); // question is in round 2 courseScore.addQuestion("q1", "lecture", 2, 100); // 25 points in round 1, 75 points in round two for the first user @@ -209,9 +206,9 @@ public class QuestionBasedScoreCalculatorTest { String q1 = this.addQuestion("lecture", 10); String q2 = this.addQuestion("lecture", 10); // three users - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); - User u3 = new TestUser("user3"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); + ClientAuthentication u3 = new TestClient("user3"); // six answers this.addAnswer(q1, u1, 10); this.addAnswer(q2, u1, -100); diff --git a/src/test/java/de/thm/arsnova/services/score/ScoreBasedScoreCalculatorTest.java b/src/test/java/de/thm/arsnova/service/score/ScoreBasedScoreCalculatorTest.java similarity index 84% rename from src/test/java/de/thm/arsnova/services/score/ScoreBasedScoreCalculatorTest.java rename to src/test/java/de/thm/arsnova/service/score/ScoreBasedScoreCalculatorTest.java index 36083965b9d4119d1b304ccbd45199c901758958..5469530b0924751a8a526e2e238267d8d63cd115 100644 --- a/src/test/java/de/thm/arsnova/services/score/ScoreBasedScoreCalculatorTest.java +++ b/src/test/java/de/thm/arsnova/service/score/ScoreBasedScoreCalculatorTest.java @@ -15,15 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ -package de.thm.arsnova.services.score; - -import de.thm.arsnova.entities.TestUser; -import de.thm.arsnova.entities.User; -import de.thm.arsnova.entities.transport.ScoreStatistics; -import de.thm.arsnova.persistance.SessionStatisticsRepository; -import de.thm.arsnova.services.score.Score; -import de.thm.arsnova.services.score.ScoreBasedScoreCalculator; -import de.thm.arsnova.services.score.VariantScoreCalculator; +package de.thm.arsnova.service.score; + +import de.thm.arsnova.model.TestClient; +import de.thm.arsnova.model.migration.v2.ClientAuthentication; +import de.thm.arsnova.model.transport.ScoreStatistics; +import de.thm.arsnova.persistence.SessionStatisticsRepository; import org.junit.Before; import org.junit.Test; @@ -45,7 +42,7 @@ public class ScoreBasedScoreCalculatorTest { return questionId; } - private void addAnswer(String questionId, User user, int points) { + private void addAnswer(String questionId, ClientAuthentication user, int points) { final int piRound = 1; courseScore.addAnswer(questionId, piRound, user.getUsername(), points); } @@ -64,9 +61,9 @@ public class ScoreBasedScoreCalculatorTest { String q1 = this.addQuestion("lecture", 100); String q2 = this.addQuestion("lecture", 100); String q3 = this.addQuestion("lecture", 100); - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); - User u3 = new TestUser("user3"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); + ClientAuthentication u3 = new TestClient("user3"); // Both users achieve 200 points this.addAnswer(q1, u1, 100); this.addAnswer(q1, u2, 100); @@ -92,8 +89,8 @@ public class ScoreBasedScoreCalculatorTest { String q1 = this.addQuestion("lecture", 100); String q2 = this.addQuestion("lecture", 100); String q3 = this.addQuestion("lecture", 100); - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); // Both users achieve 200 points this.addAnswer(q1, u1, 100); this.addAnswer(q1, u2, 100); @@ -111,8 +108,8 @@ public class ScoreBasedScoreCalculatorTest { @Test public void shouldConsiderAnswersOfSamePiRound() { - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); // question is in round 2 courseScore.addQuestion("q1", "lecture", 2, 100); // 25 points in round 1, 75 points in round two for the first user @@ -138,9 +135,9 @@ public class ScoreBasedScoreCalculatorTest { String q1 = this.addQuestion("lecture", 10); String q2 = this.addQuestion("lecture", 10); // three users - User u1 = new TestUser("user1"); - User u2 = new TestUser("user2"); - User u3 = new TestUser("user3"); + ClientAuthentication u1 = new TestClient("user1"); + ClientAuthentication u2 = new TestClient("user2"); + ClientAuthentication u3 = new TestClient("user3"); // six answers this.addAnswer(q1, u1, 10); this.addAnswer(q2, u1, 0); diff --git a/src/test/java/de/thm/arsnova/services/DefaultEntityServiceImplTest.java b/src/test/java/de/thm/arsnova/services/DefaultEntityServiceImplTest.java deleted file mode 100644 index a7708c9eed3eb0f5cb03b89effae4b44082bb63e..0000000000000000000000000000000000000000 --- a/src/test/java/de/thm/arsnova/services/DefaultEntityServiceImplTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package de.thm.arsnova.services; - -import com.fasterxml.jackson.databind.ObjectMapper; -import de.thm.arsnova.config.AppConfig; -import de.thm.arsnova.config.TestAppConfig; -import de.thm.arsnova.config.TestPersistanceConfig; -import de.thm.arsnova.config.TestSecurityConfig; -import de.thm.arsnova.entities.Session; -import de.thm.arsnova.persistance.SessionRepository; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; -import org.springframework.test.context.web.WebAppConfiguration; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import static org.mockito.AdditionalAnswers.*; -import static org.mockito.Mockito.*; -import static org.junit.Assert.*; - - -@RunWith(SpringJUnit4ClassRunner.class) -@WebAppConfiguration -@ContextConfiguration(classes = {AppConfig.class, TestAppConfig.class, TestPersistanceConfig.class, TestSecurityConfig.class}) -@ActiveProfiles("test") -public class DefaultEntityServiceImplTest { - @Autowired - @Qualifier("defaultJsonMessageConverter") - private MappingJackson2HttpMessageConverter jackson2HttpMessageConverter; - - @Autowired - private SessionRepository sessionRepository; - - @Test - @WithMockUser(username="TestUser") - public void testPatch() throws IOException { - final ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper(); - final DefaultEntityServiceImpl<Session> entityService = new DefaultEntityServiceImpl<>(Session.class, sessionRepository, objectMapper); - - when(sessionRepository.save(any(Session.class))).then(returnsFirstArg()); - - final String originalId = "d8833f0d78964a9487ded02ba2dfbbad"; - final String originalName = "Test Session"; - final String originalCreator = "TestUser"; - final boolean originalActive = false; - final Session session = new Session(); - session.setId(originalId); - session.setName(originalName); - session.setActive(originalActive); - session.setCreator(originalCreator); - - final String patchedName = "Patched Session"; - final boolean patchedActive = true; - final Map<String, Object> patchedValues = new HashMap<>(); - patchedValues.put("name", patchedName); - patchedValues.put("active", patchedActive); - patchedValues.put("creator", "Should not be changeable."); - - entityService.patch(session, patchedValues); - - assertEquals(originalId, session.getId()); - assertEquals(patchedName, session.getName()); - assertEquals(patchedActive, session.isActive()); - assertEquals(originalCreator, session.getCreator()); - } - - @Test - @WithMockUser(username="TestUser") - public void testPatchWithList() throws IOException { - final ObjectMapper objectMapper = jackson2HttpMessageConverter.getObjectMapper(); - final DefaultEntityServiceImpl<Session> entityService = new DefaultEntityServiceImpl<>(Session.class, sessionRepository, objectMapper); - - when(sessionRepository.save(any(Session.class))).then(returnsFirstArg()); - - List<Session> sessions = new ArrayList<>(); - final String originalId1 = "d8833f0d78964a9487ded02ba2dfbbad"; - final String originalName1 = "Test Session 1"; - final String originalCreator1 = "TestUser"; - final boolean originalActive1 = false; - final Session session1 = new Session(); - session1.setId(originalId1); - session1.setName(originalName1); - session1.setActive(originalActive1); - session1.setCreator(originalCreator1); - sessions.add(session1); - final String originalId2 = "3dc8cbff05da49d5980f6c001a6ea867"; - final String originalName2 = "Test Session 2"; - final String originalCreator2 = "TestUser"; - final boolean originalActive2 = false; - final Session session2 = new Session(); - session2.setId(originalId2); - session2.setName(originalName2); - session2.setActive(originalActive2); - session2.setCreator(originalCreator2); - sessions.add(session2); - - final String patchedName = "Patched Session"; - final boolean patchedActive = true; - final Map<String, Object> patchedValues = new HashMap<>(); - patchedValues.put("name", patchedName); - patchedValues.put("active", patchedActive); - patchedValues.put("creator", "Should not be changeable."); - - entityService.patch(sessions, patchedValues); - - assertEquals(originalId1, session1.getId()); - assertEquals(patchedName, session1.getName()); - assertEquals(patchedActive, session1.isActive()); - assertEquals(originalCreator1, session1.getCreator()); - assertEquals(originalId2, session2.getId()); - assertEquals(patchedName, session2.getName()); - assertEquals(patchedActive, session2.isActive()); - assertEquals(originalCreator2, session2.getCreator()); - } -} diff --git a/src/test/java/de/thm/arsnova/services/StubUserService.java b/src/test/java/de/thm/arsnova/services/StubUserService.java deleted file mode 100644 index d1f01232707e95b427b8d243f8d18f7c33e40a92..0000000000000000000000000000000000000000 --- a/src/test/java/de/thm/arsnova/services/StubUserService.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * This file is part of ARSnova Backend. - * Copyright (C) 2012-2018 The ARSnova Team and Contributors - * - * ARSnova Backend is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * ARSnova Backend is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see <http://www.gnu.org/licenses/>. - */ -package de.thm.arsnova.services; - -import de.thm.arsnova.entities.User; -import de.thm.arsnova.persistance.UserRepository; -import org.springframework.mail.javamail.JavaMailSender; -import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; - -public class StubUserService extends UserServiceImpl { - - private User stubUser = null; - - public StubUserService(UserRepository repository, JavaMailSender mailSender) { - super(repository, mailSender); - } - - public void setUserAuthenticated(boolean isAuthenticated) { - this.setUserAuthenticated(isAuthenticated, "ptsr00"); - } - - public void setUserAuthenticated(boolean isAuthenticated, String username) { - if (isAuthenticated) { - stubUser = new User(new UsernamePasswordAuthenticationToken(username, "testpassword")); - return; - } - stubUser = null; - } - - public void useAnonymousUser() { - stubUser = new User(new UsernamePasswordAuthenticationToken("anonymous", "")); - } - - @Override - public User getCurrentUser() { - return stubUser; - } - - public void setRole(UserSessionService.Role role) { - stubUser.setRole(role); - } -} diff --git a/src/test/java/de/thm/arsnova/test/context/support/WithMockUser.java b/src/test/java/de/thm/arsnova/test/context/support/WithMockUser.java new file mode 100644 index 0000000000000000000000000000000000000000..bcbcece44ebeed297b1d6b7df6dcab94c443122c --- /dev/null +++ b/src/test/java/de/thm/arsnova/test/context/support/WithMockUser.java @@ -0,0 +1,52 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.test.context.support; + +import de.thm.arsnova.model.UserProfile; +import org.springframework.security.test.context.support.WithSecurityContext; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Daniel Gerhardt + */ +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@WithSecurityContext( + factory = WithMockUserSecurityContextFactory.class +) +public @interface WithMockUser { + String value() default "user"; + + UserProfile.AuthProvider authProvider() default UserProfile.AuthProvider.ARSNOVA; + + String loginId() default ""; + + String userId() default ""; + + String[] roles() default {"USER"}; + + String password() default "password"; +} diff --git a/src/test/java/de/thm/arsnova/test/context/support/WithMockUserSecurityContextFactory.java b/src/test/java/de/thm/arsnova/test/context/support/WithMockUserSecurityContextFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..b158e109fac92353c91a2fc29e423eb3fda885e3 --- /dev/null +++ b/src/test/java/de/thm/arsnova/test/context/support/WithMockUserSecurityContextFactory.java @@ -0,0 +1,50 @@ +/* + * This file is part of ARSnova Backend. + * Copyright (C) 2012-2018 The ARSnova Team + * + * ARSnova Backend is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * ARSnova Backend is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +package de.thm.arsnova.test.context.support; + +import de.thm.arsnova.model.UserProfile; +import de.thm.arsnova.security.User; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; +import org.springframework.util.StringUtils; + +import java.util.Arrays; +import java.util.stream.Collectors; + +/** + * @author Daniel Gerhardt + */ +public class WithMockUserSecurityContextFactory implements WithSecurityContextFactory<WithMockUser> { + @Override + public SecurityContext createSecurityContext(final WithMockUser withMockUser) { + String loginId = StringUtils.hasLength(withMockUser.loginId()) ? withMockUser.loginId() : withMockUser.value(); + UserProfile userProfile = new UserProfile(withMockUser.authProvider(), loginId); + userProfile.setId(!withMockUser.userId().isEmpty() ? withMockUser.userId() : loginId); + User user = new User(userProfile, Arrays.stream(withMockUser.roles()) + .map(r -> new SimpleGrantedAuthority("ROLE_" + r)).collect(Collectors.toList())); + Authentication authentication = new UsernamePasswordAuthenticationToken(user, withMockUser.password(), user.getAuthorities()); + SecurityContext context = SecurityContextHolder.createEmptyContext(); + context.setAuthentication(authentication); + + return context; + } +} diff --git a/src/test/resources/arsnova.properties.example b/src/test/resources/arsnova.properties.example index 34fa9b0a1d80f6a0a91fa1bbd5e3d77c239494e8..0eda0cadfc51d0d68c578ab51db68bcb30b2a68f 100644 --- a/src/test/resources/arsnova.properties.example +++ b/src/test/resources/arsnova.properties.example @@ -38,9 +38,10 @@ security.admin-accounts= ################################################################################ couchdb.host=localhost couchdb.port=5984 -couchdb.name=arsnova +couchdb.name=arsnova3 couchdb.username=admin couchdb.password= +#couchdb.migrate-from=arsnova ################################################################################ @@ -58,6 +59,12 @@ mail.host= # several minutes security.authentication.login-try-limit=50 +# JSON Web Tokens +# +security.jwt.serverId=arsnova.backend.v3:com.example +security.jwt.secret= +security.jwt.validity-period=T6H + # Configuration parameters for authentication services: # enabled: enable or disable the service # allowed-roles: enable/disable service for a specific role (valid roles: speaker, student)