Made Exam API asynchronous a, fix tests and fix monitoring activity
This commit is contained in:
parent
a360bfffc8
commit
43578d3e1c
5 changed files with 294 additions and 195 deletions
|
@ -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 ---------------------------------------------------------------------
|
||||
|
|
|
@ -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());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue