Merge remote-tracking branch 'origin/dev-1.1.0' into development
This commit is contained in:
commit
1796ff4a7a
9 changed files with 193 additions and 46 deletions
|
@ -68,9 +68,19 @@ public class SEBExamConfigImportPopup {
|
|||
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 -> {
|
||||
|
||||
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 =
|
||||
new ModalInputDialog<FormHandle<ConfigurationNode>>(
|
||||
action.pageContext().getParent().getShell(),
|
||||
|
@ -79,7 +89,7 @@ public class SEBExamConfigImportPopup {
|
|||
|
||||
final ImportFormContext importFormContext = new ImportFormContext(
|
||||
this.pageService,
|
||||
action.pageContext(),
|
||||
context,
|
||||
newConfig);
|
||||
|
||||
dialog.open(
|
||||
|
@ -147,18 +157,6 @@ public class SEBExamConfigImportPopup {
|
|||
|
||||
if (!configuration.hasError()) {
|
||||
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 {
|
||||
final Exception error = configuration.getError();
|
||||
if (error instanceof RestCallError) {
|
||||
|
@ -177,14 +175,15 @@ public class SEBExamConfigImportPopup {
|
|||
.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;
|
||||
} else {
|
||||
formHandle.getContext().publishPageMessage(
|
||||
|
@ -194,11 +193,26 @@ public class SEBExamConfigImportPopup {
|
|||
|
||||
return false;
|
||||
} catch (final Exception e) {
|
||||
reloadPage(newConfig, formHandle.getContext());
|
||||
formHandle.getContext().notifyError(SEBExamConfigForm.FORM_TITLE, e);
|
||||
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,
|
||||
final Form form) {
|
||||
if (newConfig) {
|
||||
|
|
|
@ -170,7 +170,7 @@ public class SEBExamConfigList implements TemplateComposer {
|
|||
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_NEW_CONFIG)
|
||||
.withSelect(
|
||||
configTable.getGrantedSelection(this.currentUser, NO_MODIFY_PRIVILEGE_ON_OTHER_INSTITUTION),
|
||||
this.sebExamConfigImportPopup.importFunction(true),
|
||||
this.sebExamConfigImportPopup.importFunction(),
|
||||
EMPTY_SELECTION_TEXT_KEY)
|
||||
.noEventPropagation()
|
||||
.publishIf(examConfigGrant::im);
|
||||
|
|
|
@ -69,6 +69,8 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
|
||||
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 =
|
||||
"sebserver.examconfig.props.form.views.";
|
||||
private static final String KEY_SAVE_TO_HISTORY_SUCCESS =
|
||||
|
@ -194,7 +196,7 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
}
|
||||
|
||||
// set selection if available
|
||||
final String viewIndex = pageContext.getAttribute("VIEW_INDEX");
|
||||
final String viewIndex = pageContext.getAttribute(ATTR_VIEW_INDEX);
|
||||
if (StringUtils.isNotBlank(viewIndex)) {
|
||||
try {
|
||||
tabFolder.setSelection(Integer.parseInt(viewIndex));
|
||||
|
@ -217,7 +219,7 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
.call()
|
||||
.onError(t -> notifyErrorOnSave(t, pageContext));
|
||||
return action.withAttribute(
|
||||
"VIEW_INDEX",
|
||||
ATTR_VIEW_INDEX,
|
||||
String.valueOf(tabFolder.getSelectionIndex()));
|
||||
})
|
||||
.withSuccess(KEY_SAVE_TO_HISTORY_SUCCESS)
|
||||
|
@ -232,7 +234,7 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
.call()
|
||||
.getOrThrow();
|
||||
return action.withAttribute(
|
||||
"VIEW_INDEX",
|
||||
ATTR_VIEW_INDEX,
|
||||
String.valueOf(tabFolder.getSelectionIndex()));
|
||||
})
|
||||
.withSuccess(KEY_UNDO_SUCCESS)
|
||||
|
@ -254,7 +256,9 @@ public class SEBSettingsForm implements TemplateComposer {
|
|||
|
||||
.newAction(ActionDefinition.SEB_EXAM_CONFIG_IMPORT_TO_EXISTING_CONFIG)
|
||||
.withEntityKey(entityKey)
|
||||
.withExec(this.sebExamConfigImportPopup.importFunction(false))
|
||||
.withExec(this.sebExamConfigImportPopup.importFunction(
|
||||
() -> String.valueOf(tabFolder.getSelectionIndex())))
|
||||
.noEventPropagation()
|
||||
.publishIf(() -> examConfigGrant.iw() && !readonly && !isAttachedToExam)
|
||||
|
||||
.newAction(ActionDefinition.SEB_EXAM_CONFIG_VIEW_PROP)
|
||||
|
|
|
@ -35,13 +35,16 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
|
|||
|
||||
private final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper;
|
||||
private final long masterDelayTimeThreshold;
|
||||
private final boolean forceMaster;
|
||||
|
||||
public WebserviceInfoDAOImpl(
|
||||
final WebserviceServerInfoRecordMapper webserviceServerInfoRecordMapper,
|
||||
@Value("${sebserver.webservice.forceMaster:false}") final boolean forceMaster,
|
||||
@Value("${sebserver.webservice.master.delay.threshold:10000}") final long masterDelayTimeThreshold) {
|
||||
|
||||
this.webserviceServerInfoRecordMapper = webserviceServerInfoRecordMapper;
|
||||
this.masterDelayTimeThreshold = masterDelayTimeThreshold;
|
||||
this.forceMaster = forceMaster;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
@ -68,18 +71,24 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
|
|||
|
||||
if (masters != null && !masters.isEmpty()) {
|
||||
if (masters.size() > 1) {
|
||||
|
||||
log.error("There are more then one master registered: ", masters);
|
||||
log.info("Reset masters and set this webservice as new master");
|
||||
masters.stream()
|
||||
.forEach(masterRec -> this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
|
||||
new WebserviceServerInfoRecord(masterRec.getId(), null, null, 0, 0L)));
|
||||
this.setMasterTo(uuid);
|
||||
return true;
|
||||
|
||||
masters.stream().forEach(masterRec -> this.webserviceServerInfoRecordMapper
|
||||
.updateByPrimaryKeySelective(
|
||||
new WebserviceServerInfoRecord(
|
||||
masterRec.getId(),
|
||||
null,
|
||||
null,
|
||||
0,
|
||||
0L)));
|
||||
return this.setMasterTo(uuid);
|
||||
}
|
||||
|
||||
final WebserviceServerInfoRecord masterRec = masters.get(0);
|
||||
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();
|
||||
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
|
||||
new WebserviceServerInfoRecord(masterRec.getId(), null, null, null, now));
|
||||
|
@ -91,20 +100,18 @@ public class WebserviceInfoDAOImpl implements WebserviceInfoDAO {
|
|||
return true;
|
||||
} else {
|
||||
// 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 lastUpdateSince = now - masterRec.getUpdateTime();
|
||||
if (lastUpdateSince > this.masterDelayTimeThreshold) {
|
||||
log.info("Change webservice master form uuid: {} to uuid: {}", masterRec.getUuid(), uuid);
|
||||
this.webserviceServerInfoRecordMapper.updateByPrimaryKeySelective(
|
||||
new WebserviceServerInfoRecord(masterRec.getId(), null, null, 0, 0L));
|
||||
setMasterTo(uuid);
|
||||
if (lastUpdateSince > this.masterDelayTimeThreshold || this.forceMaster) {
|
||||
forceMaster(uuid, masterRec.getUuid(), masterRec.getId());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if we have no master yet
|
||||
setMasterTo(uuid);
|
||||
return true;
|
||||
// We have no master yet so set this as master service
|
||||
return setMasterTo(uuid);
|
||||
}
|
||||
|
||||
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);
|
||||
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))
|
||||
.where(WebserviceServerInfoRecordDynamicSqlSupport.uuid, SqlBuilder.isEqualTo(uuid))
|
||||
.build()
|
||||
.execute();
|
||||
if (execute == null || execute.intValue() <= 0) {
|
||||
log.error("Failed to update webservice with uuid: {} to become master", uuid);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
|
|
@ -49,12 +49,6 @@ class ExamSessionControlTask implements DisposableBean {
|
|||
private final String examTaskCron;
|
||||
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(
|
||||
final ExamDAO examDAO,
|
||||
final SEBClientConnectionService sebClientConnectionService,
|
||||
|
|
|
@ -32,6 +32,7 @@ sebserver.webservice.api.admin.clientSecret=${sebserver.password}
|
|||
sebserver.webservice.internalSecret=${sebserver.password}
|
||||
|
||||
### webservice networking
|
||||
sebserver.webservice.forceMaster=false
|
||||
sebserver.webservice.distributed=false
|
||||
sebserver.webservice.http.external.scheme=https
|
||||
sebserver.webservice.http.external.servername=
|
||||
|
|
|
@ -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.SEBServerAuthorizationContext;
|
||||
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)
|
||||
@SpringBootTest(
|
||||
|
@ -59,13 +61,19 @@ public abstract class GuiIntegrationTest {
|
|||
protected JSONMapper jsonMapper;
|
||||
@Autowired
|
||||
protected FilterChainProxy springSecurityFilterChain;
|
||||
@Autowired
|
||||
private WebserviceInfoDAO webserviceInfoDAO;
|
||||
@Autowired
|
||||
private WebserviceInfo webserviceInfo;
|
||||
|
||||
protected MockMvc mockMvc;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
|
||||
.addFilter(this.springSecurityFilterChain).build();
|
||||
|
||||
}
|
||||
|
||||
protected OAuth2AuthorizationContextHolder getAuthorizationContextHolder() {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -51,6 +51,8 @@ import ch.ethz.seb.sebserver.SEBServer;
|
|||
import ch.ethz.seb.sebserver.WebSecurityConfig;
|
||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||
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.weblayer.oauth.AdminAPIClientDetails;
|
||||
import ch.ethz.seb.sebserver.webservice.weblayer.oauth.WebserviceResourceConfiguration;
|
||||
|
@ -74,6 +76,10 @@ public abstract class ExamAPIIntegrationTester {
|
|||
protected JSONMapper jsonMapper;
|
||||
@Autowired
|
||||
protected FilterChainProxy springSecurityFilterChain;
|
||||
@Autowired
|
||||
private WebserviceInfoDAO webserviceInfoDAO;
|
||||
@Autowired
|
||||
private WebserviceInfo webserviceInfo;
|
||||
|
||||
protected MockMvc mockMvc;
|
||||
|
||||
|
@ -82,6 +88,7 @@ public abstract class ExamAPIIntegrationTester {
|
|||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.webserviceInfoDAO.unregister(this.webserviceInfo.getWebserviceUUID());
|
||||
this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac)
|
||||
.addFilter(this.springSecurityFilterChain).build();
|
||||
// clear all caches before a test
|
||||
|
|
Loading…
Reference in a new issue