SEBSERV-62 service implementations and preparing for config download

This commit is contained in:
anhefti 2019-06-28 12:46:36 +02:00
parent 8d9cf58e69
commit 44688429bb
21 changed files with 644 additions and 181 deletions

View file

@ -9,7 +9,10 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface ClientConnectionDAO extends EntityDAO<ClientConnection, ClientConnection> {
Result<ClientConnection> byConnectionToken(Long institutionId, String connectionToken);
}

View file

@ -183,6 +183,37 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO {
});
}
@Override
@Transactional(readOnly = true)
public Result<ClientConnection> byConnectionToken(
final Long institutionId,
final String connectionToken) {
return Result.tryCatch(() -> {
final List<ClientConnectionRecord> list = this.clientConnectionRecordMapper
.selectByExample()
.where(
ClientConnectionRecordDynamicSqlSupport.institutionId,
SqlBuilder.isEqualTo(institutionId))
.and(
ClientConnectionRecordDynamicSqlSupport.connectionToken,
SqlBuilder.isEqualTo(connectionToken))
.build()
.execute();
if (list.isEmpty()) {
throw new ResourceNotFoundException(EntityType.CLIENT_CONNECTION, "connectionToken");
}
if (list.size() > 1) {
throw new IllegalStateException("Only one ClientConnection expected but there are: " + list.size());
}
return list.get(0);
})
.flatMap(ClientConnectionDAOImpl::toDomainModel);
}
private Result<ClientConnectionRecord> recordById(final Long id) {
return Result.tryCatch(() -> {

View file

@ -14,6 +14,9 @@ import java.security.cert.Certificate;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.scheduling.annotation.Async;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.util.Utils;
/** Used for SEB Configuration encryption and decryption */
@ -58,6 +61,7 @@ public interface SebConfigEncryptionService {
* @param output the output data stream to write the cipher text to
* @param input the input stream to read the plain text from
* @param context the SebConfigEncryptionContext to access strategy specific data needed for encryption */
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void streamEncrypted(
final OutputStream output,
final InputStream input,
@ -68,6 +72,7 @@ public interface SebConfigEncryptionService {
* @param output the output data stream to write encrypted plain text to
* @param input the input stream to read the cipher text from
* @param context the SebConfigEncryptionContext to access strategy specific data needed for encryption */
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void streamDecrypted(
final OutputStream output,
final InputStream input,

View file

@ -48,6 +48,14 @@ public interface SebExamConfigService {
* @param configExamMappingId The identifier of the Exam Configuration Mapping */
void exportForExam(OutputStream out, Long configExamMappingId);
/** Used to export the default SEB Exam Configuration for a given exam identifier.
* either with encryption if defined or as plain text within the SEB Configuration format
* as described here: https://www.safeexambrowser.org/developer/seb-file-format.html
*
* @param out The output stream to write the export data to
* @param examId the exam identifier */
void exportDefaultForExam(OutputStream out, Long examId);
/** TODO */
String generateConfigKey(Long configurationNodeId);

View file

@ -44,8 +44,7 @@ public interface XMLValueConverter {
void convertToXML(
OutputStream out,
ConfigurationAttribute attribute,
ConfigurationValue value,
XMLValueConverterService xmlValueConverterService) throws IOException;
ConfigurationValue value) throws IOException;
default String extractName(final ConfigurationAttribute attribute) {
final int lastIndexOf = attribute.name.lastIndexOf('.');
@ -56,4 +55,11 @@ public interface XMLValueConverter {
}
}
/** This can be overwritten if a XMLValueConverter needs the XMLValueConverterService.
* The XMLValueConverterService is then injected by its self on initialization.
*
* @param xmlValueConverterService */
default void init(final XMLValueConverterService xmlValueConverterService) {
}
}

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.sebconfig.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.async.AsyncServiceSpringConfig;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Component
@WebServiceProfile
public class ExamConfigIO {
private static final Logger log = LoggerFactory.getLogger(ExamConfigIO.class);
private final ConfigurationAttributeDAO configurationAttributeDAO;
private final ConfigurationValueDAO configurationValueDAO;
private final ConfigurationDAO configurationDAO;
private final XMLValueConverterService xmlValueConverterService;
protected ExamConfigIO(
final ConfigurationAttributeDAO configurationAttributeDAO,
final ConfigurationValueDAO configurationValueDAO,
final ConfigurationDAO configurationDAO,
final XMLValueConverterService xmlValueConverterService) {
this.configurationAttributeDAO = configurationAttributeDAO;
this.configurationValueDAO = configurationValueDAO;
this.configurationDAO = configurationDAO;
this.xmlValueConverterService = xmlValueConverterService;
}
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void exportPlainXML(final OutputStream out, final Long institutionId, final Long configurationNodeId) {
// get all defined root configuration attributes
final Map<Long, ConfigurationAttribute> attributes = this.configurationAttributeDAO.getAllRootAttributes()
.getOrThrow()
.stream()
.collect(Collectors.toMap(
ConfigurationAttribute::getId,
Function.identity()));
final List<ConfigurationAttribute> sortedAttributes = attributes
.values()
.stream()
.sorted()
.collect(Collectors.toList());
// get follow-up configurationId for given configurationNodeId
final Long configurationId = this.configurationDAO
.getFollowupConfiguration(configurationNodeId)
.getOrThrow().id;
// get all values for that attributes for given configurationId
final Map<Long, ConfigurationValue> values = this.configurationValueDAO
.allRootAttributeValues(institutionId, configurationId)
.getOrThrow()
.stream()
.collect(Collectors.toMap(
ConfigurationValue::getAttributeId,
Function.identity()));
try {
// write headers
out.write(Constants.XML_VERSION_HEADER_UTF_8);
out.write(Constants.XML_DOCTYPE_HEADER_UTF_8);
// plist open
out.write(Constants.XML_PLIST_START_V1_UTF_8);
out.write(Constants.XML_DICT_START_UTF_8);
// write attributes
for (final ConfigurationAttribute attribute : sortedAttributes) {
final ConfigurationValue configurationValue = values.get(attribute.id);
if (configurationValue != null) {
this.xmlValueConverterService.getXMLConverter(attribute).convertToXML(
out,
attribute,
configurationValue);
}
}
// plist close
out.write(Constants.XML_DICT_END_UTF_8);
out.write(Constants.XML_PLIST_END_UTF_8);
out.flush();
} catch (final IOException e) {
log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e);
try {
out.flush();
} catch (final IOException e1) {
log.error("Unable to flush output stream after error");
}
} finally {
IOUtils.closeQuietly(out);
}
}
@Async(AsyncServiceSpringConfig.EXECUTOR_BEAN_NAME)
void importPlainXML(final InputStream in, final Long institutionId, final Long configurationNodeId) {
// TODO version 1
}
}

View file

@ -82,7 +82,6 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption
pin.close();
pout.flush();
pout.close();
output.flush();
} catch (final IOException e) {
log.error("Error while stream encrypted data: ", e);
@ -135,7 +134,6 @@ public final class SebConfigEncryptionServiceImpl implements SebConfigEncryption
pin.close();
pout.flush();
pout.close();
output.flush();
} catch (final IOException e) {
log.error("Error while stream decrypted data: ", e);

View file

@ -10,89 +10,45 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationTableValues;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationAttributeDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ConfigurationValueDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ConfigurationValueValidator;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Service
@WebServiceProfile
public class SebExamConfigServiceImpl implements SebExamConfigService, XMLValueConverterService {
public class SebExamConfigServiceImpl implements SebExamConfigService {
private static final Logger log = LoggerFactory.getLogger(SebExamConfigServiceImpl.class);
private final ExamConfigIO examConfigIO;
private final ConfigurationAttributeDAO configurationAttributeDAO;
private final ConfigurationValueDAO configurationValueDAO;
private final ConfigurationDAO configurationDAO;
private final Collection<ConfigurationValueValidator> validators;
private final Map<String, XMLValueConverter> convertersByAttributeName;
private final Map<AttributeType, XMLValueConverter> convertersByAttributeType;
protected SebExamConfigServiceImpl(
final ExamConfigIO examConfigIO,
final ConfigurationAttributeDAO configurationAttributeDAO,
final ConfigurationValueDAO configurationValueDAO,
final ConfigurationDAO configurationDAO,
final Collection<ConfigurationValueValidator> validators,
final Collection<XMLValueConverter> converters) {
final Collection<ConfigurationValueValidator> validators) {
this.examConfigIO = examConfigIO;
this.configurationAttributeDAO = configurationAttributeDAO;
this.configurationValueDAO = configurationValueDAO;
this.configurationDAO = configurationDAO;
this.validators = validators;
this.convertersByAttributeName = new HashMap<>();
this.convertersByAttributeType = new HashMap<>();
for (final XMLValueConverter converter : converters) {
if (StringUtils.isNotBlank(converter.name())) {
this.convertersByAttributeName.put(converter.name(), converter);
}
for (final AttributeType aType : converter.types()) {
if (this.convertersByAttributeType.containsKey(aType)) {
log.warn(
"Unexpected state in inititalization: A XMLValueConverter for AttributeType {} exists already: {}",
aType,
converter);
}
this.convertersByAttributeType.put(aType, converter);
}
}
}
@Override
public XMLValueConverter getXMLConverter(final ConfigurationAttribute attribute) {
if (this.convertersByAttributeName.containsKey(attribute.name)) {
return this.convertersByAttributeName.get(attribute.name);
}
if (this.convertersByAttributeType.containsKey(attribute.type)) {
return this.convertersByAttributeType.get(attribute.type);
}
throw new IllegalStateException("No XMLValueConverter found for attribute: " + attribute);
}
@Override
@ -125,68 +81,45 @@ public class SebExamConfigServiceImpl implements SebExamConfigService, XMLValueC
final Long institutionId,
final Long configurationNodeId) {
// get all defined root configuration attributes
final Map<Long, ConfigurationAttribute> attributes = this.configurationAttributeDAO.getAllRootAttributes()
.getOrThrow()
.stream()
.collect(Collectors.toMap(
ConfigurationAttribute::getId,
Function.identity()));
final List<ConfigurationAttribute> sortedAttributes = attributes
.values()
.stream()
.sorted()
.collect(Collectors.toList());
// get follow-up configurationId for given configurationNodeId
final Long configurationId = this.configurationDAO
.getFollowupConfiguration(configurationNodeId)
.getOrThrow().id;
// get all values for that attributes for given configurationId
final Map<Long, ConfigurationValue> values = this.configurationValueDAO
.allRootAttributeValues(institutionId, configurationId)
.getOrThrow()
.stream()
.collect(Collectors.toMap(
ConfigurationValue::getAttributeId,
Function.identity()));
if (log.isDebugEnabled()) {
log.debug("Start to stream plain text SEB clonfiguration data");
}
PipedOutputStream pout = null;
PipedInputStream pin = null;
try {
// write headers
out.write(Constants.XML_VERSION_HEADER_UTF_8);
out.write(Constants.XML_DOCTYPE_HEADER_UTF_8);
pout = new PipedOutputStream();
pin = new PipedInputStream(pout);
// plist open
out.write(Constants.XML_PLIST_START_V1_UTF_8);
out.write(Constants.XML_DICT_START_UTF_8);
this.examConfigIO.exportPlainXML(pout, institutionId, configurationNodeId);
// write attributes
for (final ConfigurationAttribute attribute : sortedAttributes) {
final ConfigurationValue configurationValue = values.get(attribute.id);
if (configurationValue != null) {
this.getXMLConverter(attribute).convertToXML(
out,
attribute,
configurationValue,
this);
}
}
IOUtils.copyLarge(pin, out);
// plist close
out.write(Constants.XML_DICT_END_UTF_8);
out.write(Constants.XML_PLIST_END_UTF_8);
out.flush();
pin.close();
pout.flush();
pout.close();
} catch (final IOException e) {
log.error("Unexpected error while trying to write SEB Exam Configuration XML to output stream: ", e);
log.error("Error while stream plain text SEB clonfiguration data: ", e);
} finally {
try {
out.flush();
if (pin != null)
pin.close();
} catch (final IOException e1) {
log.error("Unable to flush output stream after error");
log.error("Failed to close PipedInputStream: ", e1);
}
try {
if (pout != null)
pout.close();
} catch (final IOException e1) {
log.error("Failed to close PipedOutputStream: ", e1);
}
if (log.isDebugEnabled()) {
log.debug("Finished to stream plain text SEB clonfiguration data");
}
}
}
@Override
@ -195,6 +128,12 @@ public class SebExamConfigServiceImpl implements SebExamConfigService, XMLValueC
}
@Override
public void exportDefaultForExam(final OutputStream out, final Long examId) {
// TODO Auto-generated method stub
}
@Override
public String generateConfigKey(final Long configurationNodeId) {
// TODO https://www.safeexambrowser.org/developer/seb-config-key.html

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.sebconfig.impl;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.AttributeType;
import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationAttribute;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Service
@WebServiceProfile
public class XMLValueConverterServiceImpl implements XMLValueConverterService {
private static final Logger log = LoggerFactory.getLogger(XMLValueConverterServiceImpl.class);
private final Map<String, XMLValueConverter> convertersByAttributeName;
private final Map<AttributeType, XMLValueConverter> convertersByAttributeType;
public XMLValueConverterServiceImpl(final Collection<XMLValueConverter> converters) {
this.convertersByAttributeName = new HashMap<>();
this.convertersByAttributeType = new HashMap<>();
for (final XMLValueConverter converter : converters) {
converter.init(this);
if (StringUtils.isNotBlank(converter.name())) {
this.convertersByAttributeName.put(converter.name(), converter);
}
for (final AttributeType aType : converter.types()) {
if (this.convertersByAttributeType.containsKey(aType)) {
log.warn(
"Unexpected state in inititalization: A XMLValueConverter for AttributeType {} exists already: {}",
aType,
converter);
}
this.convertersByAttributeType.put(aType, converter);
}
}
}
@Override
public XMLValueConverter getXMLConverter(final ConfigurationAttribute attribute) {
if (this.convertersByAttributeName.containsKey(attribute.name)) {
return this.convertersByAttributeName.get(attribute.name);
}
if (this.convertersByAttributeType.containsKey(attribute.type)) {
return this.convertersByAttributeType.get(attribute.type);
}
throw new IllegalStateException("No XMLValueConverter found for attribute: " + attribute);
}
}

View file

@ -26,7 +26,6 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Component
@ -58,8 +57,7 @@ public class ArrayOfStringConverter implements XMLValueConverter {
public void convertToXML(
final OutputStream out,
final ConfigurationAttribute attribute,
final ConfigurationValue value,
final XMLValueConverterService xmlValueConverterService) throws IOException {
final ConfigurationValue value) throws IOException {
final String val = (value.value != null) ? value.value : attribute.getDefaultValue();
if (StringUtils.isNotBlank(val)) {

View file

@ -26,7 +26,6 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Component
@ -53,8 +52,7 @@ public class BooleanConverter implements XMLValueConverter {
public void convertToXML(
final OutputStream out,
final ConfigurationAttribute attribute,
final ConfigurationValue value,
final XMLValueConverterService xmlValueConverterService) throws IOException {
final ConfigurationValue value) throws IOException {
out.write(Utils.toByteArray(
String.format(

View file

@ -25,7 +25,6 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Component
@ -56,8 +55,7 @@ public class IntegerConverter implements XMLValueConverter {
public void convertToXML(
final OutputStream out,
final ConfigurationAttribute attribute,
final ConfigurationValue value,
final XMLValueConverterService xmlValueConverterService) throws IOException {
final ConfigurationValue value) throws IOException {
final String val = (value.value != null) ? value.value : attribute.getDefaultValue();
if (StringUtils.isNotBlank(val)) {

View file

@ -23,7 +23,6 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Component
@ -48,8 +47,7 @@ public class KioskModeConverter implements XMLValueConverter {
public void convertToXML(
final OutputStream out,
final ConfigurationAttribute attribute,
final ConfigurationValue value,
final XMLValueConverterService xmlValueConverterService) throws IOException {
final ConfigurationValue value) throws IOException {
final String val = value.getValue();
out.write(Utils.toByteArray(

View file

@ -25,7 +25,6 @@ import ch.ethz.seb.sebserver.gbl.model.sebconfig.ConfigurationValue;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverter;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.XMLValueConverterService;
@Lazy
@Component
@ -57,8 +56,7 @@ public class StringConverter implements XMLValueConverter {
public void convertToXML(
final OutputStream out,
final ConfigurationAttribute attribute,
final ConfigurationValue value,
final XMLValueConverterService xmlValueConverterService) throws IOException {
final ConfigurationValue value) throws IOException {
final String val = (value.value != null) ? value.value : attribute.getDefaultValue();
if (StringUtils.isNotBlank(val)) {

View file

@ -54,6 +54,7 @@ public class TableConverter implements XMLValueConverter {
private final ConfigurationAttributeDAO configurationAttributeDAO;
private final ConfigurationValueDAO configurationValueDAO;
private XMLValueConverterService xmlValueConverterService;
public TableConverter(
final ConfigurationAttributeDAO configurationAttributeDAO,
@ -63,6 +64,11 @@ public class TableConverter implements XMLValueConverter {
this.configurationValueDAO = configurationValueDAO;
}
@Override
public void init(final XMLValueConverterService xmlValueConverterService) {
this.xmlValueConverterService = xmlValueConverterService;
}
@Override
public Set<AttributeType> types() {
return SUPPORTED_TYPES;
@ -77,8 +83,7 @@ public class TableConverter implements XMLValueConverter {
public void convertToXML(
final OutputStream out,
final ConfigurationAttribute attribute,
final ConfigurationValue value,
final XMLValueConverterService xmlValueConverterService) throws IOException {
final ConfigurationValue value) throws IOException {
out.write(Utils.toByteArray(String.format(KEY_TEMPLATE, extractName(attribute))));
@ -101,7 +106,7 @@ public class TableConverter implements XMLValueConverter {
out,
getAttributes(attribute),
values,
xmlValueConverterService);
this.xmlValueConverterService);
if (attribute.type != AttributeType.COMPOSITE_TABLE) {
out.write(ARRAY_END);
@ -134,7 +139,7 @@ public class TableConverter implements XMLValueConverter {
for (final ConfigurationValue value : rowValues) {
final ConfigurationAttribute attr = attributeMap.get(value.attributeId);
final XMLValueConverter converter = xmlValueConverterService.getXMLConverter(attr);
converter.convertToXML(out, attr, value, xmlValueConverterService);
converter.convertToXML(out, attr, value);
}
out.write(DICT_END);
out.flush();

View file

@ -11,19 +11,31 @@ 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;
/** A Service to handle running exam sessions */
public interface ExamSessionService {
/** Indicates whether an Exam is currently running or not.
*
* @param examId the PK of the Exam to test
* @return true if an Exam is currently running */
boolean isExamRunning(Long examId);
/** Use this to get currently running exams by exam identifier.
* This test first if the Exam with the given identifier is currently/still
* running. If true the Exam is returned within the result. Otherwise the
* returned Result contains a NoSuchElementException error.
*
* @param examId The Exam identifier (PK)
* @return Result referencing the running Exam or an error if the Exam not exists or is not currently running */
Result<Exam> getRunningExam(Long examId);
/** Gets all all currently running Exams for a particular Institution.
*
* @param institutionId the Institution identifier
* @return Result referencing the list of all currently running Exams of the institution or to an error if
* happened. */
Result<Collection<Exam>> getRunningExamsForInstitution(Long institutionId);
void notifyPing(Long connectionId, long timestamp, int pingNumber);
void notifyClientEvent(final ClientEvent event, Long connectionId);
}

View file

@ -9,17 +9,27 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.util.Result;
public interface SebClientConnectionService {
Result<ClientConnection> createClientConnection(Long instituionId, Long examId);
Result<ClientConnection> createClientConnection(
Long institutionId,
String clientAddress,
Long examId);
Result<ClientConnection> establishClientConnection(
final Long institutionId,
String connectionToken,
Long examId,
final String clientAddress,
String userSessionId);
Result<ClientConnection> closeConnection(String connectionToken);
void notifyPing(Long connectionId, long timestamp, int pingNumber);
void notifyClientEvent(final ClientEvent event, Long connectionId);
}

View file

@ -21,6 +21,7 @@ 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;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.SebExamConfigService;
@Lazy
@Service
@ -29,21 +30,25 @@ 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";
public static final String CACHE_NAME_SEB_CONFIG_EXAM = "SEB_CONFIG_EXAM";
private static final Logger log = LoggerFactory.getLogger(ExamSessionCacheService.class);
private final ExamDAO examDAO;
private final ClientConnectionDAO clientConnectionDAO;
private final ClientIndicatorFactory clientIndicatorFactory;
private final SebExamConfigService sebExamConfigService;
protected ExamSessionCacheService(
final ExamDAO examDAO,
final ClientConnectionDAO clientConnectionDAO,
final ClientIndicatorFactory clientIndicatorFactory) {
final ClientIndicatorFactory clientIndicatorFactory,
final SebExamConfigService sebExamConfigService) {
this.examDAO = examDAO;
this.clientConnectionDAO = clientConnectionDAO;
this.clientIndicatorFactory = clientIndicatorFactory;
this.sebExamConfigService = sebExamConfigService;
}
@Cacheable(
@ -123,10 +128,28 @@ public class ExamSessionCacheService {
@CacheEvict(
cacheNames = CACHE_NAME_ACTIVE_CLIENT_CONNECTION,
key = "#connectionId")
void evict(final Long connectionId) {
void evictClientConnection(final Long connectionId) {
if (log.isDebugEnabled()) {
log.debug("Eviction of ClientConnectionData from cache: {}", connectionId);
}
}
@Cacheable(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId",
unless = "#result == null")
InMemorySebConfig getDefaultSebConfigForExam(final Long examId) {
// TODO
return null;
}
@CacheEvict(
cacheNames = CACHE_NAME_SEB_CONFIG_EXAM,
key = "#examId")
void evictDefaultSebConfig(final Long examId) {
if (log.isDebugEnabled()) {
log.debug("Eviction of default SEB Configuration from cache for exam: {}", examId);
}
}
}

View file

@ -12,19 +12,15 @@ 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;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SebClientConnectionService;
@Lazy
@Service
@ -33,27 +29,14 @@ 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) {
final SebClientConnectionService sebClientConnectionService) {
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
@ -85,30 +68,4 @@ public class ExamSessionServiceImpl implements ExamSessionService {
.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,67 @@
/*
* 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;
public final class InMemorySebConfig {
public final Long configId;
public final Long examId;
private final byte[] data;
protected InMemorySebConfig(final Long configId, final Long examId, final byte[] data) {
super();
this.configId = configId;
this.examId = examId;
this.data = data;
}
public Long getConfigId() {
return this.configId;
}
public Long getExamId() {
return this.examId;
}
public byte[] getData() {
return this.data;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((this.configId == null) ? 0 : this.configId.hashCode());
result = prime * result + ((this.examId == null) ? 0 : this.examId.hashCode());
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final InMemorySebConfig other = (InMemorySebConfig) obj;
if (this.configId == null) {
if (other.configId != null)
return false;
} else if (!this.configId.equals(other.configId))
return false;
if (this.examId == null) {
if (other.examId != null)
return false;
} else if (!this.examId.equals(other.examId))
return false;
return true;
}
}

View file

@ -0,0 +1,206 @@
/*
* 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.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection;
import ch.ethz.seb.sebserver.gbl.model.session.ClientEvent;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ClientConnectionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.EventHandlingStrategy;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.SebClientConnectionService;
import io.micrometer.core.instrument.util.StringUtils;
public class SebClientConnectionServiceImpl implements SebClientConnectionService {
private static final Logger log = LoggerFactory.getLogger(SebClientConnectionServiceImpl.class);
private final ExamSessionService examSessionService;
private final ExamSessionCacheService examSessionCacheService;
private final EventHandlingStrategy eventHandlingStrategy;
private final ClientConnectionDAO clientConnectionDAO;
protected SebClientConnectionServiceImpl(
final ExamSessionService examSessionService,
final ExamSessionCacheService examSessionCacheService,
final ClientConnectionDAO clientConnectionDAO,
final Environment environment,
final ApplicationContext applicationContext) {
this.examSessionService = examSessionService;
this.examSessionCacheService = examSessionCacheService;
this.clientConnectionDAO = clientConnectionDAO;
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 Result<ClientConnection> createClientConnection(
final Long institutionId,
final String clientAddress,
final Long examId) {
return Result.tryCatch(() -> {
if (log.isDebugEnabled()) {
log.debug("SEB client connection attempt, create ClientConnection for instituion {} and exam: {}",
institutionId,
examId);
}
// Integrity check: in case examId is provided is the specified exam running?
if (examId != null && !this.examSessionService.isExamRunning(examId)) {
examNotRunningException(examId);
}
// Create ClientConnection in status CONNECTION_REQUESTED for further processing
final String connectionToken = createToken();
final ClientConnection clientConnection = this.clientConnectionDAO.createNew(new ClientConnection(
null,
institutionId,
examId,
ClientConnection.ConnectionStatus.CONNECTION_REQUESTED,
connectionToken,
null,
clientAddress,
null))
.getOrThrow();
if (log.isDebugEnabled()) {
log.debug("New ClientConnection created: {}", clientConnection);
}
return clientConnection;
});
}
@Override
public Result<ClientConnection> establishClientConnection(
final Long institutionId,
final String connectionToken,
final Long examId,
final String clientAddress,
final String userSessionId) {
return Result.tryCatch(() -> {
if (log.isDebugEnabled()) {
log.debug(
"SEB client connection, establish ClientConnection for instituion {} and exam: {} and userSessionId: {}",
institutionId,
examId,
userSessionId);
}
// Integrity check: is the specified exam running?
if (!this.examSessionService.isExamRunning(examId)) {
examNotRunningException(examId);
}
final ClientConnection clientConnection = this.clientConnectionDAO
.byConnectionToken(institutionId, connectionToken)
.get(t -> {
// TODO: This indicates some irregularity on SEB-Client connection attempt.
// Later we should handle this more accurately, and maybe indicate this to the monitoring board
// For example; check if there is already a connection for the userIdentifier and
// if true in which state it is.
log.debug("Unable to connect SEB-Client {} to exam {}",
clientAddress,
this.examSessionService.getRunningExam(examId).map(exam -> exam.name));
throw new IllegalStateException("Unable to connect SEB-Client to exam");
});
// Integrity checks:
if (!institutionId.equals(clientConnection.institutionId)) {
log.error("Instituion integrity violation with institution: {} on clientConnection: {}",
institutionId,
clientConnection);
throw new IllegalAccessError("Instituion integrity violation");
}
if (clientConnection.examId != null && !examId.equals(clientConnection.examId)) {
log.error("Exam integrity violation with examId: {} on clientConnection: {}",
examId,
clientConnection);
throw new IllegalAccessError("Exam integrity violation");
}
final ClientConnection updatedClientConnection = this.clientConnectionDAO.save(new ClientConnection(
clientConnection.id,
clientConnection.institutionId,
clientConnection.examId,
ClientConnection.ConnectionStatus.ESTABLISHED,
null,
userSessionId,
null,
null)).getOrThrow();
return updatedClientConnection;
});
}
@Override
public Result<ClientConnection> closeConnection(final String connectionToken) {
// TODO Auto-generated method stub
return null;
}
@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));
}
}
// TODO maybe we need a stronger connectionToken but for now a simple UUID is used
private String createToken() {
return UUID.randomUUID().toString();
}
private void examNotRunningException(final Long examId) {
log.error("The exam {} is not running", examId);
throw new IllegalStateException("The exam " + examId + " is not running");
}
}