From d5f7752e9828d27f9a2532f9b7eaee82109ae3aa Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 30 Jan 2019 17:02:27 +0100 Subject: [PATCH] SEBSERV-18 SEBSERV-19 #ported from prototype --- pom.xml | 8 +- .../ethz/seb/sebserver/WebSecurityConfig.java | 3 + .../gbl/{model => api}/APIMessage.java | 30 +- .../sebserver/gbl/api/APIMessageError.java | 17 + .../sebserver/gbl/{ => api}/JSONMapper.java | 2 +- .../sebserver/gbl/{ => api}/POSTMapper.java | 2 +- .../api/SEBServerRestEndpoints.java} | 4 +- .../ethz/seb/sebserver/gbl/model/Entity.java | 4 +- ...{EntityKeyAndName.java => EntityName.java} | 8 +- .../gbl/model/EntityProcessingReport.java | 1 + .../seb/sebserver/gbl/model/exam/Exam.java | 2 +- .../gbl/model/institution/Institution.java | 2 +- .../gbl/model/institution/LmsSetup.java | 8 +- .../sebserver/gbl/model/user/UserInfo.java | 7 + .../seb/sebserver/gbl/model/user/UserMod.java | 2 +- .../ethz/seb/sebserver/gbl/util/Result.java | 91 ++-- .../ch/ethz/seb/sebserver/gbl/util/Utils.java | 2 +- .../sebserver/gui/GuiWebsecurityConfig.java | 35 -- .../gui/service/i18n/I18nSupport.java | 72 +++ .../gui/service/i18n/LocTextKey.java | 58 +++ .../gui/service/i18n/PolyglotPageService.java | 38 ++ .../service/i18n/impl/I18nSupportImpl.java | 131 +++++ .../i18n/impl/PolyglotPageServiceImpl.java | 58 +++ .../gui/service/page/ComposerService.java | 66 +++ .../gui/service/page/PageContext.java | 128 +++++ .../gui/service/page/PageDefinition.java | 16 + .../gui/service/page/PageEventListener.java | 25 + .../gui/service/page/PopupMenuComposer.java | 17 + .../gui/service/page/TemplateComposer.java | 19 + .../service/page/action/ActionDefinition.java | 77 +++ .../gui/service/page/action/ActionPane.java | 96 ++++ .../page/action/InstitutionActions.java | 41 ++ .../page/action/SafeActionExecution.java | 68 +++ .../service/page/activity/ActivitiesPane.java | 281 +++++++++++ .../page/activity/ActivityActionHandler.java | 26 + .../page/activity/ActivitySelection.java | 162 ++++++ .../gui/service/page/event/ActionEvent.java | 26 + .../page/event/ActionEventListener.java | 73 +++ .../page/event/ActionPublishEvent.java | 53 ++ .../event/ActionPublishEventListener.java | 20 + .../page/event/ActivitySelectionEvent.java | 21 + .../page/event/ActivitySelectionListener.java | 20 + .../gui/service/page/event/LogoutEvent.java | 21 + .../page/event/LogoutEventListener.java | 20 + .../gui/service/page/event/PageEvent.java | 13 + .../page/impl/ComposerServiceImpl.java | 186 +++++++ .../service/page/impl/DefaultLoginPage.java | 37 ++ .../service/page/impl/DefaultMainPage.java | 37 ++ .../service/page/impl/DefaultPageLayout.java | 275 ++++++++++ .../gui/service/page/impl/MainPageState.java | 49 ++ .../service/page/impl/PageContextImpl.java | 275 ++++++++++ .../gui/service/page/impl/SEBLogin.java | 147 ++++++ .../gui/service/page/impl/SEBMainPage.java | 166 +++++++ .../gui/service/page/impl/TODOTemplate.java | 33 ++ .../WebserviceConnectionConfig.java | 96 ++++ .../remote/webservice/api/RestCall.java | 15 +- .../remote/webservice/api/RestCallError.java | 6 +- .../remote/webservice/api/RestService.java | 42 +- .../api/institution/GetInstitutionNames.java | 39 ++ .../remote/webservice/auth/CurrentUser.java | 4 +- .../OAuth2AuthorizationContextHolder.java | 70 +-- .../auth/SEBServerAuthorizationContext.java | 3 +- .../auth/WebserviceURIBuilderSupplier.java | 40 -- .../webservice/auth/WebserviceURIService.java | 63 +++ .../sebserver/gui/service/widget/Message.java | 42 ++ .../gui/service/widget/SingleSelection.java | 56 +++ .../gui/service/widget/WidgetFactory.java | 470 ++++++++++++++++++ .../bulkaction/BulkActionSupportDAO.java | 6 +- .../servicelayer/dao/EntityDAO.java | 6 +- .../servicelayer/dao/FilterMap.java | 2 +- .../servicelayer/dao/impl/UserDaoImpl.java | 4 +- .../ClientSessionWebSecurityConfig.java | 2 + .../weblayer/WebServiceUserDetails.java | 4 +- .../weblayer/api/APIExceptionHandler.java | 8 +- .../weblayer/api/EntityController.java | 6 +- .../api/ExamAdministrationController.java | 5 +- .../weblayer/api/InstitutionController.java | 5 +- .../weblayer/api/LmsSetupController.java | 5 +- .../weblayer/api/QuizImportController.java | 3 +- .../weblayer/api/UserAccountController.java | 5 +- .../api/UserActivityLogController.java | 3 +- .../resources/config/application.properties | 4 +- src/main/resources/messages-de.properties | 0 src/main/resources/messages.properties | 31 ++ src/main/resources/static/css/sebserver.css | 2 +- .../gbl/model/user/UserActivityLogTest.java | 2 +- .../seb/sebserver/gbl/util/ResultTest.java | 8 +- .../AdministrationAPIIntegrationTester.java | 2 +- .../api/admin/InstitutionAPITest.java | 46 +- .../integration/api/admin/UserAPITest.java | 130 ++--- .../api/admin/UserActivityLogAPITest.java | 36 +- .../api/exam/ExamAPIIntegrationTester.java | 2 +- 92 files changed, 4016 insertions(+), 336 deletions(-) rename src/main/java/ch/ethz/seb/sebserver/gbl/{model => api}/APIMessage.java (79%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessageError.java rename src/main/java/ch/ethz/seb/sebserver/gbl/{ => api}/JSONMapper.java (93%) rename src/main/java/ch/ethz/seb/sebserver/gbl/{ => api}/POSTMapper.java (95%) rename src/main/java/ch/ethz/seb/sebserver/{webservice/weblayer/api/RestAPI.java => gbl/api/SEBServerRestEndpoints.java} (87%) rename src/main/java/ch/ethz/seb/sebserver/gbl/model/{EntityKeyAndName.java => EntityName.java} (89%) create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/LocTextKey.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/I18nSupportImpl.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/ComposerService.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageDefinition.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageEventListener.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/PopupMenuComposer.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/TemplateComposer.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionDefinition.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionPane.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/InstitutionActions.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/SafeActionExecution.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitiesPane.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivityActionHandler.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitySelection.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEvent.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEventListener.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEvent.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEventListener.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionEvent.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionListener.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEvent.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEventListener.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/PageEvent.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ComposerServiceImpl.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/MainPageState.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageContextImpl.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBLogin.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBMainPage.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/TODOTemplate.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/WebserviceConnectionConfig.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionNames.java delete mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/widget/Message.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/widget/SingleSelection.java create mode 100644 src/main/java/ch/ethz/seb/sebserver/gui/service/widget/WidgetFactory.java create mode 100644 src/main/resources/messages-de.properties create mode 100644 src/main/resources/messages.properties diff --git a/pom.xml b/pom.xml index 7e598fb3..47fbfcee 100644 --- a/pom.xml +++ b/pom.xml @@ -224,7 +224,13 @@ spring-security-jwt 1.0.9.RELEASE - + + + + org.apache.httpcomponents + httpclient + + org.eclipse.rap diff --git a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java index aa43138e..539e7128 100644 --- a/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/WebSecurityConfig.java @@ -19,6 +19,7 @@ import org.springframework.boot.web.servlet.error.ErrorController; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @@ -90,6 +91,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E final HttpServletResponse response, final AuthenticationException authException) throws IOException, ServletException { + response.setStatus(HttpStatus.UNAUTHORIZED.value()); response.sendRedirect(WebSecurityConfig.this.unauthorizedRedirect); } }) @@ -104,6 +106,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter implements E @RequestMapping("/error") public void handleError(final HttpServletResponse response) throws IOException { + response.setStatus(HttpStatus.NOT_FOUND.value()); response.sendRedirect(this.unauthorizedRedirect); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/APIMessage.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java similarity index 79% rename from src/main/java/ch/ethz/seb/sebserver/gbl/model/APIMessage.java rename to src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java index c38ff7ff..e18f2558 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/APIMessage.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessage.java @@ -6,9 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gbl.model; +package ch.ethz.seb.sebserver.gbl.api; import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.List; @@ -72,12 +74,14 @@ public class APIMessage implements Serializable { return new APIMessage(this.messageCode, this.systemMessage, error.getMessage()); } - public ResponseEntity createErrorResponse() { - return new ResponseEntity<>(of(), this.httpStatus); + public ResponseEntity> createErrorResponse() { + final APIMessage message = of(); + return new ResponseEntity<>(Arrays.asList(message), this.httpStatus); } public ResponseEntity createErrorResponse(final String details, final String... attributes) { - return new ResponseEntity<>(of(details, attributes), this.httpStatus); + final APIMessage message = of(details, attributes); + return new ResponseEntity<>(Arrays.asList(message), this.httpStatus); } } @@ -134,6 +138,24 @@ public class APIMessage implements Serializable { return ErrorMessage.FIELD_VALIDATION.of(error.toString(), args); } + public static String toHTML(final Collection messages) { + final StringBuilder builder = new StringBuilder(); + builder.append("Messages:

"); + messages.stream().forEach(message -> { + builder + .append("  code : ") + .append(message.messageCode) + .append("
") + .append("  system message : ") + .append(message.systemMessage) + .append("
") + .append("  details : ") + .append(StringUtils.abbreviate(message.details, 100)) + .append("

"); + }); + return builder.toString(); + } + public static class APIMessageException extends RuntimeException { private static final long serialVersionUID = 1453431210820677296L; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessageError.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessageError.java new file mode 100644 index 00000000..3aafc410 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/APIMessageError.java @@ -0,0 +1,17 @@ +/* + * 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.api; + +import java.util.List; + +public interface APIMessageError { + + List getErrorMessages(); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/JSONMapper.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/JSONMapper.java similarity index 93% rename from src/main/java/ch/ethz/seb/sebserver/gbl/JSONMapper.java rename to src/main/java/ch/ethz/seb/sebserver/gbl/api/JSONMapper.java index 959d9ffc..3bf31141 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/JSONMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/JSONMapper.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gbl; +package ch.ethz.seb.sebserver.gbl.api; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/POSTMapper.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java similarity index 95% rename from src/main/java/ch/ethz/seb/sebserver/gbl/POSTMapper.java rename to src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java index c9e4901a..76e1732e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/POSTMapper.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/POSTMapper.java @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.gbl; +package ch.ethz.seb.sebserver.gbl.api; import java.util.Collections; import java.util.List; diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RestAPI.java b/src/main/java/ch/ethz/seb/sebserver/gbl/api/SEBServerRestEndpoints.java similarity index 87% rename from src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RestAPI.java rename to src/main/java/ch/ethz/seb/sebserver/gbl/api/SEBServerRestEndpoints.java index 4cffc9d2..4fbe08d2 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/RestAPI.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/api/SEBServerRestEndpoints.java @@ -6,9 +6,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package ch.ethz.seb.sebserver.webservice.weblayer.api; +package ch.ethz.seb.sebserver.gbl.api; -public class RestAPI { +public class SEBServerRestEndpoints { public static final String ENDPOINT_INSTITUTION = "/institution"; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java index 5cbb2df3..d3bc93ff 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/Entity.java @@ -22,8 +22,8 @@ public interface Entity extends ModelIdAware { @JsonIgnore String getName(); - public static EntityKeyAndName toName(final Entity entity) { - return new EntityKeyAndName( + public static EntityName toName(final Entity entity) { + return new EntityName( entity.entityType(), entity.getModelId(), entity.getName()); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKeyAndName.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityName.java similarity index 89% rename from src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKeyAndName.java rename to src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityName.java index 7de9b769..c102ed8a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKeyAndName.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityName.java @@ -14,7 +14,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; @JsonIgnoreProperties(ignoreUnknown = true) -public class EntityKeyAndName implements ModelIdAware, ModelNameAware { +public class EntityName implements ModelIdAware, ModelNameAware { @JsonProperty(value = "entityType", required = true) public final EntityType entityType; @@ -24,7 +24,7 @@ public class EntityKeyAndName implements ModelIdAware, ModelNameAware { public final String name; @JsonCreator - public EntityKeyAndName( + public EntityName( @JsonProperty(value = "entityType", required = true) final EntityType entityType, @JsonProperty(value = Domain.ATTR_ID, required = true) final String id, @JsonProperty(value = "name", required = true) final String name) { @@ -34,7 +34,7 @@ public class EntityKeyAndName implements ModelIdAware, ModelNameAware { this.name = name; } - public EntityKeyAndName(final EntityKey entityKey, final String name) { + public EntityName(final EntityKey entityKey, final String name) { this.entityType = entityKey.entityType; this.modelId = entityKey.modelId; @@ -74,7 +74,7 @@ public class EntityKeyAndName implements ModelIdAware, ModelNameAware { return false; if (getClass() != obj.getClass()) return false; - final EntityKeyAndName other = (EntityKeyAndName) obj; + final EntityName other = (EntityName) obj; if (this.entityType != other.entityType) return false; if (this.modelId == null) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityProcessingReport.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityProcessingReport.java index b0bc079f..68a9c902 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityProcessingReport.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityProcessingReport.java @@ -14,6 +14,7 @@ import java.util.Set; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; import ch.ethz.seb.sebserver.gbl.util.Utils; public class EntityProcessingReport { diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java index 787e87a2..b991c735 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/exam/Exam.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import ch.ethz.seb.sebserver.gbl.POSTMapper; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.Domain.EXAM; import ch.ethz.seb.sebserver.gbl.model.EntityType; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/Institution.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/Institution.java index d20f1984..157330f5 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/Institution.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/Institution.java @@ -13,7 +13,7 @@ import javax.validation.constraints.Size; import com.fasterxml.jackson.annotation.JsonProperty; -import ch.ethz.seb.sebserver.gbl.POSTMapper; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain.INSTITUTION; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index 11a72fd7..3abd14d3 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -15,12 +15,12 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; -import ch.ethz.seb.sebserver.gbl.POSTMapper; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.Domain; import ch.ethz.seb.sebserver.gbl.model.Domain.INSTITUTION; import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; -import ch.ethz.seb.sebserver.gbl.model.EntityKeyAndName; +import ch.ethz.seb.sebserver.gbl.model.EntityName; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; @@ -198,8 +198,8 @@ public final class LmsSetup implements GrantEntity, Activatable { + this.sebAuthSecret + ", active=" + this.active + "]"; } - public static EntityKeyAndName toName(final LmsSetup lmsSetup) { - return new EntityKeyAndName( + public static EntityName toName(final LmsSetup lmsSetup) { + return new EntityName( EntityType.LMS_SETUP, String.valueOf(lmsSetup.id), lmsSetup.name); diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java index 1fa3fbb6..7573a84f 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserInfo.java @@ -156,6 +156,13 @@ public final class UserInfo implements GrantEntity, Activatable, Serializable { return this.roles; } + public boolean hasRole(final UserRole userRole) { + if (userRole == null) { + return false; + } + return this.roles.contains(userRole.name()); + } + @Override public int hashCode() { final int prime = 31; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java index 2932d287..1bcaae3e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/user/UserMod.java @@ -22,9 +22,9 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -import ch.ethz.seb.sebserver.gbl.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.Domain.USER; import ch.ethz.seb.sebserver.gbl.model.Domain.USER_ROLE; +import ch.ethz.seb.sebserver.gbl.api.POSTMapper; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java index 5ac9e2b5..501f0919 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java @@ -69,23 +69,58 @@ public final class Result { return this.value; } - /** @return the error if some was reporter or null if there was no error */ - public Throwable getError() { - return this.error; + /** Use this to get the resulting value. In an error case, a given error handling + * function is used that receives the error and returns a resulting value instead + * (or throw some error instead) + * + * @param errorHandler the error handling function + * @return */ + public T get(final Function errorHandler) { + return this.error != null ? errorHandler.apply(this.error) : this.value; } - public boolean hasError() { - return this.error != null; + public T get(final Consumer errorHandler, final Supplier supplier) { + if (this.error != null) { + errorHandler.accept(this.error); + return supplier.get(); + } else { + return this.value; + } } /** Use this to get the resulting value or (if null) to get a given other value * * @param other the other value to get if the computed value is null * @return return either the computed value if existing or a given other value */ - public T getOrElse(final T other) { + public T getOr(final T other) { return this.value != null ? this.value : other; } + /** Use this to get the resulting value if existing or throw an Runtime exception with + * given message otherwise. + * + * @param message the message for the RuntimeException in error case + * @return the resulting value */ + public T getOrThrowRuntime(final String message) { + if (this.error != null) { + throw new RuntimeException(message, this.error); + } + + return this.value; + } + + public T getOrThrow() { + if (this.error != null) { + if (this.error instanceof RuntimeException) { + throw (RuntimeException) this.error; + } else { + throw new RuntimeException("RuntimeExceptionWrapper cause: ", this.error); + } + } + + return this.value; + } + /** Use this to get the resulting value or (if null) to get a given other value * * @param supplier supplier to get the value from if the computed value is null @@ -94,6 +129,15 @@ public final class Result { return this.value != null ? this.value : supplier.get(); } + /** @return the error if some was reporter or null if there was no error */ + public Throwable getError() { + return this.error; + } + + public boolean hasError() { + return this.error != null; + } + /** If a value is present, performs the given action with the value, * otherwise performs the given empty-based action. * @@ -155,16 +199,6 @@ public final class Result { } } - /** Use this to get the resulting value. In an error case, a given error handling - * function is used that receives the error and returns a resulting value instead - * (or throw some error instead) - * - * @param errorHandler the error handling function - * @return */ - public T getOrHandleError(final Function errorHandler) { - return this.error != null ? errorHandler.apply(this.error) : this.value; - } - public Result onErrorDo(final Consumer block) { if (this.error != null) { block.accept(this.error); @@ -172,31 +206,6 @@ public final class Result { return this; } - /** Use this to get the resulting value if existing or throw an Runtime exception with - * given message otherwise. - * - * @param message the message for the RuntimeException in error case - * @return the resulting value */ - public T getOrThrowRuntime(final String message) { - if (this.error != null) { - throw new RuntimeException(message, this.error); - } - - return this.value; - } - - public T getOrThrow() { - if (this.error != null) { - if (this.error instanceof RuntimeException) { - throw (RuntimeException) this.error; - } else { - throw new RuntimeException("RuntimeExceptionWrapper cause: ", this.error); - } - } - - return this.value; - } - /** Use this to create a Result of a given resulting value. * * @param value resulting value diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java index 6cfc1aa5..1e0d97cc 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Utils.java @@ -103,7 +103,7 @@ public final class Utils { public static Long dateTimeStringToTimestamp(final String startTime, final Long defaultValue) { return dateTimeStringToTimestamp(startTime) - .getOrElse(defaultValue); + .getOr(defaultValue); } public static , K, V> M mapPut(final M map, final K key, final V value) { diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java index 3e60e612..2f85a652 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/GuiWebsecurityConfig.java @@ -11,7 +11,6 @@ package ch.ethz.seb.sebserver.gui; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -43,38 +42,4 @@ public class GuiWebsecurityConfig extends WebSecurityConfigurerAdapter { .requestMatchers(PUBLIC_URLS); } - @Override - protected void configure(final HttpSecurity http) throws Exception { - System.out.println("**************** GuiWebConfig: "); -// //@formatter:off -// http -// .sessionManagement() -// .sessionCreationPolicy(SessionCreationPolicy.STATELESS) -// .and() -// .antMatcher("/**"/*this.guiEndpointPath + "/**"*/) -// .authorizeRequests() -// .anyRequest() -// .authenticated() -// .and() -// .exceptionHandling() -// .authenticationEntryPoint( -// new AuthenticationEntryPoint() { -// -// @Override -// public void commence(final HttpServletRequest request, final HttpServletResponse response, -// final AuthenticationException authException) throws IOException, ServletException { -// response.sendRedirect("/gui"); -// } -// -// }) -// .and() -// .formLogin().disable() -// .httpBasic().disable() -// .logout().disable() -// .headers().frameOptions().disable() -// .and() -// .csrf().disable(); - //@formatter:on - } - } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java new file mode 100644 index 00000000..15995416 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/I18nSupport.java @@ -0,0 +1,72 @@ +/* + * 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.gui.service.i18n; + +import java.util.Collection; +import java.util.Locale; + +import org.joda.time.DateTime; + +public interface I18nSupport { + + Collection supportedLanguages(); + + /** Get the current Locale either form a user if this is called from a logged in user context or the + * applications default locale. + * + * @return the current Locale to use in context */ + Locale getCurrentLocale(); + + void setSessionLocale(Locale locale); + + /** Format a DateTime to a text format to display. + * + * @param date + * @return */ + String formatDisplayDate(DateTime date); + + /** Get localized text of specified key for currently set Locale. + * + * @param key the key name of localized text + * @param args additional arguments to parse the localized text + * @return the text in current language parsed from localized text */ + String getText(String key, Object... args); + + /** Get localized text of specified key for currently set Locale. + * + * @param key LocTextKey instance + * @return the text in current language parsed from localized text */ + String getText(LocTextKey key); + + /** Get localized text of specified key for currently set Locale. + * + * @param key the key name of localized text + * @param def default text that is returned if no localized test with specified key was found + * @param args additional arguments to parse the localized text + * @return the text in current language parsed from localized text */ + String getText(final String key, String def, Object... args); + + /** Get localized text of specified key and Locale. + * + * @param key the key name of localized text + * @param locale the Locale + * @param args additional arguments to parse the localized text + * @return the text in current language parsed from localized text */ + String getText(String key, Locale locale, Object... args); + + /** Get localized text of specified key and Locale. + * + * @param key the key name of localized text + * @param locale the Locale + * @param def default text that is returned if no localized test with specified key was found + * @param args additional arguments to parse the localized text + * @return the text in current language parsed from localized text */ + String getText(String key, Locale locale, String def, Object... args); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/LocTextKey.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/LocTextKey.java new file mode 100644 index 00000000..23275e56 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/LocTextKey.java @@ -0,0 +1,58 @@ +/* + * 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.gui.service.i18n; + +import java.util.Arrays; + +public class LocTextKey { + + public final String name; + public final Object[] args; + + public LocTextKey(final String name) { + this.name = name; + this.args = null; + } + + public LocTextKey(final String name, final Object... args) { + this.name = name; + this.args = args; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.name == null) ? 0 : this.name.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 LocTextKey other = (LocTextKey) obj; + if (this.name == null) { + if (other.name != null) + return false; + } else if (!this.name.equals(other.name)) + return false; + return true; + } + + @Override + public String toString() { + return "LocTextKey [name=" + this.name + ", args=" + Arrays.toString(this.args) + "]"; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java new file mode 100644 index 00000000..9474b76b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/PolyglotPageService.java @@ -0,0 +1,38 @@ +/* + * 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.gui.service.i18n; + +import java.util.Locale; + +import org.eclipse.swt.widgets.Composite; + +public interface PolyglotPageService { + + String POLYGLOT_WIDGET_FUNCTION_KEY = "POLYGLOT_WIDGET_FUNCTION"; + String POLYGLOT_TREE_ITEM_TEXT_DATA_KEY = "POLYGLOT_TREE_ITEM_TEXT_DATA"; + + /** Gets the underling I18nSupport + * + * @return the underling I18nSupport */ + I18nSupport getI18nSupport(); + + /** The default locale for the page. + * Uses I18nSupport.getCurrentLocale to do so. + * + * @param root the root Composite of the page to change the language */ + void setDefaultPageLocale(Composite root); + + /** Sets the given Locale and if needed, updates the page language according to the + * given Locale + * + * @param root root the root Composite of the page to change the language + * @param locale the Locale to set */ + void setPageLocale(Composite root, Locale locale); + +} \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/I18nSupportImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/I18nSupportImpl.java new file mode 100644 index 00000000..2c65d65a --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/I18nSupportImpl.java @@ -0,0 +1,131 @@ +/* + * 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.gui.service.i18n.impl; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Locale; + +import org.eclipse.rap.rwt.RWT; +import org.joda.time.DateTime; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.CurrentUser; + +@Lazy +@Component +public class I18nSupportImpl implements I18nSupport { + + private static final Logger log = LoggerFactory.getLogger(I18nSupportImpl.class); + + public static final String EMPTY_DISPLAY_VALUE = "--"; + private static final String ATTR_CURRENT_SESSION_LOCALE = "CURRENT_SESSION_LOCALE"; + + private final DateTimeFormatter displayDateFormatter; + private final CurrentUser currentUser; + private final MessageSource messageSource; + private final Locale defaultLocale = Locale.ENGLISH; + + public I18nSupportImpl( + final CurrentUser currentUser, + final MessageSource messageSource, + @Value("${sebserver.gui.date.displayformat}") final String displayDateFormat) { + + this.currentUser = currentUser; + this.messageSource = messageSource; + this.displayDateFormatter = DateTimeFormat + .forPattern(displayDateFormat) + .withZoneUTC(); + } + + private static final Collection SUPPORTED = Arrays.asList(Locale.ENGLISH, Locale.GERMANY); + + @Override + public Collection supportedLanguages() { + return SUPPORTED; + } + + @Override + public void setSessionLocale(final Locale locale) { + try { + RWT.getUISession() + .getHttpSession() + .setAttribute(ATTR_CURRENT_SESSION_LOCALE, locale); + RWT.setLocale(locale); + } catch (final IllegalStateException e) { + log.error("Set current locale for session failed: ", e); + } + } + + @Override + public Locale getCurrentLocale() { + // first session-locale if available + try { + final Locale sessionLocale = (Locale) RWT.getUISession() + .getHttpSession() + .getAttribute(ATTR_CURRENT_SESSION_LOCALE); + if (sessionLocale != null) { + return sessionLocale; + } + } catch (final IllegalStateException e) { + log.warn("Get current locale for session failed: {}", e.getMessage()); + } + + // second user-locale if available + if (this.currentUser.isAvailable()) { + return this.currentUser.get().locale; + } + + // last the default locale + return this.defaultLocale; + } + + @Override + public String formatDisplayDate(final DateTime date) { + if (date == null) { + return EMPTY_DISPLAY_VALUE; + } + return date.toString(this.displayDateFormatter); + } + + @Override + public String getText(final LocTextKey key) { + return getText(key.name, key.args); + } + + @Override + public String getText(final String key, final Object... args) { + return getText(key, key, args); + } + + @Override + public String getText(final String key, final String def, final Object... args) { + return this.messageSource.getMessage(key, args, def, this.getCurrentLocale()); + } + + @Override + public String getText(final String key, final Locale locale, final Object... args) { + return getText(key, locale, key, args); + } + + @Override + public String getText(final String key, final Locale locale, final String def, final Object... args) { + return this.messageSource.getMessage(key, args, def, locale); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java new file mode 100644 index 00000000..6d8c46eb --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/i18n/impl/PolyglotPageServiceImpl.java @@ -0,0 +1,58 @@ +/* + * 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.gui.service.i18n.impl; + +import java.util.Locale; +import java.util.function.Consumer; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; +import ch.ethz.seb.sebserver.gui.service.page.ComposerService; + +/** Service that supports page language change on the fly */ +@Lazy +@Service +@GuiProfile +public final class PolyglotPageServiceImpl implements PolyglotPageService { + + private final I18nSupport i18nSupport; + + public PolyglotPageServiceImpl(final I18nSupport i18nSupport) { + this.i18nSupport = i18nSupport; + } + + @Override + public I18nSupport getI18nSupport() { + return this.i18nSupport; + } + + @Override + public void setDefaultPageLocale(final Composite root) { + setPageLocale(root, this.i18nSupport.getCurrentLocale()); + } + + @Override + @SuppressWarnings("unchecked") + public void setPageLocale(final Composite root, final Locale locale) { + this.i18nSupport.setSessionLocale(locale); + ComposerService.traversePageTree( + root, + comp -> comp.getData(POLYGLOT_WIDGET_FUNCTION_KEY) != null, + comp -> ((Consumer) comp.getData(POLYGLOT_WIDGET_FUNCTION_KEY)).accept(comp)); + + root.layout(true, true); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/ComposerService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/ComposerService.java new file mode 100644 index 00000000..9e07ffb0 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/ComposerService.java @@ -0,0 +1,66 @@ +/* + * 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.gui.service.page; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; + +import ch.ethz.seb.sebserver.gui.RAPConfiguration.EntryPointService; + +public interface ComposerService extends EntryPointService { + + boolean validate(String composerName, PageContext pageContext); + + PageDefinition mainPage(); + + PageDefinition loginPage(); + + void compose( + String composerName, + PageContext pageContext); + + void compose( + Class composerType, + PageContext pageContext); + + void composePage( + Class pageType, + Composite root); + + void composePage( + PageDefinition pageDefinition, + Composite root); + + static void traversePageTree( + final Composite root, + final Predicate predicate, + final Consumer f) { + + if (predicate.test(root)) { + f.accept(root); + } + + final Control[] children = root.getChildren(); + if (children != null) { + for (final Control control : children) { + if (!(control instanceof Composite)) { + if (predicate.test(control)) { + f.accept(control); + } + } else { + traversePageTree((Composite) control, predicate, f); + } + } + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java new file mode 100644 index 00000000..ae24cf57 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageContext.java @@ -0,0 +1,128 @@ +/* + * 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.gui.service.page; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Shell; + +import ch.ethz.seb.sebserver.gui.service.page.event.PageEvent; + +public interface PageContext { + + public static final class PageAttr { + + public final String name; + public final String value; + + public PageAttr(final String name, final String value) { + this.name = name; + this.value = value; + } + } + + public interface AttributeKeys { + + public static final String ATTR_PAGE_TEMPLATE_COMPOSER_NAME = "ATTR_PAGE_TEMPLATE_COMPOSER_NAME"; + + public static final String INSTITUTION_ID = "INSTITUTION_ID"; + +// public static final String USER_NAME = "USER_NAME"; +// public static final String PASSWORD = "PASSWORD"; +// + +// +// public static final String CONFIG_ID = "CONFIG_ID"; +// public static final String CONFIG_VIEW_NAME = "CONFIG_VIEW_NAME"; +// public static final String CONFIG_ATTRIBUTE_SAVE_TYPE = "CONFIG_ATTRIBUTE_SAVE_TYPE"; +// public static final String CONFIG_ATTRIBUTE_VALUE = "CONFIG_ATTRIBUTE_VALUE"; +// +// public static final String EXAM_ID = "EXAM_ID"; +// public static final String STATE_NAME = "STATE_NAME"; +// +// public static final String AUTHORIZATION_CONTEXT = "AUTHORIZATION_CONTEXT"; +// public static final String AUTHORIZATION_HEADER = "AUTHORIZATION_HEADER"; + public static final String AUTHORIZATION_FAILURE = "AUTHORIZATION_FAILURE"; + public static final String LGOUT_SUCCESS = "LGOUT_SUCCESS"; + + } + + ComposerService composerService(); + + Shell getShell(); + + /** Get the page root Component. + * + * @return the page root Component. */ + Composite getRoot(); + + /** Get the Component that is currently set as parent during page tree compose + * + * @return the parent Component */ + Composite getParent(); + + /** Create a copy of this PageContext with a new parent Composite. + * + * @param parent the new parent Composite + * @return a copy of this PageContext with a new parent Composite. */ + PageContext copyOf(Composite parent); + + /** Create a copy of this PageContext with and additionally page context attributes. + * The additionally page context attributes will get merged with them already defined + * + * @param attributes additionally page context attributes. + * @return a copy of this PageContext with with and additionally page context attributes. */ + PageContext copyOfAttributes(PageContext otherContext); + + /** Adds the specified attribute to the existing page context attributes. + * + * @param key the key of the attribute + * @param value the value of the attribute + * @return this PageContext instance (builder pattern) */ + PageContext withAttr(String key, String value); + + String getAttribute(String name); + + String getAttribute(String name, String def); + + boolean hasAttribute(String name); + + /** Publishes a given PageEvent to the current page tree + * This goes through the page-tree and collects all listeners the are listen to + * the specified page event type. + * + * @param event the concrete PageEvent instance */ + void publishPageEvent(T event); + + /** Apply a confirm dialog with a specified confirm message and a callback code + * block that will be executed on users OK selection. + * + * @param confirmMessage + * @param onOK callback code block that will be executed on users OK selection */ + void applyConfirmDialog(String confirmMessage, Runnable onOK); + + void forwardToPage( + PageDefinition pageDefinition, + PageContext pageContext); + + void forwardToMainPage(PageContext pageContext); + + void forwardToLoginPage(PageContext pageContext); + + /** Notify an error dialog to the user with specified error message and + * optional exception instance + * + * @param errorMessage the error message to display + * @param error the error as Throwable */ + void notifyError(String errorMessage, Throwable error); + + void notifyError(Throwable error); + + T logoutOnError(Throwable error); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageDefinition.java new file mode 100644 index 00000000..a5bfa567 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageDefinition.java @@ -0,0 +1,16 @@ +/* + * 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.gui.service.page; + +public interface PageDefinition { + + Class composer(); + + PageContext applyPageContext(PageContext pageContext); +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageEventListener.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageEventListener.java new file mode 100644 index 00000000..ab98a22f --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PageEventListener.java @@ -0,0 +1,25 @@ +/* + * 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.gui.service.page; + +import ch.ethz.seb.sebserver.gui.service.page.event.PageEvent; + +public interface PageEventListener { + + String LISTENER_ATTRIBUTE_KEY = "PageEventListener"; + + boolean match(Class eventType); + + default int priority() { + return 1; + } + + void notify(T event); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PopupMenuComposer.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PopupMenuComposer.java new file mode 100644 index 00000000..b6ef8831 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/PopupMenuComposer.java @@ -0,0 +1,17 @@ +/* + * 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.gui.service.page; + +import org.eclipse.swt.widgets.Event; + +public interface PopupMenuComposer { + + void onMenuEvent(final Event event); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/TemplateComposer.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/TemplateComposer.java new file mode 100644 index 00000000..79a640d8 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/TemplateComposer.java @@ -0,0 +1,19 @@ +/* + * 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.gui.service.page; + +public interface TemplateComposer { + + default boolean validate(final PageContext context) { + return true; + } + + void compose(PageContext composerCtx); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionDefinition.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionDefinition.java new file mode 100644 index 00000000..8edf5310 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionDefinition.java @@ -0,0 +1,77 @@ +/* + * 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.gui.service.page.action; + +import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory.IconButtonType; + +public enum ActionDefinition { + + INSTITUTION_NEW( + "actions.new.institution", + IconButtonType.NEW_ACTION), + + INSTITUTION_MODIFY( + "actions.modify.institution", + IconButtonType.SAVE_ACTION), + + INSTITUTION_DELETE( + "actions.delete.institution", + IconButtonType.DELETE_ACTION), + + LMS_SETUP_NEW( + "New LMS Setup", + IconButtonType.NEW_ACTION), + + LMS_SETUP_MODIFY( + "Save LMS Setup", + IconButtonType.SAVE_ACTION), + + LMS_SETUP_DELETE( + "Delete LMS Setup", + IconButtonType.DELETE_ACTION), + + LMS_SETUP_TEST( + "Test LMS Setup", + IconButtonType.SAVE_ACTION), + + SEB_CONFIG_NEW( + "New Configuration", + IconButtonType.NEW_ACTION), + + SEB_CONFIG_MODIFY( + "Save Configuration", + IconButtonType.SAVE_ACTION), + + SEB_CONFIG_DELETE( + "Delete Configuration", + IconButtonType.DELETE_ACTION), + + EXAM_IMPORT( + "Import Exam", + IconButtonType.SAVE_ACTION), + + EXAM_EDIT( + "Edit Selected Exam", + IconButtonType.NEW_ACTION), + + EXAM_DELETE( + "Delete Selected Exam", + IconButtonType.DELETE_ACTION), + + ; + + public final String name; + public final IconButtonType icon; + + private ActionDefinition(final String name, final IconButtonType icon) { + this.name = name; + this.icon = icon; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionPane.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionPane.java new file mode 100644 index 00000000..e2268001 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/ActionPane.java @@ -0,0 +1,96 @@ +/* + * 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.gui.service.page.action; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.rap.rwt.template.ImageCell; +import org.eclipse.rap.rwt.template.Template; +import org.eclipse.rap.rwt.template.TextCell; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEvent; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEventListener; +import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; + +@Lazy +@Component +public class ActionPane implements TemplateComposer { + + private static final String ACTION_EVENT_CALL_KEY = "ACTION_EVENT_CALL"; + + private final WidgetFactory widgetFactory; + + public ActionPane(final WidgetFactory widgetFactory) { + super(); + this.widgetFactory = widgetFactory; + } + + @Override + public void compose(final PageContext composerCtx) { + + final Label label = this.widgetFactory.labelLocalized( + composerCtx.getParent(), + "h3", + new LocTextKey("sebserver.actionpane.title")); + final GridData titleLayout = new GridData(SWT.FILL, SWT.TOP, true, false); + titleLayout.verticalIndent = 10; + titleLayout.horizontalIndent = 10; + label.setLayoutData(titleLayout); + + final Tree actions = this.widgetFactory.treeLocalized(composerCtx.getParent(), SWT.SINGLE | SWT.FULL_SELECTION); + actions.setData(RWT.CUSTOM_VARIANT, "actions"); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + actions.setLayoutData(gridData); + final Template template = new Template(); + final ImageCell imageCell = new ImageCell(template); + imageCell.setLeft(0, 0).setWidth(40).setTop(0).setBottom(0, 0).setHorizontalAlignment(SWT.LEFT); + imageCell.setBindingIndex(0); + final TextCell textCell = new TextCell(template); + textCell.setLeft(0, 30).setWidth(150).setTop(7).setBottom(0, 0).setHorizontalAlignment(SWT.LEFT); + textCell.setBindingIndex(0); + actions.setData(RWT.ROW_TEMPLATE, template); + + actions.addListener(SWT.Selection, event -> { + final TreeItem treeItem = (TreeItem) event.item; + + final Runnable action = (Runnable) treeItem.getData(ACTION_EVENT_CALL_KEY); + action.run(); + + if (!treeItem.isDisposed()) { + treeItem.getParent().deselectAll(); + } + }); + + actions.setData( + PageEventListener.LISTENER_ATTRIBUTE_KEY, + new ActionPublishEventListener() { + @Override + public void notify(final ActionPublishEvent event) { + final TreeItem actionItem = ActionPane.this.widgetFactory.treeItemLocalized( + actions, + event.actionDefinition.name); + actionItem.setImage(event.actionDefinition.icon.getImage(composerCtx.getParent().getDisplay())); + actionItem.setData(ACTION_EVENT_CALL_KEY, + new SafeActionExecution(composerCtx, event, event.run)); + } + }); + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/InstitutionActions.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/InstitutionActions.java new file mode 100644 index 00000000..fe9b1f32 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/InstitutionActions.java @@ -0,0 +1,41 @@ +/* + * 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.gui.service.page.action; + +public interface InstitutionActions { + +// /** Use this higher-order function to create a new Institution action Runnable. +// * +// * @return */ +// static Runnable newInstitution(final PageContext composerCtx, final RestServices restServices) { +// return () -> { +// final IdAndName newInstitutionId = restServices +// .sebServerAPICall(NewInstitution.class) +// .doAPICall() +// .onErrorThrow("Unexpected Error"); +// composerCtx.notify(new ActionEvent(ActionDefinition.INSTITUTION_NEW, newInstitutionId)); +// }; +// } +// +// /** Use this higher-order function to create a delete Institution action Runnable. +// * +// * @return */ +// static Runnable deleteInstitution(final PageContext composerCtx, final RestServices restServices, +// final String instId) { +// return () -> { +// restServices +// .sebServerAPICall(DeleteInstitution.class) +// .attribute(AttributeKeys.INSTITUTION_ID, instId) +// .doAPICall() +// .onErrorThrow("Unexpected Error"); +// composerCtx.notify(new ActionEvent(ActionDefinition.INSTITUTION_DELETE, instId)); +// }; +// } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/SafeActionExecution.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/SafeActionExecution.java new file mode 100644 index 00000000..02ee9a22 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/action/SafeActionExecution.java @@ -0,0 +1,68 @@ +/* + * 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.gui.service.page.action; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionPublishEvent; + +public class SafeActionExecution implements Runnable { + + private static final Logger log = LoggerFactory.getLogger(SafeActionExecution.class); + + private final PageContext pageContext; + private final ActionPublishEvent actionEvent; + private final Runnable actionExecution; + + public SafeActionExecution( + final PageContext pageContext, + final ActionPublishEvent actionEvent, + final Runnable actionExecution) { + + this.pageContext = pageContext; + this.actionEvent = actionEvent; + this.actionExecution = actionExecution; + } + + @Override + public void run() { + try { + if (StringUtils.isNotBlank(this.actionEvent.confirmationMessage)) { + this.pageContext.applyConfirmDialog( + this.actionEvent.confirmationMessage, + createConfirmationCallback()); + } else { + this.actionExecution.run(); + } + } catch (final Throwable t) { + log.error("Failed to execute action: {}", this.actionEvent, t); + this.pageContext.notifyError("action.error.unexpected.message", t); + } + } + + private final Runnable createConfirmationCallback() { + return new Runnable() { + + @Override + public void run() { + try { + SafeActionExecution.this.actionExecution.run(); + } catch (final Throwable t) { + log.error("Failed to execute action: {}", SafeActionExecution.this.actionEvent, t); + SafeActionExecution.this.pageContext.notifyError("action.error.unexpected.message", t); + } + + } + }; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitiesPane.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitiesPane.java new file mode 100644 index 00000000..ecae2c79 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitiesPane.java @@ -0,0 +1,281 @@ +/* + * 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.gui.service.page.activity; + +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.model.EntityName; +import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; +import ch.ethz.seb.sebserver.gbl.model.user.UserRole; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.page.action.ActionDefinition; +import ch.ethz.seb.sebserver.gui.service.page.activity.ActivitySelection.Activity; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionEventListener; +import ch.ethz.seb.sebserver.gui.service.page.event.ActivitySelectionEvent; +import ch.ethz.seb.sebserver.gui.service.page.impl.MainPageState; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestService; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.institution.GetInstitutionNames; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; +import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; + +@Lazy +@Component +public class ActivitiesPane implements TemplateComposer { + + private final WidgetFactory widgetFactory; + private final RestService restService; + private final AuthorizationContextHolder authorizationContextHolder; + + private final Map activityActionHandler = + new EnumMap<>(ActionDefinition.class); + + public ActivitiesPane( + final WidgetFactory widgetFactory, + final RestService restService, + final AuthorizationContextHolder authorizationContextHolder, + final Collection activityActionHandler) { + + this.widgetFactory = widgetFactory; + this.restService = restService; + this.authorizationContextHolder = authorizationContextHolder; + + for (final ActivityActionHandler aah : activityActionHandler) { + this.activityActionHandler.put(aah.handlesAction(), aah); + } + } + + @Override + public void compose(final PageContext pageContext) { + final UserInfo userInfo = this.authorizationContextHolder + .getAuthorizationContext() + .getLoggedInUser() + .get(pageContext::logoutOnError); + + final Label activities = this.widgetFactory.labelLocalized( + pageContext.getParent(), + "h3", + new LocTextKey("sebserver.activitiespane.title")); + final GridData activitiesGridData = new GridData(SWT.FILL, SWT.TOP, true, false); + activitiesGridData.horizontalIndent = 20; + activities.setLayoutData(activitiesGridData); + + final Tree navigation = + this.widgetFactory.treeLocalized(pageContext.getParent(), SWT.SINGLE | SWT.FULL_SELECTION); + final GridData navigationGridData = new GridData(SWT.FILL, SWT.FILL, true, true); + navigationGridData.horizontalIndent = 10; + navigation.setLayoutData(navigationGridData); + + final List insitutionNames = this.restService + .getBuilder(GetInstitutionNames.class) + .call() + .get(pageContext::notifyError, () -> Collections.emptyList()); + + if (userInfo.hasRole(UserRole.SEB_SERVER_ADMIN)) { + // institutions (list) as root + final TreeItem institutions = this.widgetFactory.treeItemLocalized( + navigation, + Activity.INSTITUTION_ROOT.title); + ActivitySelection.inject(institutions, Activity.INSTITUTION_ROOT.createSelection()); + + for (final EntityName inst : insitutionNames) { + createInstitutionItem(institutions, inst); + } + } else { + final EntityName inst = insitutionNames.iterator().next(); + createInstitutionItem(navigation, inst); + } + +// final TreeItem user = this.widgetFactory.treeItemLocalized( +// navigation, +// "org.sebserver.activities.user"); +// ActivitySelection.set(user, Activity.USERS.createSelection()); +// +// final TreeItem configs = this.widgetFactory.treeItemLocalized( +// navigation, +// "org.sebserver.activities.sebconfigs"); +// ActivitySelection.set(configs, Activity.SEB_CONFIGS.createSelection()); +// +// final TreeItem config = this.widgetFactory.treeItemLocalized( +// configs, +// "org.sebserver.activities.sebconfig"); +// ActivitySelection.set(config, Activity.SEB_CONFIG.createSelection()); +// +// final TreeItem configTemplates = this.widgetFactory.treeItemLocalized( +// configs, +// "org.sebserver.activities.sebconfig.templates"); +// ActivitySelection.set(configTemplates, Activity.SEB_CONFIG_TEMPLATES.createSelection()); +// +// final TreeItem exams = this.widgetFactory.treeItemLocalized( +// navigation, +// "org.sebserver.activities.exam"); +// ActivitySelection.set(exams, Activity.EXAMS.createSelection()); +// +// final TreeItem monitoring = this.widgetFactory.treeItemLocalized( +// navigation, +// "org.sebserver.activities.monitoring"); +// ActivitySelection.set(monitoring, Activity.MONITORING.createSelection()); +// +// final TreeItem runningExams = this.widgetFactory.treeItemLocalized( +// monitoring, +// "org.sebserver.activities.runningExams"); +// ActivitySelection.set(runningExams, Activity.RUNNING_EXAMS.createSelection() +// .withExpandFunction(this::runningExamExpand)); +// runningExams.setItemCount(1); +// +// final TreeItem logs = this.widgetFactory.treeItemLocalized( +// monitoring, +// "org.sebserver.activities.logs"); +// ActivitySelection.set(logs, Activity.LOGS.createSelection()); + + navigation.addListener(SWT.Expand, this::handleExpand); + navigation.addListener(SWT.Selection, event -> handleSelection(pageContext, event)); + + navigation.setData( + PageEventListener.LISTENER_ATTRIBUTE_KEY, + new ActionEventListener() { + @Override + public void notify(final ActionEvent event) { + final ActivityActionHandler aah = + ActivitiesPane.this.activityActionHandler.get(event.actionDefinition); + if (aah != null) { + aah.notifyAction(event, navigation, pageContext); + } + } + }); + + // page-selection on (re)load + final MainPageState mainPageState = MainPageState.get(); + + if (mainPageState.activitySelection == null) { + mainPageState.activitySelection = ActivitySelection.get(navigation.getItem(0)); + } + pageContext.publishPageEvent( + new ActivitySelectionEvent(mainPageState.activitySelection)); + } + +// private void runningExamExpand(final TreeItem item) { +// item.removeAll(); +// final List runningExamNames = this.restService +// .sebServerCall(GetRunningExamNames.class) +// .onError(t -> { +// throw new RuntimeException(t); +// }); +// +// if (runningExamNames != null) { +// for (final EntityName runningExamName : runningExamNames) { +// final TreeItem runningExams = this.widgetFactory.treeItemLocalized( +// item, +// runningExamName.name); +// ActivitySelection.set(runningExams, Activity.RUNNING_EXAM.createSelection(runningExamName)); +// } +// } +// } + + private void handleExpand(final Event event) { + final TreeItem treeItem = (TreeItem) event.item; + + System.out.println("opened: " + treeItem); + + final ActivitySelection activity = ActivitySelection.get(treeItem); + if (activity != null) { + activity.processExpand(treeItem); + } + } + + private void handleSelection(final PageContext composerCtx, final Event event) { + final TreeItem treeItem = (TreeItem) event.item; + + System.out.println("selected: " + treeItem); + + final MainPageState mainPageState = MainPageState.get(); + final ActivitySelection activitySelection = ActivitySelection.get(treeItem); + if (mainPageState.activitySelection == null) { + mainPageState.activitySelection = Activity.NONE.createSelection(); + } + if (!mainPageState.activitySelection.equals(activitySelection)) { + mainPageState.activitySelection = activitySelection; + composerCtx.publishPageEvent( + new ActivitySelectionEvent(mainPageState.activitySelection)); + } + } + + static TreeItem createInstitutionItem(final Tree parent, final EntityName idAndName) { + final TreeItem institution = new TreeItem(parent, SWT.NONE); + createInstitutionItem(idAndName, institution); + return institution; + } + + static TreeItem createInstitutionItem(final TreeItem parent, final EntityName idAndName) { + final TreeItem institution = new TreeItem(parent, SWT.NONE); + createInstitutionItem(idAndName, institution); + return institution; + } + + static void createInstitutionItem(final EntityName idAndName, final TreeItem institution) { + institution.setText(idAndName.name); + ActivitySelection.inject(institution, Activity.INSTITUTION_NODE.createSelection(idAndName)); + } + + static final TreeItem findItemByActivity( + final TreeItem[] items, + final Activity activity, + final String objectId) { + + if (items == null) { + return null; + } + + for (final TreeItem item : items) { + final ActivitySelection activitySelection = ActivitySelection.get(item); + final String id = activitySelection.getObjectIdentifier(); + if (activitySelection != null && activitySelection.activity == activity && + (id == null || (objectId != null && objectId.equals(id)))) { + return item; + } + + final TreeItem _item = findItemByActivity(item.getItems(), activity, objectId); + if (_item != null) { + return _item; + } + } + + return null; + } + + static final TreeItem findItemByActivity(final TreeItem[] items, final Activity activity) { + return findItemByActivity(items, activity, null); + } + + static final void expand(final TreeItem item) { + if (item == null) { + return; + } + + item.setExpanded(true); + expand(item.getParentItem()); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivityActionHandler.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivityActionHandler.java new file mode 100644 index 00000000..8a125d5e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivityActionHandler.java @@ -0,0 +1,26 @@ +/* + * 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.gui.service.page.activity; + +import org.eclipse.swt.widgets.Tree; + +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.action.ActionDefinition; +import ch.ethz.seb.sebserver.gui.service.page.event.ActionEvent; + +public interface ActivityActionHandler { + + public ActionDefinition handlesAction(); + + void notifyAction( + final ActionEvent event, + final Tree navigation, + final PageContext composerCtx); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitySelection.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitySelection.java new file mode 100644 index 00000000..8dff9ad6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/activity/ActivitySelection.java @@ -0,0 +1,162 @@ +/* + * 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.gui.service.page.activity; + +import java.util.function.Consumer; + +import org.eclipse.swt.widgets.TreeItem; + +import ch.ethz.seb.sebserver.gbl.model.EntityName; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.page.action.ActionPane; +import ch.ethz.seb.sebserver.gui.service.page.impl.TODOTemplate; + +public class ActivitySelection { + + public static final Consumer EMPTY_FUNCTION = ti -> { + }; + public static final Consumer COLLAPSE_NONE_EMPTY = ti -> { + ti.removeAll(); + ti.setItemCount(1); + }; + + public enum Activity { + NONE(TODOTemplate.class, TODOTemplate.class, (String) null), + INSTITUTION_ROOT( + TODOTemplate.class, + ActionPane.class, + new LocTextKey("sebserver.activities.inst")), + INSTITUTION_NODE( + TODOTemplate.class, + ActionPane.class, + AttributeKeys.INSTITUTION_ID), +// +// USERS(UserAccountsForm.class, ActionPane.class), +// +// EXAMS(ExamsListPage.class, ActionPane.class), +// SEB_CONFIGS(SEBConfigurationForm.class, ActionPane.class), +// SEB_CONFIG(SEBConfigurationPage.class, ActionPane.class), +// SEB_CONFIG_TEMPLATES(TODOTemplate.class, ActionPane.class), +// MONITORING(MonitoringForm.class, ActionPane.class), +// RUNNING_EXAMS(RunningExamForm.class, ActionPane.class), +// RUNNING_EXAM(RunningExamPage.class, ActionPane.class, AttributeKeys.EXAM_ID), +// LOGS(TODOTemplate.class, ActionPane.class), + ; + + public final LocTextKey title; + public final Class contentPaneComposer; + public final Class actionPaneComposer; + public final String objectIdentifierAttribute; + + private Activity( + final Class objectPaneComposer, + final Class selectionPaneComposer, + final LocTextKey title) { + + this.title = title; + this.contentPaneComposer = objectPaneComposer; + this.actionPaneComposer = selectionPaneComposer; + this.objectIdentifierAttribute = null; + } + + private Activity( + final Class objectPaneComposer, + final Class selectionPaneComposer, + final String objectIdentifierAttribute) { + + this.title = null; + this.contentPaneComposer = objectPaneComposer; + this.actionPaneComposer = selectionPaneComposer; + this.objectIdentifierAttribute = objectIdentifierAttribute; + } + + public final ActivitySelection createSelection() { + return new ActivitySelection(this); + } + + public final ActivitySelection createSelection(final EntityName entityName) { + return new ActivitySelection(this, entityName); + } + } + + private static final String ATTR_ACTIVITY_SELECTION = "ACTIVITY_SELECTION"; + + public final Activity activity; + public final EntityName entityName; + Consumer expandFunction = EMPTY_FUNCTION; + + ActivitySelection(final Activity activity) { + this(activity, null); + } + + ActivitySelection(final Activity activity, final EntityName entityName) { + this.activity = activity; + this.entityName = entityName; + this.expandFunction = EMPTY_FUNCTION; + } + + public ActivitySelection withExpandFunction(final Consumer expandFunction) { + if (expandFunction == null) { + this.expandFunction = EMPTY_FUNCTION; + } + this.expandFunction = expandFunction; + return this; + } + + public String getObjectIdentifier() { + if (this.entityName == null) { + return null; + } + + return this.entityName.modelId; + } + + public void processExpand(final TreeItem item) { + this.expandFunction.accept(item); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.activity == null) ? 0 : this.activity.hashCode()); + result = prime * result + ((this.entityName == null) ? 0 : this.entityName.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 ActivitySelection other = (ActivitySelection) obj; + if (this.activity != other.activity) + return false; + if (this.entityName == null) { + if (other.entityName != null) + return false; + } else if (!this.entityName.equals(other.entityName)) + return false; + return true; + } + + public static ActivitySelection get(final TreeItem item) { + return (ActivitySelection) item.getData(ATTR_ACTIVITY_SELECTION); + } + + public static void inject(final TreeItem item, final ActivitySelection selection) { + item.setData(ATTR_ACTIVITY_SELECTION, selection); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEvent.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEvent.java new file mode 100644 index 00000000..fc4f344b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEvent.java @@ -0,0 +1,26 @@ +/* + * 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.gui.service.page.event; + +import ch.ethz.seb.sebserver.gui.service.page.action.ActionDefinition; + +public final class ActionEvent implements PageEvent { + + public final ActionDefinition actionDefinition; + public final Object source; + + public ActionEvent( + final ActionDefinition actionDefinition, + final Object source) { + + this.actionDefinition = actionDefinition; + this.source = source; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEventListener.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEventListener.java new file mode 100644 index 00000000..86e765a2 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionEventListener.java @@ -0,0 +1,73 @@ +/* + * 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.gui.service.page.event; + +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.eclipse.swt.widgets.Widget; + +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; +import ch.ethz.seb.sebserver.gui.service.page.action.ActionDefinition; + +public interface ActionEventListener extends PageEventListener { + + @Override + default boolean match(final Class type) { + return type == ActionEvent.class; + } + + static ActionEventListener of(final Consumer eventConsumer) { + return new ActionEventListener() { + @Override + public void notify(final ActionEvent event) { + eventConsumer.accept(event); + } + }; + } + + static ActionEventListener of( + final Predicate predicate, + final Consumer eventConsumer) { + + return new ActionEventListener() { + @Override + public void notify(final ActionEvent event) { + if (predicate.test(event)) { + eventConsumer.accept(event); + } + } + }; + } + + static ActionEventListener of( + final ActionDefinition actionDefinition, + final Consumer eventConsumer) { + + return new ActionEventListener() { + @Override + public void notify(final ActionEvent event) { + if (event.actionDefinition == actionDefinition) { + eventConsumer.accept(event); + } + } + }; + } + + static void injectListener( + final Widget widget, + final ActionDefinition actionDefinition, + final Consumer eventConsumer) { + + widget.setData( + PageEventListener.LISTENER_ATTRIBUTE_KEY, + of(actionDefinition, eventConsumer)); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEvent.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEvent.java new file mode 100644 index 00000000..08a8892b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEvent.java @@ -0,0 +1,53 @@ +/* + * 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.gui.service.page.event; + +import ch.ethz.seb.sebserver.gui.service.page.action.ActionDefinition; + +public class ActionPublishEvent implements PageEvent { + + public final ActionDefinition actionDefinition; + public final Runnable run; + public final String confirmationMessage; + public final String successMessage; + + public ActionPublishEvent( + final ActionDefinition actionDefinition, + final Runnable run) { + + this(actionDefinition, run, null, null); + } + + public ActionPublishEvent( + final ActionDefinition actionDefinition, + final Runnable run, + final String confirmationMessage) { + + this(actionDefinition, run, confirmationMessage, null); + } + + public ActionPublishEvent( + final ActionDefinition actionDefinition, + final Runnable run, + final String confirmationMessage, + final String successMessage) { + + this.actionDefinition = actionDefinition; + this.run = run; + this.confirmationMessage = confirmationMessage; + this.successMessage = successMessage; + } + + @Override + public String toString() { + return "ActionPublishEvent [actionDefinition=" + this.actionDefinition + ", confirmationMessage=" + + this.confirmationMessage + ", successMessage=" + this.successMessage + "]"; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEventListener.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEventListener.java new file mode 100644 index 00000000..0748fad6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActionPublishEventListener.java @@ -0,0 +1,20 @@ +/* + * 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.gui.service.page.event; + +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; + +public interface ActionPublishEventListener extends PageEventListener { + + @Override + default boolean match(final Class type) { + return type == ActionPublishEvent.class; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionEvent.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionEvent.java new file mode 100644 index 00000000..cdf4829e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionEvent.java @@ -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.gui.service.page.event; + +import ch.ethz.seb.sebserver.gui.service.page.activity.ActivitySelection; + +public class ActivitySelectionEvent implements PageEvent { + + public final ActivitySelection selection; + + public ActivitySelectionEvent(final ActivitySelection selection) { + this.selection = selection; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionListener.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionListener.java new file mode 100644 index 00000000..7a1942d9 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/ActivitySelectionListener.java @@ -0,0 +1,20 @@ +/* + * 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.gui.service.page.event; + +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; + +public interface ActivitySelectionListener extends PageEventListener { + + @Override + default boolean match(final Class eventType) { + return eventType == ActivitySelectionEvent.class; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEvent.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEvent.java new file mode 100644 index 00000000..4bc4a9e6 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEvent.java @@ -0,0 +1,21 @@ +/* + * 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.gui.service.page.event; + +import ch.ethz.seb.sebserver.gui.service.page.PageContext; + +public final class LogoutEvent implements PageEvent { + + public final PageContext pageContext; + + public LogoutEvent(final PageContext pageContext) { + this.pageContext = pageContext; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEventListener.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEventListener.java new file mode 100644 index 00000000..f6839c32 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/LogoutEventListener.java @@ -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.gui.service.page.event; + +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; + +public interface LogoutEventListener extends PageEventListener { + + @Override + default boolean match(final Class eventType) { + return eventType == LogoutEvent.class; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/PageEvent.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/PageEvent.java new file mode 100644 index 00000000..727888c7 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/event/PageEvent.java @@ -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.gui.service.page.event; + +public interface PageEvent { + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ComposerServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ComposerServiceImpl.java new file mode 100644 index 00000000..8c76d3ac --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/ComposerServiceImpl.java @@ -0,0 +1,186 @@ +/* + * 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.gui.service.page.impl; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +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.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.page.ComposerService; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageDefinition; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; + +@Lazy +@Service +@GuiProfile +public class ComposerServiceImpl implements ComposerService { + + private static final Logger log = LoggerFactory.getLogger(ComposerServiceImpl.class); + + // TODO configurable + private final Class loginPageType = DefaultLoginPage.class; + private final Class mainPageType = DefaultMainPage.class; + + final AuthorizationContextHolder authorizationContextHolder; + private final I18nSupport i18nSupport; + private final Map composer; + private final Map pages; + + public ComposerServiceImpl( + final AuthorizationContextHolder authorizationContextHolder, + final I18nSupport i18nSupport, + final Collection composer, + final Collection pageDefinitions) { + + this.authorizationContextHolder = authorizationContextHolder; + this.i18nSupport = i18nSupport; + this.composer = composer + .stream() + .collect(Collectors.toMap( + comp -> comp.getClass().getName(), + Function.identity())); + + this.pages = pageDefinitions + .stream() + .collect(Collectors.toMap( + page -> page.getClass().getName(), + Function.identity())); + } + + @Override + public PageDefinition mainPage() { + return this.pages.get(this.mainPageType.getName()); + } + + @Override + public PageDefinition loginPage() { + return this.pages.get(this.loginPageType.getName()); + } + + @Override + public boolean validate(final String composerName, final PageContext pageContext) { + if (!this.composer.containsKey(composerName)) { + return false; + } + + return this.composer + .get(composerName) + .validate(pageContext); + } + + @Override + public void compose( + final Class composerType, + final PageContext pageContext) { + + compose(composerType.getName(), pageContext); + } + + @Override + public void compose( + final String name, + final PageContext pageContext) { + + if (!this.composer.containsKey(name)) { + log.error("No TemplateComposer with name: " + name + " found. Check Spring confiuration and beans"); + return; + } + + final TemplateComposer composer = this.composer.get(name); + + if (composer.validate(pageContext)) { + + clear(pageContext.getParent()); + + try { + composer.compose(pageContext); + } catch (final Exception e) { + log.warn("Failed to compose: {}, pageContext: {}", name, pageContext, e); + } + + try { + pageContext.getParent().layout(); + } catch (final Exception e) { + log.warn("Failed to layout new composition: {}, pageContext: {}", name, pageContext, e); + } + + } else { + log.error( + "Invalid or missing mandatory attributes to handle compose request of ViewComposer: {} pageContext: {}", + name, + pageContext); + } + + } + + @Override + public void composePage( + final PageDefinition pageDefinition, + final Composite root) { + + compose( + pageDefinition.composer(), + pageDefinition.applyPageContext(createPageContext(root))); + } + + @Override + public void composePage( + final Class pageType, + final Composite root) { + + final String pageName = pageType.getName(); + if (!this.pages.containsKey(pageName)) { + log.error("Unknown page with name: {}", pageName); + return; + } + + final PageDefinition pageDefinition = this.pages.get(pageName); + compose( + pageDefinition.composer(), + pageDefinition.applyPageContext(createPageContext(root))); + } + + @Override + public void loadLoginPage(final Composite parent) { + composePage(this.loginPageType, parent); + } + + @Override + public void loadMainPage(final Composite parent) { + composePage(this.mainPageType, parent); + } + + private PageContext createPageContext(final Composite root) { + return new PageContextImpl( + this.i18nSupport, this, root, root, null); + } + + private void clear(final Composite parent) { + if (parent == null) { + return; + } + + for (final Control control : parent.getChildren()) { + control.dispose(); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java new file mode 100644 index 00000000..ff8d7576 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultLoginPage.java @@ -0,0 +1,37 @@ +/* + * 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.gui.service.page.impl; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; +import ch.ethz.seb.sebserver.gui.service.page.PageDefinition; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; + +/** Default login page works with the DefaultPageLayout and the + * SEBLogin template */ +@Lazy +@Component +public class DefaultLoginPage implements PageDefinition { + + @Override + public Class composer() { + return DefaultPageLayout.class; + } + + @Override + public PageContext applyPageContext(final PageContext pageContext) { + return pageContext.withAttr( + AttributeKeys.ATTR_PAGE_TEMPLATE_COMPOSER_NAME, + SEBLogin.class.getName()); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java new file mode 100644 index 00000000..0185b52b --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultMainPage.java @@ -0,0 +1,37 @@ +/* + * 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.gui.service.page.impl; + +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; +import ch.ethz.seb.sebserver.gui.service.page.PageDefinition; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; + +/** Default main page works with the DefaultPageLayout and the + * SEBMainPage template */ +@Lazy +@Component +public class DefaultMainPage implements PageDefinition { + + @Override + public Class composer() { + return DefaultPageLayout.class; + } + + @Override + public PageContext applyPageContext(final PageContext pageContext) { + return pageContext.withAttr( + AttributeKeys.ATTR_PAGE_TEMPLATE_COMPOSER_NAME, + SEBMainPage.class.getName()); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java new file mode 100644 index 00000000..4f684e4a --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/DefaultPageLayout.java @@ -0,0 +1,275 @@ +/* + * 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.gui.service.page.impl; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; +import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; + +@Lazy +@Component +public class DefaultPageLayout implements TemplateComposer { + + private final WidgetFactory widgetFactory; + private final PolyglotPageService polyglotPageService; + private final AuthorizationContextHolder authorizationContextHolder; + private final String sebServerVersion; + + public DefaultPageLayout( + final WidgetFactory widgetFactory, + final PolyglotPageService polyglotPageService, + final AuthorizationContextHolder authorizationContextHolder, + @Value("${sebserver.version}") final String sebServerVersion) { + + this.widgetFactory = widgetFactory; + this.polyglotPageService = polyglotPageService; + this.authorizationContextHolder = authorizationContextHolder; + this.sebServerVersion = sebServerVersion; + } + + @Override + public boolean validate(final PageContext pageContext) { + return pageContext.hasAttribute(AttributeKeys.ATTR_PAGE_TEMPLATE_COMPOSER_NAME); + } + + @Override + public void compose(final PageContext pageContext) { + + final GridLayout skeletonLayout = new GridLayout(); + skeletonLayout.marginBottom = 0; + skeletonLayout.marginLeft = 0; + skeletonLayout.marginRight = 0; + skeletonLayout.marginTop = 0; + skeletonLayout.marginHeight = 0; + skeletonLayout.marginWidth = 0; + skeletonLayout.verticalSpacing = 0; + skeletonLayout.horizontalSpacing = 0; + pageContext.getParent().setLayout(skeletonLayout); + + composeHeader(pageContext); + composeLogoBar(pageContext); + composeContent(pageContext); + composeFooter(pageContext); + + this.polyglotPageService.setDefaultPageLocale(pageContext.getRoot()); + } + + private void composeHeader(final PageContext pageContext) { + final Composite header = new Composite(pageContext.getParent(), SWT.NONE); + final GridLayout gridLayout = new GridLayout(); + gridLayout.marginRight = 50; + gridLayout.marginLeft = 50; + header.setLayout(gridLayout); + final GridData headerCell = new GridData(SWT.FILL, SWT.TOP, true, false); + headerCell.minimumHeight = 40; + headerCell.heightHint = 40; + header.setLayoutData(headerCell); + header.setData(RWT.CUSTOM_VARIANT, "header"); + + final Composite headerRight = new Composite(header, SWT.NONE); + headerRight.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, true)); + final GridLayout headerRightGrid = new GridLayout(2, false); + headerRightGrid.marginHeight = 0; + headerRightGrid.marginWidth = 0; + headerRightGrid.horizontalSpacing = 20; + headerRight.setLayout(headerRightGrid); + headerRight.setData(RWT.CUSTOM_VARIANT, "header"); + + if (this.authorizationContextHolder.getAuthorizationContext().isLoggedIn()) { + final Label username = new Label(headerRight, SWT.NONE); + username.setData(RWT.CUSTOM_VARIANT, "header"); + username.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, true)); + username.setText(this.authorizationContextHolder + .getAuthorizationContext() + .getLoggedInUser() + .get(pageContext::logoutOnError).username); + + final Button logout = this.widgetFactory.buttonLocalized(headerRight, "sebserver.logout"); + logout.setLayoutData(new GridData(SWT.RIGHT, SWT.FILL, true, true)); + logout.setData(RWT.CUSTOM_VARIANT, "header"); + logout.addListener(SWT.Selection, event -> { + final boolean logoutSuccessful = this.authorizationContextHolder + .getAuthorizationContext() + .logout(); + + if (!logoutSuccessful) { + // TODO error handling + } + + MainPageState.clear(); + + // forward to login page with success message + pageContext.forwardToLoginPage( + pageContext.withAttr(AttributeKeys.LGOUT_SUCCESS, "true")); + }); + } + } + + private void composeLogoBar(final PageContext pageContext) { + final Composite logoBar = new Composite(pageContext.getParent(), SWT.NONE); + final GridData logoBarCell = new GridData(SWT.FILL, SWT.TOP, false, false); + logoBarCell.minimumHeight = 80; + logoBarCell.heightHint = 80; + logoBar.setLayoutData(logoBarCell); + logoBar.setData(RWT.CUSTOM_VARIANT, "logo"); + final GridLayout logoBarLayout = new GridLayout(2, false); + logoBarLayout.horizontalSpacing = 0; + logoBarLayout.marginHeight = 0; + logoBar.setLayout(logoBarLayout); + + final Composite logo = new Composite(logoBar, SWT.NONE); + final GridData logoCell = new GridData(SWT.LEFT, SWT.CENTER, true, true); + logoCell.minimumHeight = 80; + logoCell.heightHint = 80; + logoCell.minimumWidth = 400; + logoCell.horizontalIndent = 50; + logo.setLayoutData(logoCell); + logo.setData(RWT.CUSTOM_VARIANT, "bgLogo"); + + final Composite langSupport = new Composite(logoBar, SWT.NONE); + final GridData langSupportCell = new GridData(SWT.RIGHT, SWT.CENTER, false, false); + langSupportCell.heightHint = 20; + logoCell.horizontalIndent = 50; + langSupport.setLayoutData(langSupportCell); + langSupport.setData(RWT.CUSTOM_VARIANT, "logo"); + final RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); + rowLayout.spacing = 7; + rowLayout.marginRight = 70; + langSupport.setLayout(rowLayout); + + this.widgetFactory.createLanguageSelector(pageContext.copyOf(langSupport)); +// for (final Locale locale : this.i18nSupport.supportedLanguages()) { +// final LanguageSelection languageSelection = new LanguageSelection(langSupport, locale); +// languageSelection.updateLocale(this.i18nSupport); +// languageSelection.addListener(SWT.MouseDown, event -> { +// this.polyglotPageService.setPageLocale(pageContext.root, languageSelection.locale); +// +// }); +// } + } + + private void composeContent(final PageContext pageContext) { + final Composite contentBackground = new Composite(pageContext.getParent(), SWT.NONE); + contentBackground.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + contentBackground.setData(RWT.CUSTOM_VARIANT, "bgContent"); + final GridLayout innerGrid = new GridLayout(); + innerGrid.marginLeft = 50; + innerGrid.marginRight = 50; + innerGrid.marginHeight = 0; + innerGrid.marginWidth = 0; + + contentBackground.setLayout(innerGrid); + + final Composite content = new Composite(contentBackground, SWT.NONE); + content.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + content.setData(RWT.CUSTOM_VARIANT, "content"); + final GridLayout contentGrid = new GridLayout(); + contentGrid.marginHeight = 0; + contentGrid.marginWidth = 0; + content.setLayout(contentGrid); + + final Composite contentInner = new Composite(content, SWT.NONE); + contentInner.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true)); + final GridLayout gridLayout = new GridLayout(); + gridLayout.marginHeight = 0; + gridLayout.marginWidth = 0; + contentInner.setLayout(gridLayout); + + final String contentComposerName = pageContext.getAttribute( + AttributeKeys.ATTR_PAGE_TEMPLATE_COMPOSER_NAME); + pageContext.composerService().compose( + contentComposerName, + pageContext.copyOf(contentInner)); + } + + private void composeFooter(final PageContext pageContext) { + final Composite footerBar = new Composite(pageContext.getParent(), SWT.NONE); + final GridData footerCell = new GridData(SWT.FILL, SWT.BOTTOM, false, false); + footerCell.minimumHeight = 30; + footerCell.heightHint = 30; + footerBar.setLayoutData(footerCell); + footerBar.setData(RWT.CUSTOM_VARIANT, "bgFooter"); + final GridLayout innerBarGrid = new GridLayout(); + innerBarGrid.marginHeight = 0; + innerBarGrid.marginWidth = 0; + innerBarGrid.marginLeft = 50; + innerBarGrid.marginRight = 50; + footerBar.setLayout(innerBarGrid); + + final Composite footer = new Composite(footerBar, SWT.NONE); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true); + footer.setLayoutData(gridData); + final GridLayout footerGrid = new GridLayout(2, false); + footerGrid.marginHeight = 0; + footerGrid.marginWidth = 0; + footerGrid.horizontalSpacing = 0; + footer.setLayout(footerGrid); + footer.setData(RWT.CUSTOM_VARIANT, "footer"); + + final Composite footerLeft = new Composite(footer, SWT.NONE); + footerLeft.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, true, true)); + footerLeft.setData(RWT.CUSTOM_VARIANT, "footer"); + RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL); + rowLayout.marginLeft = 20; + rowLayout.spacing = 20; + footerLeft.setLayout(rowLayout); + + final Composite footerRight = new Composite(footer, SWT.NONE); + footerRight.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, true, true)); + footerRight.setData(RWT.CUSTOM_VARIANT, "footer"); + rowLayout = new RowLayout(SWT.HORIZONTAL); + rowLayout.marginRight = 20; + footerRight.setLayout(rowLayout); + + this.widgetFactory.labelLocalized(footerLeft, "footer", new LocTextKey("sebserver.overall.imprint")); + this.widgetFactory.labelLocalized(footerLeft, "footer", new LocTextKey("sebserver.overall.about")); + this.widgetFactory.labelLocalized( + footerRight, + "footer", + new LocTextKey("sebserver.overall.version", this.sebServerVersion)); + } + +// private final class LanguageSelection extends Label implements Polyglot { +// +// private static final long serialVersionUID = 8110167162843383940L; +// private final Locale locale; +// +// public LanguageSelection(final Composite parent, final Locale locale) { +// super(parent, SWT.NONE); +// this.locale = locale; +// super.setData(RWT.CUSTOM_VARIANT, "header"); +// super.setText("| " + locale.getLanguage().toUpperCase()); +// } +// +// @Override +// public void updateLocale(final I18nSupport i18nSupport) { +// super.setVisible( +// !i18nSupport.getCurrentLocale() +// .getLanguage() +// .equals(this.locale.getLanguage())); +// } +// } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/MainPageState.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/MainPageState.java new file mode 100644 index 00000000..ba00cb21 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/MainPageState.java @@ -0,0 +1,49 @@ +/* + * 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.gui.service.page.impl; + +import javax.servlet.http.HttpSession; + +import org.eclipse.rap.rwt.RWT; + +import ch.ethz.seb.sebserver.gui.service.page.activity.ActivitySelection; +import ch.ethz.seb.sebserver.gui.service.page.activity.ActivitySelection.Activity; + +public final class MainPageState { + + public ActivitySelection activitySelection = Activity.NONE.createSelection(); + + private MainPageState() { + } + + public static MainPageState get() { + try { + final HttpSession httpSession = RWT + .getUISession() + .getHttpSession(); + + MainPageState mainPageState = (MainPageState) httpSession.getAttribute(SEBMainPage.ATTR_MAIN_PAGE_STATE); + if (mainPageState == null) { + mainPageState = new MainPageState(); + httpSession.setAttribute(SEBMainPage.ATTR_MAIN_PAGE_STATE, mainPageState); + } + + return mainPageState; + } catch (final Exception e) { + SEBMainPage.log.error("Unexpected error while trying to get MainPageState from user-session"); + } + + return null; + } + + public static void clear() { + final MainPageState mainPageState = get(); + mainPageState.activitySelection = Activity.NONE.createSelection(); + } +} \ No newline at end of file diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageContextImpl.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageContextImpl.java new file mode 100644 index 00000000..e7ce105d --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/PageContextImpl.java @@ -0,0 +1,275 @@ +/* + * 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.gui.service.page.impl; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.rap.rwt.widgets.DialogCallback; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessageError; +import ch.ethz.seb.sebserver.gbl.util.Utils; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.page.ComposerService; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageDefinition; +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; +import ch.ethz.seb.sebserver.gui.service.page.event.PageEvent; +import ch.ethz.seb.sebserver.gui.service.widget.Message; + +public class PageContextImpl implements PageContext { + + private static final Logger log = LoggerFactory.getLogger(PageContextImpl.class); + + private final I18nSupport i18nSupport; + private final ComposerService composerService; + private final Composite root; + private final Composite parent; + private final Map attributes; + + PageContextImpl( + final I18nSupport i18nSupport, + final ComposerService composerService, + final Composite root, + final Composite parent, + final Map attributes) { + + this.i18nSupport = i18nSupport; + this.composerService = composerService; + this.root = root; + this.parent = parent; + this.attributes = Utils.immutableMapOf(attributes); + } + + @Override + public Shell getShell() { + if (this.root == null) { + return null; + } + + return this.root.getShell(); + } + + @Override + public ComposerService composerService() { + return this.composerService; + } + + @Override + public Composite getRoot() { + return this.root; + } + + @Override + public Composite getParent() { + return this.parent; + } + + @Override + public PageContext copyOf(final Composite parent) { + return new PageContextImpl( + this.i18nSupport, + this.composerService, + this.root, + parent, + this.attributes); + } + + @Override + public PageContext copyOfAttributes(final PageContext otherContext) { + final Map attrs = new HashMap<>(); + attrs.putAll(this.attributes); + attrs.putAll(((PageContextImpl) otherContext).attributes); + return new PageContextImpl( + this.i18nSupport, + this.composerService, + this.root, + this.parent, + attrs); + } + + @Override + public PageContext withAttr(final String key, final String value) { + final Map attrs = new HashMap<>(); + attrs.putAll(this.attributes); + attrs.put(key, value); + return new PageContextImpl( + this.i18nSupport, + this.composerService, + this.root, + this.parent, attrs); + } + + @Override + public String getAttribute(final String name) { + return this.attributes.get(name); + } + + @Override + public String getAttribute(final String name, final String def) { + if (this.attributes.containsKey(name)) { + return this.attributes.get(name); + } else { + return def; + } + } + + @Override + public boolean hasAttribute(final String name) { + return this.attributes.containsKey(name); + } + + @Override + @SuppressWarnings("unchecked") + public void publishPageEvent(final T event) { + final Class typeClass = event.getClass(); + final List> listeners = new ArrayList<>(); + ComposerService.traversePageTree( + this.root, + c -> { + final PageEventListener listener = + (PageEventListener) c.getData(PageEventListener.LISTENER_ATTRIBUTE_KEY); + return listener != null && listener.match(typeClass); + }, + c -> listeners.add(((PageEventListener) c.getData(PageEventListener.LISTENER_ATTRIBUTE_KEY)))); + + if (listeners.isEmpty()) { + return; + } + + listeners.stream() + .sorted(LISTENER_COMPARATOR) + .forEach(listener -> listener.notify(event)); + } + + @Override + @SuppressWarnings("serial") + public void applyConfirmDialog(final String confirmMessage, final Runnable onOK) { + final Message messageBox = new Message( + this.root.getShell(), + this.i18nSupport.getText("org.sebserver.dialog.confirm.title"), + this.i18nSupport.getText(confirmMessage), + SWT.OK | SWT.CANCEL); + messageBox.open(new DialogCallback() { + @Override + public void dialogClosed(final int returnCode) { + if (returnCode == SWT.OK) { + try { + onOK.run(); + } catch (final Throwable t) { + log.error( + "Unexpected on confirm callback execution. This should not happen, plase secure the given onOK Runnable", + t); + } + } + } + }); + } + +// public void applyValidationErrorDialog(final Collection validationErrors) { +// final Message messageBox = new Message( +// this.root.getShell(), +// this.i18nSupport.getText("org.sebserver.dialog.validationErrors.title"), +// this.i18nSupport.getText(confirmMessage), +// SWT.OK); +// } + + @Override + public void forwardToPage( + final PageDefinition pageDefinition, + final PageContext pageContext) { + + this.composerService.compose( + pageDefinition.composer(), + pageDefinition.applyPageContext(pageContext.copyOf(pageContext.getRoot()))); + } + + @Override + public void forwardToMainPage(final PageContext pageContext) { + forwardToPage(this.composerService.mainPage(), pageContext); + } + + @Override + public void forwardToLoginPage(final PageContext pageContext) { + forwardToPage(this.composerService.loginPage(), pageContext); + } + + @Override + public void notifyError(final String errorMessage, final Throwable error) { + if (error instanceof APIMessageError) { + final List errorMessages = ((APIMessageError) error).getErrorMessages(); + final MessageBox messageBox = new Message( + getShell(), + this.i18nSupport.getText("sebserver.error.unexpected"), + APIMessage.toHTML(errorMessages), + SWT.ERROR); + messageBox.setMarkupEnabled(true); + messageBox.open(null); + return; + } + + final MessageBox messageBox = new Message( + getShell(), + this.i18nSupport.getText("sebserver.error.unexpected"), + error.toString(), + SWT.ERROR); + messageBox.open(null); + } + + @Override + public void notifyError(final Throwable error) { + notifyError(error.getMessage(), error); + } + + @Override + public T logoutOnError(final Throwable error) { + // just to be sure we leave a clean and proper authorizationContext + try { + ((ComposerServiceImpl) this.composerService).authorizationContextHolder + .getAuthorizationContext() + .logout(); + } catch (final Exception e) { + log.info("Cleanup logout failed: {}", e.getMessage()); + } + + MainPageState.clear(); + forwardToLoginPage(this.withAttr( + AttributeKeys.AUTHORIZATION_FAILURE, + error.getMessage())); + + return null; + } + + @Override + public String toString() { + return "PageContextImpl [root=" + this.root + ", parent=" + this.parent + ", attributes=" + this.attributes + + "]"; + } + + private static final Comparator> LISTENER_COMPARATOR = + new Comparator<>() { + @Override + public int compare(final PageEventListener o1, final PageEventListener o2) { + final int x = o1.priority(); + final int y = o2.priority(); + return (x < y) ? -1 : ((x == y) ? 0 : 1); + } + }; + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBLogin.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBLogin.java new file mode 100644 index 00000000..9c5c05c8 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBLogin.java @@ -0,0 +1,147 @@ +/* + * 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.gui.service.page.impl; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Text; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageContext.AttributeKeys; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext; +import ch.ethz.seb.sebserver.gui.service.widget.Message; +import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; + +@Lazy +@Component +@GuiProfile +public class SEBLogin implements TemplateComposer { + + private static final Logger log = LoggerFactory.getLogger(SEBLogin.class); + + private final AuthorizationContextHolder authorizationContextHolder; + private final WidgetFactory widgetFactory; + private final I18nSupport i18nSupport; + + public SEBLogin( + final AuthorizationContextHolder authorizationContextHolder, + final WidgetFactory widgetFactory, + final I18nSupport i18nSupport) { + + this.authorizationContextHolder = authorizationContextHolder; + this.widgetFactory = widgetFactory; + this.i18nSupport = i18nSupport; + } + + @Override + public void compose(final PageContext pageContext) { + final Composite parent = pageContext.getParent(); + + if (pageContext.hasAttribute((AttributeKeys.LGOUT_SUCCESS))) { + final MessageBox logoutSuccess = new Message( + pageContext.getShell(), + this.i18nSupport.getText("org.sebserver.logout"), + this.i18nSupport.getText("org.sebserver.logout.success.message"), + SWT.ICON_INFORMATION); + logoutSuccess.open(null); + } + + final Composite loginGroup = new Composite(parent, SWT.NONE); + final GridLayout rowLayout = new GridLayout(); + rowLayout.marginWidth = 20; + rowLayout.marginRight = 100; + loginGroup.setLayout(rowLayout); + loginGroup.setData(RWT.CUSTOM_VARIANT, "login"); + + final Label name = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.username"); + name.setLayoutData(new GridData(300, -1)); + name.setAlignment(SWT.BOTTOM); + final Text loginName = new Text(loginGroup, SWT.LEFT | SWT.BORDER); + loginName.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false)); + GridData gridData = new GridData(SWT.FILL, SWT.TOP, false, false); + gridData.verticalIndent = 10; + final Label pwd = this.widgetFactory.labelLocalized(loginGroup, "sebserver.login.pwd"); + pwd.setLayoutData(gridData); + final Text loginPassword = new Text(loginGroup, SWT.LEFT | SWT.PASSWORD | SWT.BORDER); + loginPassword.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false)); + + final Button button = this.widgetFactory.buttonLocalized(loginGroup, "sebserver.login.login"); + gridData = new GridData(SWT.LEFT, SWT.TOP, false, false); + gridData.verticalIndent = 10; + button.setLayoutData(gridData); + + final SEBServerAuthorizationContext authorizationContext = this.authorizationContextHolder + .getAuthorizationContext(RWT.getUISession().getHttpSession()); + + button.addListener(SWT.Selection, event -> { + final String username = loginName.getText(); + try { + + final boolean loggedIn = authorizationContext.login( + username, + loginPassword.getText()); + + if (loggedIn) { + // Set users locale on page after successful login + this.i18nSupport.setSessionLocale( + authorizationContext + .getLoggedInUser() + .get(pageContext::logoutOnError).locale); + + pageContext.forwardToMainPage(pageContext); + + } else { + loginError(pageContext, "sebserver.login.failed.message"); + } + } catch (final Exception e) { + log.error("Unexpected error while trying to login with user: {}", username, e); + loginError(pageContext, "Unexpected Error. Please call an Administrator"); + } + }); + loginName.addListener(SWT.KeyDown, event -> { + if (event.character == '\n' || event.character == '\r') { + loginPassword.setFocus(); + } + }); + loginPassword.addListener(SWT.KeyDown, event -> { + if (event.character == '\n' || event.character == '\r') { + button.setFocus(); + } + }); + + } + + private void loginError( + final PageContext pageContext, + final String message) { + + final MessageBox error = new Message( + pageContext.getShell(), + this.i18nSupport.getText("sebserver.login.failed.title"), + this.i18nSupport.getText(message, message), + SWT.ERROR); + error.open(null); + pageContext.logoutOnError(new RuntimeException(message)); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBMainPage.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBMainPage.java new file mode 100644 index 00000000..a11dfe51 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/SEBMainPage.java @@ -0,0 +1,166 @@ +/* + * 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.gui.service.page.impl; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.SashForm; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.PageEventListener; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; +import ch.ethz.seb.sebserver.gui.service.page.activity.ActivitiesPane; +import ch.ethz.seb.sebserver.gui.service.page.event.ActivitySelectionEvent; +import ch.ethz.seb.sebserver.gui.service.page.event.ActivitySelectionListener; +import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory; +import ch.ethz.seb.sebserver.gui.service.widget.WidgetFactory.IconButtonType; + +@Lazy +@Component +@GuiProfile +public class SEBMainPage implements TemplateComposer { + + static final Logger log = LoggerFactory.getLogger(SEBMainPage.class); + + public static final String ATTR_MAIN_PAGE_STATE = "MAIN_PAGE_STATE"; + + private static final int ACTIVITY_PANE_WEIGHT = 20; + private static final int CONTENT_PANE_WEIGHT = 65; + private static final int ACTION_PANE_WEIGHT = 15; + private static final int[] DEFAULT_SASH_WEIGHTS = new int[] { + ACTIVITY_PANE_WEIGHT, + CONTENT_PANE_WEIGHT, + ACTION_PANE_WEIGHT + }; + private static final int[] OPENED_SASH_WEIGHTS = new int[] { 0, 100, 0 }; + + private final WidgetFactory widgetFactory; + + public SEBMainPage(final WidgetFactory widgetFactory) { + this.widgetFactory = widgetFactory; + } + + @Override + public void compose(final PageContext pageContext) { + MainPageState.clear(); + + final Composite parent = pageContext.getParent(); + parent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + final SashForm mainSash = new SashForm(parent, SWT.HORIZONTAL); + final GridLayout gridLayout = new GridLayout(); + + mainSash.setLayout(gridLayout); + mainSash.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + final Composite nav = new Composite(mainSash, SWT.NONE); + nav.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + final GridLayout navLayout = new GridLayout(); + navLayout.marginHeight = 20; + navLayout.marginWidth = 0; + nav.setLayout(navLayout); + + final Composite content = new Composite(mainSash, SWT.NONE); + content.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + final GridLayout contentOuterlayout = new GridLayout(); + contentOuterlayout.marginHeight = 0; + contentOuterlayout.marginWidth = 0; + content.setLayout(contentOuterlayout); + + final Label toggleView = this.widgetFactory.imageButton( + IconButtonType.MAXIMIZE, + content, + new LocTextKey("sebserver.mainpage.maximize.tooltip"), + event -> { + final Label ib = (Label) event.widget; + if ((Boolean) ib.getData("fullScreen")) { + mainSash.setWeights(DEFAULT_SASH_WEIGHTS); + ib.setData("fullScreen", false); + ib.setImage(WidgetFactory.IconButtonType.MAXIMIZE.getImage(ib.getDisplay())); + this.widgetFactory.injectI18n( + ib, + null, + new LocTextKey("sebserver.mainpage.maximize.tooltip")); + } else { + mainSash.setWeights(OPENED_SASH_WEIGHTS); + ib.setData("fullScreen", true); + ib.setImage(WidgetFactory.IconButtonType.MINIMIZE.getImage(ib.getDisplay())); + this.widgetFactory.injectI18n( + ib, + null, + new LocTextKey("sebserver.mainpage.minimize.tooltip")); + } + }); + final GridData gridData = new GridData(SWT.RIGHT, SWT.TOP, true, false); + toggleView.setLayoutData(gridData); + toggleView.setData("fullScreen", false); + + final Composite contentObjects = new Composite(content, SWT.NONE); + contentObjects.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + final GridLayout contentObjectslayout = new GridLayout(); + contentObjectslayout.marginHeight = 0; + contentObjectslayout.marginWidth = 0; + contentObjects.setLayout(contentObjectslayout); + contentObjects.setData(PageEventListener.LISTENER_ATTRIBUTE_KEY, + new ActivitySelectionListener() { + @Override + public int priority() { + return 2; + } + + @Override + public void notify(final ActivitySelectionEvent event) { + pageContext.composerService().compose( + event.selection.activity.contentPaneComposer, + pageContext.copyOf(contentObjects).withAttr( + event.selection.activity.objectIdentifierAttribute, + event.selection.getObjectIdentifier())); + } + }); + + final Composite actionPane = new Composite(mainSash, SWT.NONE); + actionPane.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + final GridLayout actionPaneGrid = new GridLayout(); + actionPane.setLayout(actionPaneGrid); + actionPane.setData(RWT.CUSTOM_VARIANT, "actionPane"); + actionPane.setData(PageEventListener.LISTENER_ATTRIBUTE_KEY, + new ActivitySelectionListener() { + @Override + public int priority() { + return 1; + } + + @Override + public void notify(final ActivitySelectionEvent event) { + pageContext.composerService().compose( + event.selection.activity.actionPaneComposer, + pageContext.copyOf(actionPane).withAttr( + event.selection.activity.objectIdentifierAttribute, + event.selection.getObjectIdentifier())); + } + }); + + pageContext.composerService().compose( + ActivitiesPane.class, + pageContext.copyOf(nav)); + + mainSash.setWeights(DEFAULT_SASH_WEIGHTS); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/TODOTemplate.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/TODOTemplate.java new file mode 100644 index 00000000..6605a392 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/page/impl/TODOTemplate.java @@ -0,0 +1,33 @@ +/* + * 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.gui.service.page.impl; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Label; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; + +import ch.ethz.seb.sebserver.gui.service.page.PageContext; +import ch.ethz.seb.sebserver.gui.service.page.TemplateComposer; + +@Lazy +@Component +public class TODOTemplate implements TemplateComposer { + + @Override + public void compose(final PageContext composerCtx) { + + final Label tree = new Label(composerCtx.getParent(), SWT.NONE); + tree.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + tree.setText("[TODO]"); + + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/WebserviceConnectionConfig.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/WebserviceConnectionConfig.java new file mode 100644 index 00000000..cb364aee --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/WebserviceConnectionConfig.java @@ -0,0 +1,96 @@ +/* + * 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.gui.service.remote.webservice; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; + +import javax.net.ssl.SSLContext; + +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.env.Environment; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.util.ResourceUtils; + +import ch.ethz.seb.sebserver.gbl.profile.DevGuiProfile; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.profile.ProdGuiProfile; + +@Configuration +@GuiProfile +public class WebserviceConnectionConfig { + + /** A ClientHttpRequestFactory for development profile with no TSL SSL protocol and + * not following redirects on redirect responses. + * + * @return ClientHttpRequestFactory bean for development profiles */ + @Bean + @DevGuiProfile + public ClientHttpRequestFactory clientHttpRequestFactory() { + return new SimpleClientHttpRequestFactory() { + + @Override + protected void prepareConnection(final HttpURLConnection connection, final String httpMethod) + throws IOException { + super.prepareConnection(connection, httpMethod); + connection.setInstanceFollowRedirects(false); + } + }; + } + + /** A ClientHttpRequestFactory used in production with TSL SSL configuration. + * + * NOTE: + * environment property: sebserver.gui.truststore.pwd is expected to have the correct truststore password set + * environment property: sebserver.gui.truststore.type is expected to set to the correct type of truststore + * truststore.jks is expected to be on the classpath containing all trusted certificates for request + * to SSL secured SEB Server webservice + * + * @return ClientHttpRequestFactory with TLS / SSL configuration + * @throws IOException + * @throws FileNotFoundException + * @throws CertificateException + * @throws KeyStoreException + * @throws NoSuchAlgorithmException + * @throws KeyManagementException */ + @Bean + @ProdGuiProfile + public ClientHttpRequestFactory clientHttpRequestFactoryTLS(final Environment env) throws KeyManagementException, + NoSuchAlgorithmException, KeyStoreException, CertificateException, FileNotFoundException, IOException { + + final char[] password = env + .getProperty("sebserver.gui.truststore.pwd") + .toCharArray(); + + final SSLContext sslContext = SSLContextBuilder + .create() + .loadTrustMaterial(ResourceUtils.getFile( + "classpath:truststore.jks"), + password) + .build(); + + final HttpClient client = HttpClients.custom() + .setSSLContext(sslContext) + .build(); + + return new HttpComponentsClientHttpRequestFactory(client); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java index 93f43b82..2a8b1dd4 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCall.java @@ -28,8 +28,8 @@ import org.springframework.web.client.RestClientResponseException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; -import ch.ethz.seb.sebserver.gbl.JSONMapper; -import ch.ethz.seb.sebserver.gbl.model.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.model.Page; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService.SortOrder; @@ -58,9 +58,10 @@ public abstract class RestCall { } - void init(final RestService restService, final JSONMapper jsonMapper) { + RestCall init(final RestService restService, final JSONMapper jsonMapper) { this.restService = restService; this.jsonMapper = jsonMapper; + return this; } protected Result exchange(final RestCallBuilder builder) { @@ -107,6 +108,10 @@ public abstract class RestCall { } } + public RestCallBuilder newBuilder() { + return new RestCallBuilder(); + } + public final class RestCallBuilder { private final HttpHeaders httpHeaders = new HttpHeaders(); @@ -117,7 +122,7 @@ public abstract class RestCall { RestCallBuilder() { this.httpHeaders.set( HttpHeaders.CONTENT_TYPE, - RestCall.this.contentType.getType()); + RestCall.this.contentType.toString()); } public RestCallBuilder withHeaders(final HttpHeaders headers) { @@ -165,7 +170,7 @@ public abstract class RestCall { return this; } - public final Result exchange() { + public final Result call() { return RestCall.this.exchange(this); } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java index 59a532fa..ec64e933 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestCallError.java @@ -11,9 +11,10 @@ package ch.ethz.seb.sebserver.gui.service.remote.webservice.api; import java.util.ArrayList; import java.util.List; -import ch.ethz.seb.sebserver.gbl.model.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessageError; -public class RestCallError extends RuntimeException { +public class RestCallError extends RuntimeException implements APIMessageError { private static final long serialVersionUID = -5201349295667957490L; @@ -27,6 +28,7 @@ public class RestCallError extends RuntimeException { super(message); } + @Override public List getErrorMessages() { return this.errors; } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java index bab4862a..ea37aa89 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/RestService.java @@ -8,6 +8,10 @@ package ch.ethz.seb.sebserver.gui.service.remote.webservice.api; +import java.util.Collection; +import java.util.Map; +import java.util.stream.Collectors; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; @@ -15,10 +19,10 @@ import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; -import ch.ethz.seb.sebserver.gbl.JSONMapper; +import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder; -import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIBuilderSupplier; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.WebserviceURIService; @Lazy @Service @@ -28,17 +32,23 @@ public class RestService { private static final Logger log = LoggerFactory.getLogger(RestService.class); private final AuthorizationContextHolder authorizationContextHolder; - private final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier; - private final JSONMapper jsonMapper; + private final WebserviceURIService webserviceURIBuilderSupplier; + private final Map> calls; public RestService( final AuthorizationContextHolder authorizationContextHolder, - final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier, - final JSONMapper jsonMapper) { + final WebserviceURIService webserviceURIBuilderSupplier, + final JSONMapper jsonMapper, + final Collection> calls) { this.authorizationContextHolder = authorizationContextHolder; this.webserviceURIBuilderSupplier = webserviceURIBuilderSupplier; - this.jsonMapper = jsonMapper; + + this.calls = calls + .stream() + .collect(Collectors.toMap( + call -> call.getClass().getName(), + call -> call.init(this, jsonMapper))); } public RestTemplate getWebserviceAPIRestTemplate() { @@ -51,15 +61,19 @@ public class RestService { return this.webserviceURIBuilderSupplier.getBuilder(); } + @SuppressWarnings("unchecked") public RestCall getRestCall(final Class> type) { - try { - final RestCall restCall = type.getDeclaredConstructor().newInstance(); - restCall.init(this, this.jsonMapper); - return restCall; - } catch (final Exception e) { - log.error("Error while trying to create RestCall of type: {}", type, e); - return new BuildErrorCall<>(e); + return (RestCall) this.calls.get(type.getName()); + } + + public RestCall.RestCallBuilder getBuilder(final Class> type) { + @SuppressWarnings("unchecked") + final RestCall restCall = (RestCall) this.calls.get(type.getName()); + if (restCall == null) { + return null; } + + return restCall.newBuilder(); } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionNames.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionNames.java new file mode 100644 index 00000000..9f0f9969 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/api/institution/GetInstitutionNames.java @@ -0,0 +1,39 @@ +/* + * 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.gui.service.remote.webservice.api.institution; + +import java.util.List; + +import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; + +import com.fasterxml.jackson.core.type.TypeReference; + +import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints; +import ch.ethz.seb.sebserver.gbl.model.EntityName; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.RestCall; + +@Lazy +@Component +@GuiProfile +public class GetInstitutionNames extends RestCall> { + + protected GetInstitutionNames() { + super( + new TypeReference>() { + }, + HttpMethod.GET, + MediaType.APPLICATION_FORM_URLENCODED, + SEBServerRestEndpoints.ENDPOINT_INSTITUTION + "/names"); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java index db856eaf..0725da60 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/CurrentUser.java @@ -34,7 +34,9 @@ public class CurrentUser { public UserInfo get() { if (isAvailable()) { - return this.authContext.getLoggedInUser(); + return this.authContext + .getLoggedInUser() + .getOrThrow(); } log.warn("Current user requested but no user is currently logged in"); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java index 6a28585e..174bf3aa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/OAuth2AuthorizationContextHolder.java @@ -22,7 +22,9 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; @@ -41,6 +43,7 @@ import org.springframework.web.client.RestTemplate; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Result; @Lazy @Component @@ -50,23 +53,23 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol private static final Logger log = LoggerFactory.getLogger(OAuth2AuthorizationContextHolder.class); private static final String CONTEXT_HOLDER_ATTRIBUTE = "CONTEXT_HOLDER_ATTRIBUTE"; - private static final String OAUTH_TOKEN_URI_PATH = "oauth/token"; // TODO to config properties? - private static final String OAUTH_REVOKE_TOKEN_URI_PATH = "/oauth/revoke-token"; // TODO to config properties? - private static final String CURRENT_USER_URI_PATH = "/user/me"; // TODO to config properties? private final String guiClientId; private final String guiClientSecret; - private final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier; + private final WebserviceURIService webserviceURIService; + private final ClientHttpRequestFactory clientHttpRequestFactory; @Autowired public OAuth2AuthorizationContextHolder( @Value("${sebserver.gui.webservice.clientId}") final String guiClientId, @Value("${sebserver.gui.webservice.clientSecret}") final String guiClientSecret, - final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier) { + final WebserviceURIService webserviceURIService, + final ClientHttpRequestFactory clientHttpRequestFactory) { this.guiClientId = guiClientId; this.guiClientSecret = guiClientSecret; - this.webserviceURIBuilderSupplier = webserviceURIBuilderSupplier; + this.webserviceURIService = webserviceURIService; + this.clientHttpRequestFactory = clientHttpRequestFactory; } @Override @@ -85,7 +88,8 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol context = new OAuth2AuthorizationContext( this.guiClientId, this.guiClientSecret, - this.webserviceURIBuilderSupplier); + this.webserviceURIService, + this.clientHttpRequestFactory); session.setAttribute(CONTEXT_HOLDER_ATTRIBUTE, context); } @@ -132,7 +136,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol private static final String GRANT_TYPE = "password"; private static final List SCOPES = Collections.unmodifiableList( - Arrays.asList("web-service-api-read", "web-service-api-write")); + Arrays.asList("read", "write")); private boolean valid = true; @@ -141,34 +145,26 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol private final String revokeTokenURI; private final String currentUserURI; - private UserInfo loggedInUser = null; + private Result loggedInUser = null; OAuth2AuthorizationContext( final String guiClientId, final String guiClientSecret, - final WebserviceURIBuilderSupplier webserviceURIBuilderSupplier) { + final WebserviceURIService webserviceURIService, + final ClientHttpRequestFactory clientHttpRequestFactory) { this.resource = new ResourceOwnerPasswordResourceDetails(); - this.resource.setAccessTokenUri( - webserviceURIBuilderSupplier - .getBuilder() - .path(OAUTH_TOKEN_URI_PATH) - .toUriString() /* restCallBuilder.withPath(OAUTH_TOKEN_URI_PATH) */); + this.resource.setAccessTokenUri(webserviceURIService.getOAuthTokenURI()); this.resource.setClientId(guiClientId); this.resource.setClientSecret(guiClientSecret); this.resource.setGrantType(GRANT_TYPE); this.resource.setScope(SCOPES); this.restTemplate = new DisposableOAuth2RestTemplate(this.resource); + this.restTemplate.setRequestFactory(clientHttpRequestFactory); - this.revokeTokenURI = webserviceURIBuilderSupplier - .getBuilder() - .path(OAUTH_REVOKE_TOKEN_URI_PATH) - .toUriString(); //restCallBuilder.withPath(OAUTH_REVOKE_TOKEN_URI_PATH); - this.currentUserURI = webserviceURIBuilderSupplier - .getBuilder() - .path(CURRENT_USER_URI_PATH) - .toUriString(); //restCallBuilder.withPath(CURRENT_USER_URI_PATH); + this.revokeTokenURI = webserviceURIService.getOAuthRevokeTokenURI(); + this.currentUserURI = webserviceURIService.getCurrentUserRequestURI(); } @Override @@ -227,7 +223,7 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol } @Override - public UserInfo getLoggedInUser() { + public Result getLoggedInUser() { if (this.loggedInUser != null) { return this.loggedInUser; } @@ -237,18 +233,27 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol try { if (isValid() && isLoggedIn()) { final ResponseEntity response = - this.restTemplate.getForEntity(this.currentUserURI, UserInfo.class); - this.loggedInUser = response.getBody(); - return this.loggedInUser; + this.restTemplate + .getForEntity(this.currentUserURI, UserInfo.class); + if (response.getStatusCode() == HttpStatus.OK) { + this.loggedInUser = Result.of(response.getBody()); + return this.loggedInUser; + } else { + log.error("Unexpected error response: {}", response); + return Result.ofError(new IllegalStateException( + "Http Request responded with status: " + response.getStatusCode())); + } } else { - throw new IllegalStateException("Logged in User requested on invalid or not logged in "); + return Result.ofError( + new IllegalStateException("Logged in User requested on invalid or not logged in ")); } } catch (final AccessDeniedException | OAuth2AccessDeniedException ade) { log.error("Acccess denied while trying to request logged in User from API", ade); - throw ade; + return Result.ofError(ade); } catch (final Exception e) { log.error("Unexpected error while trying to request logged in User from API", e); - throw new RuntimeException("Unexpected error while trying to request logged in User from API", e); + return Result.ofError( + new RuntimeException("Unexpected error while trying to request logged in User from API", e)); } } @@ -258,8 +263,9 @@ public class OAuth2AuthorizationContextHolder implements AuthorizationContextHol return false; } - return getLoggedInUser().roles - .contains(role.name()); + return getLoggedInUser() + .getOrThrow().roles + .contains(role.name()); } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java index ede1a752..cdc26284 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/SEBServerAuthorizationContext.java @@ -12,6 +12,7 @@ import org.springframework.web.client.RestTemplate; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserRole; +import ch.ethz.seb.sebserver.gbl.util.Result; public interface SEBServerAuthorizationContext { @@ -23,7 +24,7 @@ public interface SEBServerAuthorizationContext { boolean logout(); - UserInfo getLoggedInUser(); + Result getLoggedInUser(); public boolean hasRole(UserRole role); diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java deleted file mode 100644 index a0ff9ae1..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIBuilderSupplier.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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.gui.service.remote.webservice.auth; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.web.util.UriComponentsBuilder; - -import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; - -@Lazy -@Component -@GuiProfile -public class WebserviceURIBuilderSupplier { - - private final UriComponentsBuilder webserviceURIBuilder; - - public WebserviceURIBuilderSupplier( - @Value("${sebserver.gui.webservice.protocol}") final String webserviceProtocol, - @Value("${sebserver.gui.webservice.address}") final String webserviceServerAdress, - @Value("${sebserver.gui.webservice.portol}") final String webserviceServerPort, - @Value("${sebserver.gui.webservice.apipath}") final String webserviceAPIPath) { - - this.webserviceURIBuilder = UriComponentsBuilder - .fromHttpUrl(webserviceProtocol + "://" + webserviceServerAdress) - .port(webserviceServerPort) - .path(webserviceAPIPath); - } - - public UriComponentsBuilder getBuilder() { - return this.webserviceURIBuilder.cloneBuilder(); - } -} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java new file mode 100644 index 00000000..f6eb75b1 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/remote/webservice/auth/WebserviceURIService.java @@ -0,0 +1,63 @@ +/* + * 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.gui.service.remote.webservice.auth; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.util.UriComponentsBuilder; + +import ch.ethz.seb.sebserver.gbl.api.SEBServerRestEndpoints; +import ch.ethz.seb.sebserver.gbl.profile.GuiProfile; + +@Component +@GuiProfile +public class WebserviceURIService { + + private static final String OAUTH_TOKEN_URI_PATH = "oauth/token"; // TODO to config properties? + private static final String OAUTH_REVOKE_TOKEN_URI_PATH = "/oauth/revoke-token"; // TODO to config properties? + private static final String CURRENT_USER_URI_PATH = SEBServerRestEndpoints.ENDPOINT_USER_ACCOUNT + "/me"; + + private final String webserviceServerAddress; + private final UriComponentsBuilder webserviceURIBuilder; + + public WebserviceURIService( + @Value("${sebserver.gui.webservice.protocol}") final String webserviceProtocol, + @Value("${sebserver.gui.webservice.address}") final String webserviceServerAdress, + @Value("${sebserver.gui.webservice.port}") final String webserviceServerPort, + @Value("${sebserver.gui.webservice.apipath}") final String webserviceAPIPath) { + + this.webserviceServerAddress = webserviceProtocol + "://" + webserviceServerAdress + ":" + webserviceServerPort; + this.webserviceURIBuilder = UriComponentsBuilder + .fromHttpUrl(webserviceProtocol + "://" + webserviceServerAdress) + .port(webserviceServerPort) + .path(webserviceAPIPath); + } + + public UriComponentsBuilder getBuilder() { + return this.webserviceURIBuilder.cloneBuilder(); + } + + public String getOAuthTokenURI() { + return UriComponentsBuilder.fromHttpUrl(this.webserviceServerAddress) + .path(OAUTH_TOKEN_URI_PATH) + .toUriString(); + } + + public String getOAuthRevokeTokenURI() { + return UriComponentsBuilder.fromHttpUrl(this.webserviceServerAddress) + .path(OAUTH_REVOKE_TOKEN_URI_PATH) + .toUriString(); + } + + public String getCurrentUserRequestURI() { + return getBuilder() + .path(CURRENT_USER_URI_PATH) + .toUriString(); + } +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/Message.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/Message.java new file mode 100644 index 00000000..2a2c4c3e --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/Message.java @@ -0,0 +1,42 @@ +/* + * 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.gui.service.widget; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.graphics.Rectangle; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.MessageBox; +import org.eclipse.swt.widgets.Shell; + +public class Message extends MessageBox { + + private static final long serialVersionUID = 6973272221493264432L; + + public Message(final Shell parent, final String title, final String message, final int type) { + super(parent, type); + super.setText(title); + super.setMessage(message); + } + + @Override + protected void prepareOpen() { + super.prepareOpen(); + final GridLayout layout = (GridLayout) super.shell.getLayout(); + layout.marginTop = 10; + layout.marginBottom = 10; + super.shell.setData(RWT.CUSTOM_VARIANT, "message"); + final Rectangle bounds = super.shell.getBounds(); + if (bounds.width < 400) { + bounds.x = bounds.x - (400 - bounds.width) / 2; + bounds.width = 400; + super.shell.setBounds(bounds); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/SingleSelection.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/SingleSelection.java new file mode 100644 index 00000000..e9dc70bf --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/SingleSelection.java @@ -0,0 +1,56 @@ +/* + * 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.gui.service.widget; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; + +import ch.ethz.seb.sebserver.gbl.util.Tuple; + +public class SingleSelection extends Combo { + + private static final long serialVersionUID = 6522063655406404279L; + + final List valueMapping; + final List keyMapping; + + public SingleSelection(final Composite parent, final List> mapping) { + super(parent, SWT.READ_ONLY); + this.valueMapping = mapping.stream() + .map(t -> t._2) + .collect(Collectors.toList()); + this.keyMapping = mapping.stream() + .map(t -> t._1) + .collect(Collectors.toList()); + super.setItems(this.valueMapping.toArray(new String[mapping.size()])); + } + + public void select(final String key) { + final int selectionindex = this.keyMapping.indexOf(key); + if (selectionindex < 0) { + return; + } + + super.select(selectionindex); + } + + public String getSelectionValue() { + final int selectionindex = super.getSelectionIndex(); + if (selectionindex < 0) { + return null; + } + + return this.keyMapping.get(selectionindex); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/WidgetFactory.java b/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/WidgetFactory.java new file mode 100644 index 00000000..3c09184a --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gui/service/widget/WidgetFactory.java @@ -0,0 +1,470 @@ +/* + * 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.gui.service.widget; + +import static ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService.POLYGLOT_TREE_ITEM_TEXT_DATA_KEY; +import static ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService.POLYGLOT_WIDGET_FUNCTION_KEY; + +import java.io.InputStream; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; + +import org.eclipse.rap.rwt.RWT; +import org.eclipse.swt.SWT; +import org.eclipse.swt.graphics.Device; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.ImageData; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Listener; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +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.profile.GuiProfile; +import ch.ethz.seb.sebserver.gbl.util.Tuple; +import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport; +import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey; +import ch.ethz.seb.sebserver.gui.service.i18n.PolyglotPageService; +import ch.ethz.seb.sebserver.gui.service.page.PageContext; + +@Lazy +@Service +@GuiProfile +public class WidgetFactory { + + private static final Logger log = LoggerFactory.getLogger(WidgetFactory.class); + + public enum IconButtonType { + MAXIMIZE("maximize.png"), + MINIMIZE("minimize.png"), + SAVE_ACTION("saveAction.png"), + NEW_ACTION("newAction.png"), + DELETE_ACTION("deleteAction.png"), + ; + + private String fileName; + private ImageData image = null; + + private IconButtonType(final String fileName) { + this.fileName = fileName; + } + + public Image getImage(final Device device) { + if (this.image == null) { + try { + final InputStream resourceAsStream = + WidgetFactory.class.getResourceAsStream("/static/images/" + this.fileName); + this.image = new ImageData(resourceAsStream); + } catch (final Exception e) { + log.error("Failed to load resource image: {}", this.fileName, e); + } + } + + return new Image(device, this.image); + } + + } + + private final PolyglotPageService polyglotPageService; + private final I18nSupport i18nSupport; + + public WidgetFactory(final PolyglotPageService polyglotPageService) { + this.polyglotPageService = polyglotPageService; + this.i18nSupport = polyglotPageService.getI18nSupport(); + } + + public Button buttonLocalized(final Composite parent, final String locTextKey) { + final Button button = new Button(parent, SWT.NONE); + this.injectI18n(button, new LocTextKey(locTextKey)); + return button; + } + + public Button buttonLocalized(final Composite parent, final LocTextKey locTextKey) { + final Button button = new Button(parent, SWT.NONE); + this.injectI18n(button, locTextKey); + return button; + } + + public Button buttonLocalized(final Composite parent, final String style, final String locTextKey) { + final Button button = new Button(parent, SWT.NONE); + this.injectI18n(button, new LocTextKey(locTextKey)); + button.setData(RWT.CUSTOM_VARIANT, style); + return button; + } + + public Label label(final Composite parent, final String text) { + final Label label = new Label(parent, SWT.NONE); + label.setText(text); + return label; + } + + public Label labelLocalized(final Composite parent, final String locTextKey) { + final Label label = new Label(parent, SWT.NONE); + this.injectI18n(label, new LocTextKey(locTextKey)); + return label; + } + + public Label labelLocalized(final Composite parent, final LocTextKey locTextKey) { + final Label label = new Label(parent, SWT.NONE); + this.injectI18n(label, locTextKey); + return label; + } + + public Label labelLocalized(final Composite parent, final String style, final LocTextKey locTextKey) { + final Label label = new Label(parent, SWT.NONE); + this.injectI18n(label, locTextKey); + label.setData(RWT.CUSTOM_VARIANT, style); + return label; + } + + public Label labelLocalized( + final Composite parent, + final LocTextKey locTextKey, + final LocTextKey locToolTextKey) { + + final Label label = new Label(parent, SWT.NONE); + this.injectI18n(label, locTextKey, locToolTextKey); + return label; + } + + public Label labelLocalized( + final Composite parent, + final String style, + final LocTextKey locTextKey, + final LocTextKey locToolTextKey) { + + final Label label = new Label(parent, SWT.NONE); + this.injectI18n(label, locTextKey, locToolTextKey); + label.setData(RWT.CUSTOM_VARIANT, style); + return label; + } + + public Tree treeLocalized(final Composite parent, final int style) { + final Tree tree = new Tree(parent, SWT.SINGLE | SWT.FULL_SELECTION); + this.injectI18n(tree); + return tree; + } + + public TreeItem treeItemLocalized(final Tree parent, final String locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.injectI18n(item, new LocTextKey(locTextKey)); + return item; + } + + public TreeItem treeItemLocalized(final Tree parent, final LocTextKey locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.injectI18n(item, locTextKey); + return item; + } + + public TreeItem treeItemLocalized(final TreeItem parent, final String locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.injectI18n(item, new LocTextKey(locTextKey)); + return item; + } + + public TreeItem treeItemLocalized(final TreeItem parent, final LocTextKey locTextKey) { + final TreeItem item = new TreeItem(parent, SWT.NONE); + this.injectI18n(item, locTextKey); + return item; + } + + public Table tableLocalized(final Composite parent) { + final Table table = new Table(parent, SWT.NONE); + this.injectI18n(table); + return table; + } + + public TableColumn tableColumnLocalized(final Table table, final String locTextKey) { + final TableColumn tableColumn = new TableColumn(table, SWT.NONE); + this.injectI18n(tableColumn, new LocTextKey(locTextKey)); + return tableColumn; + } + + public Label labelSeparator(final Composite parent) { + final Label label = new Label(parent, SWT.SEPARATOR); + return label; + } + + public Label imageButton( + final IconButtonType type, + final Composite parent, + final LocTextKey toolTip, + final Listener listener) { + + final Label imageButton = labelLocalized(parent, (LocTextKey) null, toolTip); + imageButton.setData(RWT.CUSTOM_VARIANT, "imageButton"); + imageButton.setImage(type.getImage(parent.getDisplay())); + if (listener != null) { + imageButton.addListener(SWT.MouseDown, listener); + } + return imageButton; + } + + public Label formLabelLocalized(final Composite parent, final String locTextKey) { + final Label label = labelLocalized(parent, locTextKey); + final GridData gridData = new GridData(SWT.RIGHT, SWT.CENTER, true, false); + label.setLayoutData(gridData); + return label; + } + + public Label formValueLabel(final Composite parent, final String value, final int span) { + final Label label = new Label(parent, SWT.NONE); + label.setText(value); + final GridData gridData = new GridData(SWT.LEFT, SWT.CENTER, true, false, span, 1); + label.setLayoutData(gridData); + return label; + } + + public Text formTextInput(final Composite parent, final String value) { + return formTextInput(parent, value, 1, 1); + } + + public Text formTextInput(final Composite parent, final String value, final int hspan, final int vspan) { + final Text textInput = new Text(parent, SWT.LEFT | SWT.BORDER); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false, hspan, vspan); + gridData.heightHint = 15; + textInput.setLayoutData(gridData); + textInput.setText(value); + return textInput; + } + + public Combo formSingleSelectionLocalized( + final Composite parent, + final String selection, + final List> items) { + + return formSingleSelectionLocalized(parent, selection, items, 1, 1); + } + + public Combo formSingleSelectionLocalized( + final Composite parent, + final String selection, + final List> items, + final int hspan, final int vspan) { + + final SingleSelection combo = singleSelectionLocalized(parent, items); + final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, false, hspan, vspan); + gridData.heightHint = 25; + combo.setLayoutData(gridData); + combo.select(selection); + return combo; + } + + public void formEmpty(final Composite parent) { + formEmpty(parent, 1, 1); + } + + public void formEmpty(final Composite parent, final int hspan, final int vspan) { + final Label empty = new Label(parent, SWT.LEFT); + empty.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, false, hspan, vspan)); + empty.setText(""); + } + + public SingleSelection singleSelectionLocalized( + final Composite parent, + final List> items) { + + final SingleSelection combo = new SingleSelection(parent, items); + this.injectI18n(combo, combo.valueMapping); + return combo; + } + + public void injectI18n(final Label label, final LocTextKey locTextKey) { + injectI18n(label, locTextKey, null); + } + + public void injectI18n(final Label label, final LocTextKey locTextKey, final LocTextKey locToolTipKey) { + final Consumer