Made Exam API asynchronous a, fix tests and fix monitoring activity

This commit is contained in:
anhefti 2020-02-26 10:01:55 +01:00
parent a360bfffc8
commit 43578d3e1c
5 changed files with 294 additions and 195 deletions

View file

@ -335,10 +335,14 @@ public class ActivitiesPane implements TemplateComposer {
.create());
}
if (monitoring.getItemCount() > 0) {
monitoring.setExpanded(
this.currentUser
.get()
.hasAnyRole(UserRole.EXAM_SUPPORTER));
} else {
monitoring.dispose();
}
}
// ---- MONITORING ---------------------------------------------------------------------

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletionException;
import java.util.stream.Collectors;
import org.slf4j.Logger;
@ -205,4 +206,24 @@ public class APIExceptionHandler extends ResponseEntityExceptionHandler {
HttpStatus.BAD_REQUEST);
}
@ExceptionHandler(CompletionException.class)
public ResponseEntity<Object> handleCompletionException(
final CompletionException ex,
final WebRequest request) {
final Throwable cause = ex.getCause();
if (cause instanceof APIMessageException) {
return handleAPIMessageException((APIMessageException) cause, request);
} else if (cause instanceof APIConstraintViolationException) {
return handleIllegalAPIArgumentException((APIConstraintViolationException) cause, request);
} else if (cause instanceof ResourceNotFoundException) {
return APIMessage.ErrorMessage.RESOURCE_NOT_FOUND.createErrorResponse(cause.getMessage());
} else if (cause instanceof RuntimeException) {
return APIMessage.ErrorMessage.UNEXPECTED.createErrorResponse(cause.getMessage());
}
return APIMessage.ErrorMessage.GENERIC.createErrorResponse(cause.getMessage());
}
}

View file

@ -13,6 +13,7 @@ import java.security.Principal;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
@ -86,7 +87,7 @@ public class ExamAPI_V1_Controller {
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Collection<RunningExamInfo> handshakeCreate(
public CompletableFuture<Collection<RunningExamInfo>> handshakeCreate(
@RequestParam(name = API.PARAM_INSTITUTION_ID, required = false) final Long instIdRequestParam,
@RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examIdRequestParam,
@RequestBody(required = false) final MultiValueMap<String, String> formParams,
@ -94,6 +95,9 @@ public class ExamAPI_V1_Controller {
final HttpServletRequest request,
final HttpServletResponse response) {
return CompletableFuture.supplyAsync(
() -> {
final POSTMapper mapper = new POSTMapper(formParams);
final String remoteAddr = request.getRemoteAddr();
@ -129,25 +133,31 @@ public class ExamAPI_V1_Controller {
}
if (result.isEmpty()) {
log.warn("There are no currently running exams for institution: {}. SEB connection creation denied",
log.warn(
"There are no currently running exams for institution: {}. SEB connection creation denied",
institutionId);
throw new IllegalStateException("There are no currently running exams");
}
return result;
},
this.executor);
}
@RequestMapping(
path = API.EXAM_API_HANDSHAKE_ENDPOINT,
method = RequestMethod.PATCH,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeUpdate(
public CompletableFuture<Void> handshakeUpdate(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId,
@RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId,
final Principal principal,
final HttpServletRequest request) {
return CompletableFuture.runAsync(
() -> {
final String remoteAddr = request.getRemoteAddr();
final Long institutionId = getInstitutionId(principal);
@ -170,19 +180,24 @@ public class ExamAPI_V1_Controller {
remoteAddr,
userSessionId)
.getOrThrow();
},
this.executor);
}
@RequestMapping(
path = API.EXAM_API_HANDSHAKE_ENDPOINT,
method = RequestMethod.PUT,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeEstablish(
public CompletableFuture<Void> handshakeEstablish(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_PARAM_EXAM_ID, required = false) final Long examId,
@RequestParam(name = API.EXAM_API_USER_SESSION_ID, required = false) final String userSessionId,
final Principal principal,
final HttpServletRequest request) {
return CompletableFuture.runAsync(
() -> {
final String remoteAddr = request.getRemoteAddr();
final Long institutionId = getInstitutionId(principal);
@ -203,17 +218,22 @@ public class ExamAPI_V1_Controller {
remoteAddr,
userSessionId)
.getOrThrow();
},
this.executor);
}
@RequestMapping(
path = API.EXAM_API_HANDSHAKE_ENDPOINT,
method = RequestMethod.DELETE,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void handshakeDelete(
public CompletableFuture<Void> handshakeDelete(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
final Principal principal,
final HttpServletRequest request) {
return CompletableFuture.runAsync(
() -> {
final String remoteAddr = request.getRemoteAddr();
final Long institutionId = getInstitutionId(principal);
@ -230,19 +250,91 @@ public class ExamAPI_V1_Controller {
institutionId,
remoteAddr)
.getOrThrow();
},
this.executor);
}
@RequestMapping(
path = API.EXAM_API_CONFIGURATION_REQUEST_ENDPOINT,
method = RequestMethod.GET,
produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public void getConfig(
public CompletableFuture<Void> getConfig(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(required = false) final MultiValueMap<String, String> formParams,
final Principal principal,
final HttpServletRequest request,
final HttpServletResponse response) throws IOException {
return CompletableFuture.runAsync(
() -> streamExamConfig(connectionToken, formParams, principal, response),
this.executor);
}
private static final ResponseEntity<String> EMPTY_PING_RESPONSE = ResponseEntity
.ok()
.build();
@RequestMapping(
path = API.EXAM_API_PING_ENDPOINT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public CompletableFuture<ResponseEntity<String>> ping(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_PING_TIMESTAMP, required = true) final long timestamp,
@RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) {
return CompletableFuture.supplyAsync(
() -> {
final String notifyPing = this.sebClientConnectionService
.notifyPing(connectionToken, timestamp, pingNumber);
if (notifyPing == null) {
return EMPTY_PING_RESPONSE;
}
return ResponseEntity
.ok()
.body(notifyPing);
},
this.executor);
}
@RequestMapping(
path = API.EXAM_API_EVENT_ENDPOINT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public CompletableFuture<Void> event(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestBody(required = true) final ClientEvent event) {
return CompletableFuture.runAsync(
() -> this.sebClientConnectionService
.notifyClientEvent(connectionToken, event),
this.executor);
}
private Long getInstitutionId(final Principal principal) {
final String clientId = principal.getName();
return this.sebClientConfigDAO.byClientName(clientId)
.getOrThrow().institutionId;
}
private RunningExamInfo createRunningExamInfo(final Exam exam) {
return new RunningExamInfo(
exam,
this.lmsSetupDAO.byPK(exam.lmsSetupId)
.map(lms -> lms.lmsType)
.getOr(null));
}
private void streamExamConfig(
final String connectionToken,
final MultiValueMap<String, String> formParams,
final Principal principal,
final HttpServletResponse response) {
try {
// if an examId is provided with the request, update the connection first
if (formParams != null && formParams.containsKey(API.EXAM_API_PARAM_EXAM_ID)) {
final String examId = formParams.getFirst(API.EXAM_API_PARAM_EXAM_ID);
@ -272,8 +364,9 @@ public class ExamAPI_V1_Controller {
if (connection.clientConnection.examId == null ||
!this.examSessionService.isExamRunning(connection.clientConnection.examId)) {
log.error("Missing exam identifer or requested exam is not running for connection: {}", connection);
throw new IllegalStateException("Missing exam identider or requested exam is not running");
log.error("Missing exam identifier or requested exam is not running for connection: {}",
connection);
throw new IllegalStateException("Missing exam identifier or requested exam is not running");
}
} catch (final Exception e) {
@ -305,74 +398,20 @@ public class ExamAPI_V1_Controller {
outputStream.flush();
outputStream.close();
}
} catch (final Exception e) {
log.error("Unexpected error while trying to stream SEB Exam Configuration to client with connection: {}",
connectionToken,
e);
final APIMessage errorMessage = APIMessage.ErrorMessage.GENERIC.of(e.getMessage());
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
try {
response.getOutputStream().write(Utils.toByteArray(this.jsonMapper.writeValueAsString(errorMessage)));
} catch (final Exception e1) {
log.error("Failed to write error to response: ", e1);
}
private static final ResponseEntity<String> EMPTY_PING_RESPONSE = ResponseEntity
.ok()
.build();
@RequestMapping(
path = API.EXAM_API_PING_ENDPOINT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<String> ping(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestParam(name = API.EXAM_API_PING_TIMESTAMP, required = true) final long timestamp,
@RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) {
final String notifyPing = this.sebClientConnectionService
.notifyPing(connectionToken, timestamp, pingNumber);
if (notifyPing == null) {
return EMPTY_PING_RESPONSE;
}
return ResponseEntity
.ok()
.body(notifyPing);
}
// @RequestMapping(
// path = API.EXAM_API_PING_ENDPOINT,
// method = RequestMethod.POST,
// consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE,
// produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
// public CompletableFuture<String> ping(
// @RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
// @RequestParam(name = API.EXAM_API_PING_TIMESTAMP, required = true) final long timestamp,
// @RequestParam(name = API.EXAM_API_PING_NUMBER, required = false) final int pingNumber) {
//
// return CompletableFuture.supplyAsync(
// () -> this.sebClientConnectionService
// .notifyPing(connectionToken, timestamp, pingNumber),
// this.executor);
// }
@RequestMapping(
path = API.EXAM_API_EVENT_ENDPOINT,
method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public void event(
@RequestHeader(name = API.EXAM_API_SEB_CONNECTION_TOKEN, required = true) final String connectionToken,
@RequestBody(required = true) final ClientEvent event) {
this.sebClientConnectionService
.notifyClientEvent(connectionToken, event);
}
private Long getInstitutionId(final Principal principal) {
final String clientId = principal.getName();
return this.sebClientConfigDAO.byClientName(clientId)
.getOrThrow().institutionId;
}
private RunningExamInfo createRunningExamInfo(final Exam exam) {
return new RunningExamInfo(
exam,
this.lmsSetupDAO.byPK(exam.lmsSetupId)
.map(lms -> lms.lmsType)
.getOr(null));
}
}

View file

@ -11,6 +11,7 @@ package ch.ethz.seb.sebserver.webservice.integration.api.exam;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.Collections;
@ -39,8 +40,10 @@ import org.springframework.security.web.FilterChainProxy;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -153,7 +156,12 @@ public abstract class ExamAPIIntegrationTester {
builder.content(body);
final ResultActions result = this.mockMvc.perform(builder)
final MvcResult mvcResult = this.mockMvc.perform(builder)
.andExpect(request().asyncStarted())
.andDo(MockMvcResultHandlers.log())
.andReturn();
final ResultActions result = this.mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE));
return result.andReturn().getResponse();
@ -209,7 +217,13 @@ public abstract class ExamAPIIntegrationTester {
}
builder.content(body);
final ResultActions result = this.mockMvc.perform(builder);
final MvcResult mvcResult = this.mockMvc
.perform(builder)
.andExpect(request().asyncStarted())
.andDo(MockMvcResultHandlers.log())
.andReturn();
final ResultActions result = this.mockMvc.perform(asyncDispatch(mvcResult));
return result.andReturn().getResponse();
}
@ -221,7 +235,13 @@ public abstract class ExamAPIIntegrationTester {
.header("Authorization", "Bearer " + accessToken)
.header(API.EXAM_API_SEB_CONNECTION_TOKEN, connectionToken)
.accept(MediaType.APPLICATION_JSON_UTF8_VALUE);
final ResultActions result = this.mockMvc.perform(builder);
final MvcResult mvcResult = this.mockMvc
.perform(builder)
.andExpect(request().asyncStarted())
.andDo(MockMvcResultHandlers.log())
.andReturn();
final ResultActions result = this.mockMvc.perform(asyncDispatch(mvcResult));
return result.andReturn().getResponse();
}
@ -239,7 +259,12 @@ public abstract class ExamAPIIntegrationTester {
+ "&" + API.EXAM_API_PING_NUMBER + "=" + num;
builder.content(body);
final ResultActions result = this.mockMvc.perform(builder);
final MvcResult mvcResult = this.mockMvc
.perform(builder)
.andExpect(request().asyncStarted())
.andDo(MockMvcResultHandlers.log())
.andReturn();
final ResultActions result = this.mockMvc.perform(asyncDispatch(mvcResult));
return result.andReturn().getResponse();
}
@ -259,7 +284,12 @@ public abstract class ExamAPIIntegrationTester {
final String body = "{ \"type\": \"%s\", \"timestamp\": %s, \"numericValue\": %s, \"text\": \"%s\" }";
builder.content(String.format(body, type, timestamp, value, text));
final ResultActions result = this.mockMvc.perform(builder);
final MvcResult mvcResult = this.mockMvc
.perform(builder)
.andExpect(request().asyncStarted())
.andDo(MockMvcResultHandlers.log())
.andReturn();
final ResultActions result = this.mockMvc.perform(asyncDispatch(mvcResult));
return result.andReturn().getResponse();
}
@ -278,8 +308,13 @@ public abstract class ExamAPIIntegrationTester {
builder.content("examId=" + examId);
}
final ResultActions result = this.mockMvc
.perform(builder);
final MvcResult mvcResult = this.mockMvc
.perform(builder)
.andExpect(request().asyncStarted())
.andDo(MockMvcResultHandlers.log())
.andReturn();
final ResultActions result = this.mockMvc.perform(asyncDispatch(mvcResult));
return result.andReturn().getResponse();
}

View file

@ -210,7 +210,7 @@ public class SebExamConfigurationRequestTest extends ExamAPIIntegrationTester {
// check error
final String contentAsString = configResponse.getContentAsString();
assertEquals(
"{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Missing exam identider or requested exam is not running\",\"attributes\":[]}",
"{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Missing exam identifier or requested exam is not running\",\"attributes\":[]}",
contentAsString);
// check connection cache
@ -253,7 +253,7 @@ public class SebExamConfigurationRequestTest extends ExamAPIIntegrationTester {
assertTrue(HttpStatus.OK.value() != configResponse.getStatus());
final String contentAsString = configResponse.getContentAsString();
assertEquals(
"{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Missing exam identider or requested exam is not running\",\"attributes\":[]}",
"{\"messageCode\":\"0\",\"systemMessage\":\"Generic error message\",\"details\":\"Missing exam identifier or requested exam is not running\",\"attributes\":[]}",
contentAsString);
}