Merge remote-tracking branch 'origin/dev-1.1.0' into development

This commit is contained in:
anhefti 2021-02-17 11:49:26 +01:00
commit 1796ff4a7a
9 changed files with 193 additions and 46 deletions

View file

@ -68,9 +68,19 @@ public class SEBExamConfigImportPopup {
this.pageService = pageService; this.pageService = pageService;
} }
public Function<PageAction, PageAction> importFunction(final boolean newConfig) { public Function<PageAction, PageAction> importFunction() {
return importFunction(null);
}
public Function<PageAction, PageAction> importFunction(final Supplier<String> tabSelectionSupplier) {
return action -> { return action -> {
final boolean newConfig = tabSelectionSupplier == null || tabSelectionSupplier.get() == null;
final PageContext context = (tabSelectionSupplier != null)
? action.pageContext()
.withAttribute(SEBSettingsForm.ATTR_VIEW_INDEX, tabSelectionSupplier.get())
: action.pageContext();
final ModalInputDialog<FormHandle<ConfigurationNode>> dialog = final ModalInputDialog<FormHandle<ConfigurationNode>> dialog =
new ModalInputDialog<FormHandle<ConfigurationNode>>( new ModalInputDialog<FormHandle<ConfigurationNode>>(
action.pageContext().getParent().getShell(), action.pageContext().getParent().getShell(),
@ -79,7 +89,7 @@ public class SEBExamConfigImportPopup {
final ImportFormContext importFormContext = new ImportFormContext( final ImportFormContext importFormContext = new ImportFormContext(
this.pageService, this.pageService,
action.pageContext(), context,
newConfig); newConfig);
dialog.open( dialog.open(
@ -147,18 +157,6 @@ public class SEBExamConfigImportPopup {
if (!configuration.hasError()) { if (!configuration.hasError()) {
context.publishInfo(SEBExamConfigForm.FORM_IMPORT_CONFIRM_TEXT_KEY); context.publishInfo(SEBExamConfigForm.FORM_IMPORT_CONFIRM_TEXT_KEY);
final PageAction action = (newConfig)
? this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.create()
: this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
.create();
this.pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
} else { } else {
final Exception error = configuration.getError(); final Exception error = configuration.getError();
if (error instanceof RestCallError) { if (error instanceof RestCallError) {
@ -177,14 +175,15 @@ public class SEBExamConfigImportPopup {
.notifyImportError(EntityType.CONFIGURATION_NODE, error); .notifyImportError(EntityType.CONFIGURATION_NODE, error);
} }
}); });
return true;
} else {
formHandle.getContext().notifyError(
SEBExamConfigForm.FORM_TITLE,
configuration.getError());
} }
formHandle.getContext().notifyError(
SEBExamConfigForm.FORM_TITLE,
configuration.getError());
} }
reloadPage(newConfig, context);
return true; return true;
} else { } else {
formHandle.getContext().publishPageMessage( formHandle.getContext().publishPageMessage(
@ -194,11 +193,26 @@ public class SEBExamConfigImportPopup {
return false; return false;
} catch (final Exception e) { } catch (final Exception e) {
reloadPage(newConfig, formHandle.getContext());
formHandle.getContext().notifyError(SEBExamConfigForm.FORM_TITLE, e); formHandle.getContext().notifyError(SEBExamConfigForm.FORM_TITLE, e);
return true; return true;
} }
} }
private void reloadPage(final boolean newConfig, final PageContext context) {
final PageAction action = (newConfig)
? this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.create()
: this.pageService.pageActionBuilder(context)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_MODIFY)
.create();
this.pageService.firePageEvent(
new ActionEvent(action),
action.pageContext());
}
private boolean checkInput(final FormHandle<ConfigurationNode> formHandle, final boolean newConfig, private boolean checkInput(final FormHandle<ConfigurationNode> formHandle, final boolean newConfig,
final Form form) { final Form form) {
if (newConfig) { if (newConfig) {

View file

@ -170,7 +170,7 @@ public class SEBExamConfigList implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG) .newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
.withSelect( .withSelect(
configTable.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION), configTable.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
this.sebExamConfigImportPopup.importFunction(true), this.sebExamConfigImportPopup.importFunction(),
EMPTY_SELECTION_TEXT_KEY) EMPTY_SELECTION_TEXT_KEY)
.noEventPropagation() .noEventPropagation()
.publishIf(examConfigGrant::im); .publishIf(examConfigGrant::im);

View file

@ -69,6 +69,8 @@ public class SEBSettingsForm implements TemplateComposer {
private static final Logger log = LoggerFactory.getLogger(SEBSettingsForm.class); private static final Logger log = LoggerFactory.getLogger(SEBSettingsForm.class);
public static final String ATTR_VIEW_INDEX = "VIEW_INDEX";
private static final String VIEW_TEXT_KEY_PREFIX = private static final String VIEW_TEXT_KEY_PREFIX =
"sebserver.examconfig.props.form.views."; "sebserver.examconfig.props.form.views.";
private static final String KEY_SAVE_TO_HISTORY_SUCCESS = private static final String KEY_SAVE_TO_HISTORY_SUCCESS =
@ -194,7 +196,7 @@ public class SEBSettingsForm implements TemplateComposer {
} }
// set selection if available // set selection if available
final String viewIndex = pageContext.getAttribute("VIEW_INDEX"); final String viewIndex = pageContext.getAttribute(ATTR_VIEW_INDEX);
if (StringUtils.isNotBlank(viewIndex)) { if (StringUtils.isNotBlank(viewIndex)) {
try { try {
tabFolder.setSelection(Integer.parseInt(viewIndex)); tabFolder.setSelection(Integer.parseInt(viewIndex));
@ -217,7 +219,7 @@ public class SEBSettingsForm implements TemplateComposer {
.call() .call()
.onError(t -> notifyErrorOnSave(t, pageContext)); .onError(t -> notifyErrorOnSave(t, pageContext));
return action.withAttribute( return action.withAttribute(
"VIEW_INDEX", ATTR_VIEW_INDEX,
String.valueOf(tabFolder.getSelectionIndex())); String.valueOf(tabFolder.getSelectionIndex()));
}) })
.withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS) .withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS)
@ -232,7 +234,7 @@ public class SEBSettingsForm implements TemplateComposer {
.call() .call()
.getOrThrow(); .getOrThrow();
return action.withAttribute( return action.withAttribute(
"VIEW_INDEX", ATTR_VIEW_INDEX,
String.valueOf(tabFolder.getSelectionIndex())); String.valueOf(tabFolder.getSelectionIndex()));
}) })
.withSuccess(KEY_UNDO_SUCCESS) .withSuccess(KEY_UNDO_SUCCESS)
@ -254,7 +256,9 @@ public class SEBSettingsForm implements TemplateComposer {
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_EXISTING_CONFIG) .newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_EXISTING_CONFIG)
.withEntityKey(entityKey) .withEntityKey(entityKey)
.withExec(this.sebExamConfigImportPopup.importFunction(false)) .withExec(this.sebExamConfigImportPopup.importFunction(
() -> String.valueOf(tabFolder.getSelectionIndex())))
.noEventPropagation()
.publishIf(() -> examConfigGrant.iw() && !readonly && !isAttachedToExam) .publishIf(() -> examConfigGrant.iw() && !readonly && !isAttachedToExam)
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP) .newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)

View file

@ -35,13 +35,16 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
private final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper; private final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper;
private final long masterDelayTimeThreshold; private final long masterDelayTimeThreshold;
private final boolean forceMaster;
public WebserviceInfoDAOImpl( public WebserviceInfoDAOImpl(
final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper, final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper,
@Value("${sebserver.webservice.forceMaster:false}") final boolean forceMaster,
@Value("${sebserver.webservice.master.delay.threshold:10000}") final long masterDelayTimeThreshold) { @Value("${sebserver.webservice.master.delay.threshold:10000}") final long masterDelayTimeThreshold) {
this.webserviceServerInfoRecordMapper = webserviceServerInfoRecordMapper; this.webserviceServerInfoRecordMapper = webserviceServerInfoRecordMapper;
this.masterDelayTimeThreshold = masterDelayTimeThreshold; this.masterDelayTimeThreshold = masterDelayTimeThreshold;
this.forceMaster = forceMaster;
} }
@Transactional @Transactional
@ -68,18 +71,24 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
if (masters != null && !masters.isEmpty()) { if (masters != null && !masters.isEmpty()) {
if (masters.size() > 1) { if (masters.size() > 1) {
log.error("There are more then one master registered: ", masters); log.error("There are more then one master registered: ", masters);
log.info("Reset masters and set this webservice as new master"); log.info("Reset masters and set this webservice as new master");
masters.stream()
.forEach(masterRec -> this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective( masters.stream().forEach(masterRec -> this.webserviceServerInfoRecordMapper
new WebserviceServerInfoRecord(masterRec.getId(), null, null, 0, 0L))); .updateByPrimaryKeySelective(
this.setMasterTo(uuid); new WebserviceServerInfoRecord(
return true; masterRec.getId(),
null,
null,
0,
0L)));
return this.setMasterTo(uuid);
} }
final WebserviceServerInfoRecord masterRec = masters.get(0); final WebserviceServerInfoRecord masterRec = masters.get(0);
if (masterRec.getUuid().equals(uuid)) { if (masterRec.getUuid().equals(uuid)) {
// this webservice was the master and update time to remain being master // This webservice is the master. Update time-stamp to remain being master
final long now = Utils.getMillisecondsNow(); final long now = Utils.getMillisecondsNow();
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective( this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(masterRec.getId(), null, null, null, now)); new WebserviceServerInfoRecord(masterRec.getId(), null, null, null, now));
@ -91,20 +100,18 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
return true; return true;
} else { } else {
// Another webservice is master. Check if still alive... // Another webservice is master. Check if still alive...
// Force this service to become master if the other master is not alive anymore
// Or if this service is forced to be the master service
final long now = Utils.getMillisecondsNow(); final long now = Utils.getMillisecondsNow();
final long lastUpdateSince = now - masterRec.getUpdateTime(); final long lastUpdateSince = now - masterRec.getUpdateTime();
if (lastUpdateSince > this.masterDelayTimeThreshold) { if (lastUpdateSince > this.masterDelayTimeThreshold || this.forceMaster) {
log.info("Change webservice master form uuid: {} to uuid: {}", masterRec.getUuid(), uuid); forceMaster(uuid, masterRec.getUuid(), masterRec.getId());
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(masterRec.getId(), null, null, 0, 0L));
setMasterTo(uuid);
return true; return true;
} }
} }
} else { } else {
// if we have no master yet // We have no master yet so set this as master service
setMasterTo(uuid); return setMasterTo(uuid);
return true;
} }
return false; return false;
@ -115,14 +122,41 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
} }
} }
private void setMasterTo(final String uuid) { private void forceMaster(final String uuid, final String otherUUID, final Long otherId) {
log.info("Change webservice master form uuid: {} to uuid: {}", otherUUID, uuid);
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
new WebserviceServerInfoRecord(otherId, null, null, 0, 0L));
setMasterTo(uuid);
}
private boolean setMasterTo(final String uuid) {
log.info("Set webservice {} as master", uuid); log.info("Set webservice {} as master", uuid);
final long now = Utils.getMillisecondsNow(); final long now = Utils.getMillisecondsNow();
this.webserviceServerInfoRecordMapper.updateByExampleSelective(
// check if this is registered
final List<WebserviceServerInfoRecord> entries = this.webserviceServerInfoRecordMapper.selectByExample()
.where(WebserviceServerInfoRecordDynamicSqlSupport.uuid, SqlBuilder.isEqualTo(uuid))
.build()
.execute();
if (entries == null || entries.isEmpty()) {
log.warn("The webservice with uuid: {} is not registered and cannot become a master", uuid);
return false;
}
final Integer execute = this.webserviceServerInfoRecordMapper.updateByExampleSelective(
new WebserviceServerInfoRecord(null, null, null, 1, now)) new WebserviceServerInfoRecord(null, null, null, 1, now))
.where(WebserviceServerInfoRecordDynamicSqlSupport.uuid, SqlBuilder.isEqualTo(uuid)) .where(WebserviceServerInfoRecordDynamicSqlSupport.uuid, SqlBuilder.isEqualTo(uuid))
.build() .build()
.execute(); .execute();
if (execute == null || execute.intValue() <= 0) {
log.error("Failed to update webservice with uuid: {} to become master", uuid);
return false;
}
return true;
} }
@Transactional @Transactional

View file

@ -49,12 +49,6 @@ class ExamSessionControlTask implements DisposableBean {
private final String examTaskCron; private final String examTaskCron;
private final long pingUpdateRate; private final long pingUpdateRate;
// TODO considering to have some caching of running exams end dates here to improve performance
// the end check task has than only to first update missing running exams and then
// check the exam ending within the cached end date of the exam. if an exam has ended by
// applying the check to the cached value, the process can double-check if the end date has
// no change and update if needed or end the exam and remove from cache.
protected ExamSessionControlTask( protected ExamSessionControlTask(
final ExamDAO examDAO, final ExamDAO examDAO,
final SEBClientConnectionService sebClientConnectionService, final SEBClientConnectionService sebClientConnectionService,

View file

@ -32,6 +32,7 @@ sebserver.webservice.api.admin.clientSecret=${sebserver.password}
sebserver.webservice.internalSecret=${sebserver.password} sebserver.webservice.internalSecret=${sebserver.password}
### webservice networking ### webservice networking
sebserver.webservice.forceMaster=false
sebserver.webservice.distributed=false sebserver.webservice.distributed=false
sebserver.webservice.http.external.scheme=https sebserver.webservice.http.external.scheme=https
sebserver.webservice.http.external.servername= sebserver.webservice.http.external.servername=

View file

@ -36,6 +36,8 @@ import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestServiceImpl;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.OAuth2AuthorizationContextHolder; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.OAuth2AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest( @SpringBootTest(
@ -59,13 +61,19 @@ public abstract class GuiIntegrationTest {
protected JSONMapper jsonMapper; protected JSONMapper jsonMapper;
@Autowired @Autowired
protected FilterChainProxy springSecurityFilterChain; protected FilterChainProxy springSecurityFilterChain;
@Autowired
private WebserviceInfoDAO webserviceInfoDAO;
@Autowired
private WebserviceInfo webserviceInfo;
protected MockMvc mockMvc; protected MockMvc mockMvc;
@Before @Before
public void setup() { public void setup() {
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac) this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(this.springSecurityFilterChain).build(); .addFilter(this.springSecurityFilterChain).build();
} }
protected OAuth2AuthorizationContextHolder getAuthorizationContextHolder() { protected OAuth2AuthorizationContextHolder getAuthorizationContextHolder() {

View file

@ -0,0 +1,85 @@
/*
* Copyright (c) 2021 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.integration.api.admin;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
public class WebserviceTest extends AdministrationAPIIntegrationTester {
private static final String WEBSERVICE_1 = "WEBSERVICE_1";
private static final String WEBSERVICE_2 = "WEBSERVICE_2";
@Autowired
private WebserviceInfoDAO webserviceInfoDAO;
@Autowired
private WebserviceInfo webserviceInfo;
@Before
public void init() {
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
this.webserviceInfoDAO.register(WEBSERVICE_1, "0.0.0.1");
this.webserviceInfoDAO.register(WEBSERVICE_2, "0.0.0.1");
}
@After
public void cleanup() {
this.webserviceInfoDAO.unregister(WEBSERVICE_1);
this.webserviceInfoDAO.unregister(WEBSERVICE_2);
this.webserviceInfoDAO.register(
this.webserviceInfo.getWebserviceUUID(),
this.webserviceInfo.getLocalHostAddress());
}
@Test
public void testFistBecomeMaster() {
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
}
@Test
public void testUnregister_OtherBecomeMaster() {
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertTrue(this.webserviceInfoDAO.unregister(WEBSERVICE_1));
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
}
@Test
public void testOtherBecomeMasterAfterTimout() {
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_1));
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
try {
Thread.sleep(5000);
} catch (final InterruptedException e) {
}
// Still not master
assertFalse(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
try {
Thread.sleep(6000);
} catch (final InterruptedException e) {
}
assertTrue(this.webserviceInfoDAO.isMaster(WEBSERVICE_2));
}
}

View file

@ -51,6 +51,8 @@ import ch.ethz.seb.sebserver.SEBServer;
import ch.ethz.seb.sebserver.WebSecurityConfig; import ch.ethz.seb.sebserver.WebSecurityConfig;
import ch.ethz.seb.sebserver.gbl.api.API; import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.webservice.WebserviceInfo;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.WebserviceInfoDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService; import ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.ClientConfigService;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.AdminAPIClientDetails;
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration; import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration;
@ -74,6 +76,10 @@ public abstract class ExamAPIIntegrationTester {
protected JSONMapper jsonMapper; protected JSONMapper jsonMapper;
@Autowired @Autowired
protected FilterChainProxy springSecurityFilterChain; protected FilterChainProxy springSecurityFilterChain;
@Autowired
private WebserviceInfoDAO webserviceInfoDAO;
@Autowired
private WebserviceInfo webserviceInfo;
protected MockMvc mockMvc; protected MockMvc mockMvc;
@ -82,6 +88,7 @@ public abstract class ExamAPIIntegrationTester {
@Before @Before
public void setup() { public void setup() {
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac) this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
.addFilter(this.springSecurityFilterChain).build(); .addFilter(this.springSecurityFilterChain).build();
// clear all caches before a test // clear all caches before a test