SEBSERV-62 Model, DAO and service implementation
This commit is contained in:
parent
54eea01dc2
commit
3d67b4ed9c
32 changed files with 1433 additions and 13 deletions
6
pom.xml
6
pom.xml
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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> {
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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());
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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";
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue