SEBSERV-62 Model, DAO and service implementation

This commit is contained in:
anhefti 2019-06-26 15:31:18 +02:00
parent 54eea01dc2
commit 3d67b4ed9c
32 changed files with 1433 additions and 13 deletions

View file

@ -112,7 +112,7 @@
<excludes>
<exclude>**/batis/mapper/*.java</exclude>
<exclude>**/batis/model/*.java</exclude>
<exclude name="UselessParentheses"/>
<exclude name="UselessParentheses" />
</excludes>
</configuration>
<executions>
@ -242,6 +242,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- NOTE since org.springframework.security.oauth is not fully migrated
to spring-boot-starter-security we have to declare a separate version here.
This refers to the latest version of spring-security-oauth2 and should be

View file

@ -11,10 +11,11 @@ package ch.ethz.seb.sebserver;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
/** SEB-Server (Safe Exam Browser Server) is a server component to maintain and support
* Exams running with SEB (Safe Exam Browser). TODO add link(s)
*
*
* SEB-Server uses Spring Boot as main framework is divided into two main components,
* a webservice component that implements the business logic, persistence management
* and defines a REST API to expose the services over HTTP. The second component is a
@ -25,13 +26,12 @@ import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServic
* and GUI and can be used to start the components on separate servers or within the same
* server instance. Additional to the usual profiles like dev, prod, test there are combining
* profiles like dev-ws, dev-gui and prod-ws, prod-gui
*
*
* TODO documentation for presets to start all-in-one server or separated gui- and webservice- server */
@SpringBootApplication(exclude = {
// OAuth2ResourceServerAutoConfiguration.class,
UserDetailsServiceAutoConfiguration.class,
//DataSourceAutoConfiguration.class
})
@EnableCaching
public class SEBServer {
public static void main(final String[] args) {

View file

@ -32,8 +32,26 @@ public final class Indicator implements Entity {
public static final String FILTER_ATTR_EXAM_ID = "examId";
public enum IndicatorType {
LAST_PING,
ERROR_COUNT
LAST_PING(Names.LAST_PING),
ERROR_COUNT(Names.ERROR_COUNT)
;
public final String name;
private IndicatorType(final String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
public interface Names {
public static final String LAST_PING = "LAST_PING";
public static final String ERROR_COUNT = "ERROR_COUNT";
}
}
@JsonProperty(INDICATOR.ATTR_ID)

View file

@ -17,7 +17,7 @@ import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.GrantEntity;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ClientConnection implements GrantEntity {
public final class ClientConnection implements GrantEntity {
public static final String FILTER_ATTR_EXAM_ID = Domain.CLIENT_CONNECTION.ATTR_EXAM_ID;
public static final String FILTER_ATTR_STATUS = Domain.CLIENT_CONNECTION.ATTR_STATUS;

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.session;
import java.util.Collection;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ClientConnectionData {
@JsonProperty
public final ClientConnection clientConnection;
@JsonProperty
public final Collection<? extends IndicatorValue> indicatorValues;
@JsonCreator
protected ClientConnectionData(
@JsonProperty final ClientConnection clientConnection,
@JsonProperty final Collection<? extends IndicatorValue> indicatorValues) {
this.clientConnection = clientConnection;
this.indicatorValues = indicatorValues;
}
@JsonIgnore
public Long getConnectionId() {
return this.clientConnection.id;
}
public ClientConnection getClientConnection() {
return this.clientConnection;
}
public Collection<? extends IndicatorValue> getIndicatorValues() {
return this.indicatorValues;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("ClientConnectionData [clientConnection=");
builder.append(this.clientConnection);
builder.append(", indicatorValues=");
builder.append(this.indicatorValues);
builder.append("]");
return builder.toString();
}
}

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gbl.model.session;
import java.math.BigDecimal;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -15,12 +17,23 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.Entity;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
@JsonIgnoreProperties(ignoreUnknown = true)
public final class ClientEvent implements Entity {
public final class ClientEvent implements Entity, IndicatorValueHolder {
public static final String FILTER_ATTR_CONECTION_ID = Domain.CLIENT_EVENT.ATTR_CONNECTION_ID;
public static final String FILTER_ATTR_TYPE = Domain.CLIENT_EVENT.ATTR_TYPE;
public static final String FILTER_ATTR_FROM_DATE = "fromDate";
public static enum EventType {
UNKNOWN(0), LOG(1);
UNKNOWN(0),
DEBUG_LOG(1),
INFO_LOG(2),
WARN_LOG(3),
ERROR_LOG(4),
;
public final int id;
@ -58,7 +71,7 @@ public final class ClientEvent implements Entity {
public final String text;
@JsonCreator
ClientEvent(
public ClientEvent(
@JsonProperty(Domain.CLIENT_EVENT.ATTR_ID) final Long id,
@JsonProperty(Domain.CLIENT_EVENT.ATTR_CONNECTION_ID) final Long connectionId,
@JsonProperty(Domain.CLIENT_EVENT.ATTR_TYPE) final EventType eventType,
@ -111,6 +124,13 @@ public final class ClientEvent implements Entity {
return this.numValue;
}
@Override
public double getValue() {
return this.numValue != null
? this.numValue.doubleValue()
: Double.NaN;
}
public String getText() {
return this.text;
}
@ -133,4 +153,14 @@ public final class ClientEvent implements Entity {
builder.append("]");
return builder.toString();
}
public static final ClientEventRecord toRecord(final ClientEvent event) {
return new ClientEventRecord(
event.id,
event.connectionId,
event.eventType.id,
event.timestamp,
(event.numValue != null) ? new BigDecimal(event.numValue) : null,
event.text);
}
}

View file

@ -0,0 +1,20 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.session;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
public interface IndicatorValue extends IndicatorValueHolder {
@JsonProperty(SimpleIndicatorValue.ATTR_INDICATOR_TYPE)
IndicatorType getType();
}

View file

@ -0,0 +1,19 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.session;
import com.fasterxml.jackson.annotation.JsonProperty;
@FunctionalInterface
public interface IndicatorValueHolder {
@JsonProperty(SimpleIndicatorValue.ATTR_INDICATOR_VALUE)
double getValue();
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.model.session;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
public final class SimpleIndicatorValue implements IndicatorValue {
public static final String ATTR_INDICATOR_VALUE = "indicatorValue";
public static final String ATTR_INDICATOR_TYPE = "indicatorType";
@JsonProperty(ATTR_INDICATOR_TYPE)
public final IndicatorType type;
@JsonProperty(ATTR_INDICATOR_VALUE)
public final double value;
@JsonCreator
protected SimpleIndicatorValue(
@JsonProperty(ATTR_INDICATOR_TYPE) final IndicatorType type,
@JsonProperty(ATTR_INDICATOR_VALUE) final double value) {
this.type = type;
this.value = value;
}
@Override
public IndicatorType getType() {
return this.type;
}
@Override
public double getValue() {
return this.value;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("IndicatorValue [type=");
builder.append(this.type);
builder.append(", value=");
builder.append(this.value);
builder.append("]");
return builder.toString();
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
public interface ClientEventDAO extends EntityDAO<ClientEvent, ClientEvent> {
}

View file

@ -8,9 +8,14 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
/** Concrete EntityDAO interface of Exam entities */
public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSupportDAO<Exam> {
Result<Collection<Long>> allIdsOfInstituion(Long institutionId);
}

View file

@ -9,6 +9,7 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@ -27,8 +28,11 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.Orientation;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.SebClientConfig;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import io.micrometer.core.instrument.util.StringUtils;
/** A Map containing various filter criteria from a certain API request.
* This is used as a data object that can be used to collect API request parameter
@ -190,4 +194,32 @@ public class FilterMap extends POSTMapper {
return getString(ClientConnection.FILTER_ATTR_STATUS);
}
public Long getClientEventConnectionId() {
return getLong(ClientEvent.FILTER_ATTR_CONECTION_ID);
}
public Integer getClientEventTypeId() {
final String typeName = getString(ClientEvent.FILTER_ATTR_TYPE);
if (StringUtils.isBlank(typeName)) {
return null;
}
try {
return EventType.valueOf(typeName).id;
} catch (final Exception e) {
return null;
}
}
public Long getClientEventFromDate() {
final DateTime dateTime = Utils.toDateTime(getString(ClientEvent.FILTER_ATTR_FROM_DATE));
if (dateTime == null) {
return null;
}
return dateTime
.withZone(DateTimeZone.UTC)
.getMillis();
}
}

View file

@ -33,7 +33,6 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionR
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientConnectionRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientConnectionRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
@ -101,7 +100,7 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
public Result<Collection<ClientConnection>> allOf(final Set<Long> pks) {
return Result.tryCatch(() -> {
return this.clientConnectionRecordMapper.selectByExample()
.where(LmsSetupRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks)))
.where(ClientConnectionRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks)))
.build()
.execute()
.stream()

View file

@ -0,0 +1,172 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualToWhenPresent;
import static org.mybatis.dynamic.sql.SqlBuilder.isIn;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.mybatis.dynamic.sql.SqlBuilder;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import ch.ethz.seb.sebserver.gbl.api.EntityType;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.ClientEventRecord;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.DAOLoggingSupport;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.FilterMap;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
@Lazy
@Component
@WebServiceProfile
public class ClientEventDAOImpl implements ClientEventDAO {
private final ClientEventRecordMapper clientEventRecordMapper;
protected ClientEventDAOImpl(final ClientEventRecordMapper clientEventRecordMapper) {
this.clientEventRecordMapper = clientEventRecordMapper;
}
@Override
public EntityType entityType() {
return EntityType.CLIENT_EVENT;
}
@Override
@Transactional(readOnly = true)
public Result<ClientEvent> byPK(final Long id) {
return recordById(id)
.flatMap(ClientEventDAOImpl::toDomainModel);
}
@Override
@Transactional(readOnly = true)
public Result<Collection<ClientEvent>> allMatching(
final FilterMap filterMap,
final Predicate<ClientEvent> predicate) {
return Result.tryCatch(() -> this.clientEventRecordMapper
.selectByExample()
.where(
ClientEventRecordDynamicSqlSupport.connectionId,
isEqualToWhenPresent(filterMap.getClientEventConnectionId()))
.and(
ClientEventRecordDynamicSqlSupport.type,
isEqualToWhenPresent(filterMap.getClientEventTypeId()))
.and(
ClientEventRecordDynamicSqlSupport.timestamp,
SqlBuilder.isGreaterThanOrEqualToWhenPresent(filterMap.getClientEventFromDate()))
.build()
.execute()
.stream()
.map(ClientEventDAOImpl::toDomainModel)
.flatMap(DAOLoggingSupport::logAndSkipOnError)
.filter(predicate)
.collect(Collectors.toList()));
}
@Override
@Transactional(readOnly = true)
public Result<Collection<ClientEvent>> allOf(final Set<Long> pks) {
return Result.tryCatch(() -> {
return this.clientEventRecordMapper.selectByExample()
.where(ClientEventRecordDynamicSqlSupport.id, isIn(new ArrayList<>(pks)))
.build()
.execute()
.stream()
.map(ClientEventDAOImpl::toDomainModel)
.flatMap(DAOLoggingSupport::logAndSkipOnError)
.collect(Collectors.toList());
});
}
@Override
@Transactional
public Result<ClientEvent> createNew(final ClientEvent data) {
return Result.tryCatch(() -> {
final EventType eventType = data.getEventType();
final ClientEventRecord newRecord = new ClientEventRecord(
null,
data.connectionId,
(eventType != null) ? eventType.id : EventType.UNKNOWN.id,
data.timestamp,
(data.numValue != null) ? new BigDecimal(data.numValue) : null,
data.text);
this.clientEventRecordMapper.insert(newRecord);
return newRecord;
})
.flatMap(ClientEventDAOImpl::toDomainModel)
.onError(TransactionHandler::rollback);
}
@Override
@Transactional
public Result<ClientEvent> save(final ClientEvent data) {
throw new UnsupportedOperationException("Update is not supported for client events");
}
@Override
@Transactional
public Result<Collection<EntityKey>> delete(final Set<EntityKey> all) {
throw new UnsupportedOperationException(
"Delete is not supported for particular client events. "
+ "Use delete of a client connection to delete also all client events of this connection.");
}
private Result<ClientEventRecord> recordById(final Long id) {
return Result.tryCatch(() -> {
final ClientEventRecord record = this.clientEventRecordMapper.selectByPrimaryKey(id);
if (record == null) {
throw new ResourceNotFoundException(
entityType(),
String.valueOf(id));
}
return record;
});
}
private static Result<ClientEvent> toDomainModel(final ClientEventRecord record) {
return Result.tryCatch(() -> {
final Integer type = record.getType();
final BigDecimal numericValue = record.getNumericValue();
return new ClientEvent(
record.getId(),
record.getConnectionId(),
(type != null) ? EventType.byId(type) : EventType.UNKNOWN,
record.getTimestamp(),
(numericValue != null) ? numericValue.doubleValue() : null,
record.getText());
});
}
}

View file

@ -303,6 +303,23 @@ public class ExamDAOImpl implements ExamDAO {
}).flatMap(this::toDomainModel);
}
@Override
@Transactional(readOnly = true)
public Result<Collection<Long>> allIdsOfInstituion(final Long institutionId) {
return Result.tryCatch(() -> {
return this.examRecordMapper.selectIdsByExample()
.where(
ExamRecordDynamicSqlSupport.institutionId,
isEqualTo(institutionId))
.and(
ExamRecordDynamicSqlSupport.active,
isEqualToWhenPresent(BooleanUtils.toIntegerObject(true)))
.build()
.execute();
});
}
private Result<Collection<EntityKey>> allIdsOfInstitution(final EntityKey institutionKey) {
return Result.tryCatch(() -> {
return this.examRecordMapper.selectIdsByExample()

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.Set;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValue;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValueHolder;
/** A client indicator is a indicator value holder for a specific Indicator
* on a running client connection.
* A client indicator can be used to verify a indicator value at a specific time of or
* a client indicator can be used for in memory caching of the current value of the
* indicator for a defined client connection. */
public interface ClientIndicator extends IndicatorValue {
void init(Indicator indicatorDefinition, Long connectionId, boolean cachingEnabled);
String name();
Long examId();
Long connectionId();
double computeValueAt(long timestamp);
Set<EventType> observedEvents();
void notifyValueChange(IndicatorValueHolder indicatorValueHolder);
}

View file

@ -0,0 +1,13 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
public interface ClientIndicatorService {
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.function.Consumer;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
public interface EventHandlingStrategy extends Consumer<ClientEvent> {
String EVENT_CONSUMER_STRATEGY_CONFIG_PROPERTY_KEY = "sebserver.webservice.api.exam.event-handling-strategy";
String EVENT_CONSUMER_STRATEGY_SINGLE_EVENT_STORE = "SINGLE_EVENT_STORE_STRATEGY";
String EVENT_CONSUMER_STRATEGY_ASYNC_BATCH_STORE = "ASYNC_BATCH_STORE_STRATEGY";
}

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import java.util.Collection;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ExamSessionService {
boolean isExamRunning(Long examId);
Result<Exam> getRunningExam(Long examId);
Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId);
void notifyPing(Long connectionId, long timestamp, int pingNumber);
void notifyClientEvent(final ClientEvent event, Long connectionId);
}

View file

@ -0,0 +1,25 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface SebClientConnectionService {
Result<ClientConnection> createClientConnection(Long instituionId, Long examId);
Result<ClientConnection> establishClientConnection(
String connectionToken,
Long examId,
String userSessionId);
Result<ClientConnection> closeConnection(String connectionToken);
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator;
public abstract class AbstractClientIndicator implements ClientIndicator {
protected Long examId;
protected Long connectionId;
protected String name;
protected boolean cachingEnabled;
protected double currentValue = Double.NaN;
@Override
public void init(
final Indicator indicatorDefinition,
final Long connectionId,
final boolean cachingEnabled) {
this.examId = (indicatorDefinition != null) ? indicatorDefinition.examId : null;
this.connectionId = connectionId;
this.cachingEnabled = cachingEnabled;
}
@Override
public Long examId() {
return this.examId;
}
@Override
public Long connectionId() {
return this.connectionId;
}
@Override
public String name() {
return this.name;
}
@Override
public double getValue() {
if (this.currentValue == Double.NaN || !this.cachingEnabled) {
this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis());
}
return this.currentValue;
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
public abstract class AbstractPingIndicator extends AbstractClientIndicator {
private final Set<EventType> EMPTY_SET = Collections.unmodifiableSet(EnumSet.noneOf(EventType.class));
protected int pingCount = 0;
protected int pingNumber = 0;
public void notifyPing(final long timestamp, final int pingNumber) {
super.currentValue = timestamp;
this.pingCount++;
this.pingNumber = pingNumber;
}
@Override
public double computeValueAt(final long timestamp) {
return timestamp;
}
@Override
public Set<EventType> observedEvents() {
return this.EMPTY_SET;
}
}

View file

@ -0,0 +1,156 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingDeque;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
/** Approach 2 to handle/save client events internally
*
* This Approach uses a queue to collect ClientEvents that are stored later. The queue is shared between some
* worker-threads that batch gets and stores the events from the queue afterwards. this approach is less blocking from
* the caller perspective and also faster on store data by using bulk-insert
*
* A disadvantage is an potentially multiple event data loss on total server fail. The data in the queue is state that
* is not stored somewhere yet and can't be recovered on total server fail.
*
* If the performance of this approach is not enough or the potentially data loss on total server fail is a risk that
* not can be taken, we have to consider using a messaging system/server like rabbitMQ or Apache-Kafka that brings the
* ability to effectively store and recover message queues but also comes with more complexity on setup and installation
* side as well as for the whole server system. */
@Lazy
@Component(EventHandlingStrategy.EVENT_CONSUMER_STRATEGY_ASYNC_BATCH_STORE)
@WebServiceProfile
public class AsyncBatchEventSaveStrategy implements EventHandlingStrategy {
private static final Logger log = LoggerFactory.getLogger(AsyncBatchEventSaveStrategy.class);
private static final int NUMBER_OF_WORKER_THREADS = 4;
private static final int BATCH_SIZE = 100;
private final SqlSessionFactory sqlSessionFactory;
private final Executor executor;
private final TransactionTemplate transactionTemplate;
private final BlockingDeque<ClientEvent> eventQueue = new LinkedBlockingDeque<>();
private boolean workersRunning = false;
public AsyncBatchEventSaveStrategy(
final SqlSessionFactory sqlSessionFactory,
final AsyncConfigurer asyncConfigurer,
final PlatformTransactionManager transactionManager) {
this.sqlSessionFactory = sqlSessionFactory;
this.executor = asyncConfigurer.getAsyncExecutor();
this.transactionTemplate = new TransactionTemplate(transactionManager);
this.transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
}
@EventListener(ApplicationReadyEvent.class)
protected void recover() {
runWorkers();
}
@Override
public void accept(final ClientEvent event) {
if (!this.workersRunning) {
log.error("Received ClientEvent on none enabled AsyncBatchEventSaveStrategy. ClientEvent is ignored");
return;
}
this.eventQueue.add(event);
}
private void runWorkers() {
if (this.workersRunning) {
log.warn("runWorkers called when workers are running already. Ignore that");
return;
}
this.workersRunning = true;
log.info("Start {} Event-Batch-Store Worker-Threads", NUMBER_OF_WORKER_THREADS);
for (int i = 0; i < NUMBER_OF_WORKER_THREADS; i++) {
this.executor.execute(batchSave());
}
}
private Runnable batchSave() {
return () -> {
log.debug("Worker Thread {} running", Thread.currentThread());
final Collection<ClientEvent> events = new ArrayList<>();
final SqlSessionTemplate sqlSessionTemplate = new SqlSessionTemplate(
this.sqlSessionFactory,
ExecutorType.BATCH);
final ClientEventRecordMapper clientEventMapper = sqlSessionTemplate.getMapper(
ClientEventRecordMapper.class);
long sleepTime = 100;
try {
while (this.workersRunning) {
events.clear();
this.eventQueue.drainTo(events, BATCH_SIZE);
try {
if (!events.isEmpty()) {
sleepTime = 100;
this.transactionTemplate
.execute(status -> {
events.stream()
.map(ClientEvent::toRecord)
.forEach(clientEventMapper::insert);
return null;
});
} else {
sleepTime += 100;
}
} catch (final Exception e) {
log.error("unexpected Error while trying to batch store client-events: ", e);
}
try {
Thread.sleep(sleepTime);
} catch (final InterruptedException e) {
e.printStackTrace();
}
}
} finally {
sqlSessionTemplate.close();
log.debug("Worker Thread {} stopped", Thread.currentThread());
}
};
}
}

View file

@ -0,0 +1,54 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator;
public class ClientConnectionDataInternal extends ClientConnectionData {
final Collection<AbstractPingIndicator> pingMappings;
final EnumMap<EventType, Collection<ClientIndicator>> indicatorMapping;
protected ClientConnectionDataInternal(
final ClientConnection clientConnection,
final Collection<ClientIndicator> clientIndicators) {
super(clientConnection, clientIndicators);
this.indicatorMapping = new EnumMap<>(EventType.class);
this.pingMappings = new ArrayList<>();
for (final ClientIndicator clientIndicator : clientIndicators) {
if (clientIndicator instanceof AbstractPingIndicator) {
this.pingMappings.add((AbstractPingIndicator) clientIndicator);
}
for (final EventType eventType : clientIndicator.observedEvents()) {
this.indicatorMapping
.computeIfAbsent(eventType, key -> new ArrayList<>())
.add(clientIndicator);
}
}
}
Collection<ClientIndicator> getindicatorMapping(final EventType eventType) {
if (!this.indicatorMapping.containsKey(eventType)) {
return Collections.emptyList();
}
return this.indicatorMapping.get(eventType);
}
}

View file

@ -0,0 +1,79 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
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.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.IndicatorDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ClientIndicator;
@Lazy
@Component
@WebServiceProfile
public class ClientIndicatorFactory {
private static final Logger log = LoggerFactory.getLogger(ClientIndicatorFactory.class);
private final ApplicationContext applicationContext;
private final IndicatorDAO indicatorDAO;
private final boolean enableCaching;
@Autowired
public ClientIndicatorFactory(
final ApplicationContext applicationContext,
final IndicatorDAO indicatorDAO,
@Value("${sebserver.indicator.caching}") final boolean enableCaching) {
this.applicationContext = applicationContext;
this.indicatorDAO = indicatorDAO;
this.enableCaching = enableCaching;
}
public Collection<ClientIndicator> createFor(final ClientConnection clientConnection) {
final List<ClientIndicator> result = new ArrayList<>();
try {
final Collection<Indicator> examIndicators = this.indicatorDAO
.allForExam(clientConnection.examId)
.getOrThrow();
for (final Indicator indicatorDef : examIndicators) {
try {
final ClientIndicator indicator = this.applicationContext
.getBean(indicatorDef.type.name(), ClientIndicator.class);
indicator.init(indicatorDef, clientConnection.id, this.enableCaching);
result.add(indicator);
} catch (final Exception e) {
log.warn("No Indicator with type: {} found as registered bean. Ignore this one.", indicatorDef.type,
e);
}
}
} catch (final Exception e) {
log.error("Failed to create ClientIndicator for ClientConnection: {}", clientConnection);
}
return Collections.unmodifiableList(result);
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo;
import static org.mybatis.dynamic.sql.SqlBuilder.isLessThan;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent.EventType;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValueHolder;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordDynamicSqlSupport;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.ClientEventRecordMapper;
@Lazy
@Component(IndicatorType.Names.ERROR_COUNT)
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public final class ErrorCountClientIndicator extends AbstractClientIndicator {
private final Set<EventType> OBSERVED_SET = Collections.unmodifiableSet(EnumSet.of(EventType.ERROR_LOG));
private final ClientEventRecordMapper clientEventRecordMapper;
protected ErrorCountClientIndicator(final ClientEventRecordMapper clientEventRecordMapper) {
this.clientEventRecordMapper = clientEventRecordMapper;
}
@Override
public IndicatorType getType() {
return IndicatorType.ERROR_COUNT;
}
@Override
public double computeValueAt(final long timestamp) {
final Long errors = this.clientEventRecordMapper.countByExample()
.where(ClientEventRecordDynamicSqlSupport.connectionId, isEqualTo(this.connectionId))
.and(ClientEventRecordDynamicSqlSupport.type, isEqualTo(ClientEvent.EventType.ERROR_LOG.id))
.and(ClientEventRecordDynamicSqlSupport.timestamp, isLessThan(timestamp))
.build()
.execute();
return errors.doubleValue();
}
@Override
public void notifyValueChange(final IndicatorValueHolder indicatorValueHolder) {
this.currentValue = getValue() + 1d;
}
@Override
public Set<EventType> observedEvents() {
return this.OBSERVED_SET;
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
@Lazy
@Service
@WebServiceProfile
public class ExamSessionCacheService {
public static final String CACHE_NAME_RUNNING_EXAM = "RUNNING_EXAM";
public static final String CACHE_NAME_ACTIVE_CLIENT_CONNECTION = "ACTIVE_CLIENT_CONNECTION";
private static final Logger log = LoggerFactory.getLogger(ExamSessionCacheService.class);
private final ExamDAO examDAO;
private final ClientConnectionDAO clientConnectionDAO;
private final ClientIndicatorFactory clientIndicatorFactory;
protected ExamSessionCacheService(
final ExamDAO examDAO,
final ClientConnectionDAO clientConnectionDAO,
final ClientIndicatorFactory clientIndicatorFactory) {
this.examDAO = examDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.clientIndicatorFactory = clientIndicatorFactory;
}
@Cacheable(
cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#examId",
unless = "#result == null")
Exam getRunningExam(final Long examId) {
if (log.isDebugEnabled()) {
log.debug("Verify running exam for id: {}" + examId);
}
final Result<Exam> byPK = this.examDAO.byPK(examId);
if (byPK.hasError()) {
log.error("Failed to find/load Exam with id {}", examId, byPK.getError());
return null;
}
final Exam exam = byPK.get();
if (!isRunning(exam)) {
return null;
}
return exam;
}
@CacheEvict(
cacheNames = CACHE_NAME_RUNNING_EXAM,
key = "#exam.id",
condition = "#target.isRunning(#result)")
Exam evict(final Exam exam) {
if (log.isDebugEnabled()) {
log.debug("Conditional eviction of running Exam from cache: {}", isRunning(exam));
}
return exam;
}
boolean isRunning(final Exam exam) {
if (exam == null) {
return false;
}
return ((exam.startTime.isEqualNow() || exam.startTime.isBeforeNow()) &&
exam.endTime.isAfterNow());
}
@Cacheable(
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionId",
unless = "#result == null")
ClientConnectionDataInternal getActiveClientConnection(final Long connectionId) {
if (log.isDebugEnabled()) {
log.debug("Verify ClientConnection for running exam for caching by id: ", connectionId);
}
final Result<ClientConnection> byPK = this.clientConnectionDAO.byPK(connectionId);
if (byPK.hasError()) {
log.error("Failed to find/load ClientConnection with id {}", connectionId, byPK.getError());
return null;
}
final ClientConnection clientConnection = byPK.get();
// verify exam is running
if (getRunningExam(clientConnection.examId) == null) {
log.error("Exam for ClientConnection with id { is not currently running}", connectionId);
return null;
}
return new ClientConnectionDataInternal(
clientConnection,
this.clientIndicatorFactory.createFor(clientConnection));
}
@CacheEvict(
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionId")
void evict(final Long connectionId) {
if (log.isDebugEnabled()) {
log.debug("Eviction of ClientConnectionData from cache: {}", connectionId);
}
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import java.util.Collection;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ExamDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import io.micrometer.core.instrument.util.StringUtils;
@Lazy
@Service
@WebServiceProfile
public class ExamSessionServiceImpl implements ExamSessionService {
private final ExamSessionCacheService examSessionCacheService;
private final ExamDAO examDAO;
private final EventHandlingStrategy eventHandlingStrategy;
protected ExamSessionServiceImpl(
final ExamSessionCacheService examSessionCacheService,
final ExamDAO examDAO,
final Environment environment,
final ApplicationContext applicationContext) {
this.examSessionCacheService = examSessionCacheService;
this.examDAO = examDAO;
String eventHandlingStrategyProperty =
environment.getProperty(EventHandlingStrategy.EVENT_CONSUMER_STRATEGY_CONFIG_PROPERTY_KEY);
if (StringUtils.isBlank(eventHandlingStrategyProperty)) {
eventHandlingStrategyProperty = EventHandlingStrategy.EVENT_CONSUMER_STRATEGY_SINGLE_EVENT_STORE;
}
this.eventHandlingStrategy = applicationContext.getBean(
eventHandlingStrategyProperty,
EventHandlingStrategy.class);
}
@Override
public boolean isExamRunning(final Long examId) {
return this.examSessionCacheService
.isRunning(this.examSessionCacheService.getRunningExam(examId));
}
@Override
public Result<Exam> getRunningExam(final Long examId) {
final Exam exam = this.examSessionCacheService.getRunningExam(examId);
if (this.examSessionCacheService.isRunning(exam)) {
return Result.of(exam);
} else {
if (exam != null) {
this.examSessionCacheService.evict(exam);
}
return Result.ofError(new NoSuchElementException(
"No currenlty running exam found for id: " + examId));
}
}
@Override
public Result<Collection<Exam>> getRunningExamsForInstitution(final Long institutionId) {
return this.examDAO.allIdsOfInstituion(institutionId)
.map(col -> col.stream()
.map(examId -> this.examSessionCacheService.getRunningExam(examId))
.filter(exam -> exam != null)
.collect(Collectors.toList()));
}
@Override
public void notifyPing(final Long connectionId, final long timestamp, final int pingNumber) {
final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getActiveClientConnection(connectionId);
if (activeClientConnection != null) {
activeClientConnection.pingMappings
.stream()
.forEach(pingIndicator -> pingIndicator.notifyPing(timestamp, pingNumber));
}
}
@Override
public void notifyClientEvent(final ClientEvent event, final Long connectionId) {
this.eventHandlingStrategy.accept(event);
final ClientConnectionDataInternal activeClientConnection =
this.examSessionCacheService.getActiveClientConnection(connectionId);
if (activeClientConnection != null) {
activeClientConnection.getindicatorMapping(event.eventType)
.stream()
.forEach(indicator -> indicator.notifyValueChange(event));
}
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.model.exam.Indicator.IndicatorType;
import ch.ethz.seb.sebserver.gbl.model.session.IndicatorValueHolder;
@Lazy
@Component(IndicatorType.Names.LAST_PING)
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public final class PingIntervalClientIndicator extends AbstractPingIndicator {
public PingIntervalClientIndicator() {
this.cachingEnabled = true;
this.currentValue = computeValueAt(DateTime.now(DateTimeZone.UTC).getMillis());
}
@Override
public IndicatorType getType() {
return IndicatorType.LAST_PING;
}
@Override
public double getValue() {
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
return now - super.currentValue;
}
@Override
public void notifyValueChange(final IndicatorValueHolder indicatorValueHolder) {
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientEventDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
/** Approach 1 to handle/save client events internally
*
* This saves one on one client event to persistence within separated transaction.
* NOTE: if there are a lot of clients connected, firing events at small intervals like 100ms,
* this is blocking to much because every event is saved within its own SQL commit and also
* in its own transaction.
*
* An advantage of this approach is minimal data loss on server fail. **/
@Lazy
@Component(EventHandlingStrategy.EVENT_CONSUMER_STRATEGY_SINGLE_EVENT_STORE)
@WebServiceProfile
public class SingleEventSaveStrategy implements EventHandlingStrategy {
private final ClientEventDAO clientEventDAO;
public SingleEventSaveStrategy(final ClientEventDAO clientEventDAO) {
this.clientEventDAO = clientEventDAO;
}
@Override
public void accept(final ClientEvent event) {
this.clientEventDAO.save(event);
}
}

View file

@ -19,6 +19,7 @@ sebserver.webservice.api.exam.endpoint.discovery=${sebserver.webservice.api.exam
sebserver.webservice.api.exam.endpoint.v1=${sebserver.webservice.api.exam.endpoint}/v1
sebserver.webservice.api.exam.accessTokenValiditySeconds=1800
sebserver.webservice.api.exam.refreshTokenValiditySeconds=-1
sebserver.webservice.api.exam.event-handling-strategy=ASYNC_BATCH_STORE_STRATEGY
sebserver.webservice.api.pagination.maxPageSize=500
management.endpoints.web.base-path=/actuator

View file

@ -0,0 +1,52 @@
/*
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl;
import static org.junit.Assert.assertEquals;
import org.joda.time.DateTimeUtils;
import org.junit.Test;
import com.fasterxml.jackson.core.JsonProcessingException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
public class PingIntervalClientIndicatorTest {
@Test
public void testCreation() {
DateTimeUtils.setCurrentMillisFixed(1);
final PingIntervalClientIndicator pingIntervalClientIndicator = new PingIntervalClientIndicator();
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
}
@Test
public void testInterval() {
DateTimeUtils.setCurrentMillisFixed(1);
final PingIntervalClientIndicator pingIntervalClientIndicator = new PingIntervalClientIndicator();
assertEquals("0.0", String.valueOf(pingIntervalClientIndicator.getValue()));
DateTimeUtils.setCurrentMillisFixed(10);
assertEquals("9.0", String.valueOf(pingIntervalClientIndicator.getValue()));
}
@Test
public void testSerialization() throws JsonProcessingException {
DateTimeUtils.setCurrentMillisFixed(1);
final PingIntervalClientIndicator pingIntervalClientIndicator = new PingIntervalClientIndicator();
final JSONMapper jsonMapper = new JSONMapper();
final String json = jsonMapper.writeValueAsString(pingIntervalClientIndicator);
assertEquals("{\"indicatorValue\":0.0,\"indicatorType\":\"LAST_PING\"}", json);
}
}