diff --git a/pom.xml b/pom.xml index 8a0a68ab6a06673474bc17449ad6fd350d96775a..4214ccafcb62dc81aa32c23a632d48a91c6f6d99 100644 --- a/pom.xml +++ b/pom.xml @@ -38,6 +38,11 @@ <artifactId>kypo2-security-commons</artifactId> <version>1.0.40</version> </dependency> + <dependency> + <groupId>cz.muni.ics.kypo</groupId> + <artifactId>kypo-elasticsearch-documents</artifactId> + <version>1.0.16</version> + </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> @@ -114,6 +119,12 @@ <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> + <!--RandomStringUtils--> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>3.11</version> + </dependency> </dependencies> <build> diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java index 983d9e46babbcac857704ecdccaed7ead472194d..7977ea73778de3161da023e0ea6f6db09ba143d9 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/DemoApplication.java @@ -1,12 +1,18 @@ package cz.muni.ics.kypo.training.adaptive; import cz.muni.ics.kypo.commons.startup.config.MicroserviceRegistrationConfiguration; +import cz.muni.ics.kypo.training.adaptive.config.ObjectMappersConfiguration; +import cz.muni.ics.kypo.training.adaptive.config.ValidationMessagesConfig; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Import; @SpringBootApplication -@Import(value = {MicroserviceRegistrationConfiguration.class}) +@Import(value = { + MicroserviceRegistrationConfiguration.class, + ValidationMessagesConfig.class, + ObjectMappersConfiguration.class +}) public class DemoApplication { public static void main(String[] args) { diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/config/BeanValidationDeserializer.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/BeanValidationDeserializer.java new file mode 100644 index 0000000000000000000000000000000000000000..5cd104fe606a8360ddb8b75676a516736d1b9f52 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/BeanValidationDeserializer.java @@ -0,0 +1,39 @@ +package cz.muni.ics.kypo.training.adaptive.config; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.deser.BeanDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerBase; + +import javax.validation.*; +import java.io.IOException; +import java.util.Set; + +public class BeanValidationDeserializer extends BeanDeserializer { + + private final static ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + private final Validator validator = factory.getValidator(); + + public BeanValidationDeserializer(BeanDeserializerBase src) { + super(src); + } + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + Object instance = super.deserialize(p, ctxt); + validate(instance); + return instance; + } + + private void validate(Object instance) { + Set<ConstraintViolation<Object>> violations = validator.validate(instance); + if (!violations.isEmpty()) { + StringBuilder msg = new StringBuilder(); + msg.append("JSON object is not valid. Reasons (").append(violations.size()).append("): "); + for (ConstraintViolation<Object> violation : violations) { + msg.append(violation.getMessage()).append(", "); + } + throw new ConstraintViolationException(msg.toString(), violations); + } + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/config/ObjectMappersConfiguration.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/ObjectMappersConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..9bf57dc2d2abc6f743c2176d19b5e7030785e0d9 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/ObjectMappersConfiguration.java @@ -0,0 +1,81 @@ +package cz.muni.ics.kypo.training.adaptive.config; + +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.deser.BeanDeserializer; +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +/** + * The type Object mapper config elasticsearch. + */ +@Configuration +public class ObjectMappersConfiguration { + /** + * General object mapper bean. + * + * @return the object mapper + */ + @Bean + @Primary + public ObjectMapper objectMapper() { + return initializeBasicObjectMapperConfig(); + } + + /** + * Object mapper bean used in restTemplate calls. Basically, it contains validator settings to validate object in deserialization phase using Bean Validations. + * + * @return the object mapper + */ + @Bean + @Qualifier("webClientObjectMapper") + public ObjectMapper webClientObjectMapper() { + ObjectMapper objectMapper = initializeBasicObjectMapperConfig(); + objectMapper.registerModule(getSimpleValidationModule()); + return objectMapper; + } + + /** + * Object mapper object mapper. + * + * @return the object mapper + */ + @Bean("objMapperForElasticsearch") + public ObjectMapper elasticsearchObjectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + return objectMapper; + } + + private ObjectMapper initializeBasicObjectMapperConfig() { + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + return objectMapper; + } + + private SimpleModule getSimpleValidationModule() { + SimpleModule validationModule = new SimpleModule(); + validationModule.setDeserializerModifier(new BeanDeserializerModifier() { + @Override + public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) { + if (deserializer instanceof BeanDeserializer) { + return new BeanValidationDeserializer((BeanDeserializer) deserializer); + } + return deserializer; + } + }); + return validationModule; + } + + + +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/config/WebClientConfig.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/WebClientConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..3fb95d16639dbba438a85be5098c846427b2a167 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/config/WebClientConfig.java @@ -0,0 +1,167 @@ +package cz.muni.ics.kypo.training.adaptive.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException; +import cz.muni.ics.kypo.training.adaptive.exceptions.errors.JavaApiError; +import cz.muni.ics.kypo.training.adaptive.exceptions.errors.PythonApiError; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.io.IOException; + +/** + * The type Web client config. + */ +@Import(ObjectMappersConfiguration.class) +@Configuration +public class WebClientConfig { + + + @Value("${openstack-server.uri}") + private String kypoOpenStackURI; + @Value("${user-and-group-server.uri}") + private String userAndGroupURI; + @Value("${elasticsearch-service.uri}") + private String elasticsearchServiceURI; + + private ObjectMapper objectMapper; + + @Autowired + public WebClientConfig(@Qualifier("webClientObjectMapper") ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Openstack service web client web client. + * + * @return the web client + */ + @Bean + @Qualifier("sandboxServiceWebClient") + public WebClient sandboxServiceWebClient() { + return WebClient.builder() + .baseUrl(kypoOpenStackURI) + .defaultHeaders(headers -> { + headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + }) + .filters(exchangeFilterFunctions -> { + exchangeFilterFunctions.add(addSecurityHeader()); + exchangeFilterFunctions.add(openStackSandboxServiceExceptionHandlingFunction()); + }) + .build(); + } + + /** + * User management service web client web client. + * + * @return the web client + */ + @Bean + @Qualifier("userManagementServiceWebClient") + public WebClient userManagementServiceWebClient() { + return WebClient.builder() + .baseUrl(userAndGroupURI) + .defaultHeaders(headers -> { + headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + }) + .filters(exchangeFilterFunctions -> { + exchangeFilterFunctions.add(addSecurityHeader()); + exchangeFilterFunctions.add(javaMicroserviceExceptionHandlingFunction()); + }) + .build(); + } + + /** + * Elasticsearch service web client. + * + * @return the web client + */ + @Bean + @Qualifier("elasticsearchServiceWebClient") + public WebClient elasticsearchServiceWebClient() { + return WebClient.builder() + .baseUrl(elasticsearchServiceURI) + .defaultHeaders(headers -> { + headers.add(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); + }) + .filters(exchangeFilterFunctions -> { + exchangeFilterFunctions.add(addSecurityHeader()); + exchangeFilterFunctions.add(javaMicroserviceExceptionHandlingFunction()); + }) + .build(); + } + + private ExchangeFilterFunction addSecurityHeader() { + return (request, next) -> { + OAuth2Authentication authenticatedUser = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication(); + OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authenticatedUser.getDetails(); + ClientRequest filtered = ClientRequest.from(request) + .header("Authorization", "Bearer " + details.getTokenValue()) + .build(); + return next.exchange(filtered); + }; + } + + private ExchangeFilterFunction openStackSandboxServiceExceptionHandlingFunction() { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + if(clientResponse.statusCode().is4xxClientError() || clientResponse.statusCode().is5xxServerError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> { + if (errorBody == null || errorBody.isBlank()) { + throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode()); + } + PythonApiError pythonApiError = null; + try { + pythonApiError = objectMapper.readValue(errorBody, PythonApiError.class); + pythonApiError.setStatus(clientResponse.statusCode()); + } catch (IOException e) { + throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode()); + } + throw new CustomWebClientException(pythonApiError); + }); + } else { + return Mono.just(clientResponse); + } + }); + } + + private ExchangeFilterFunction javaMicroserviceExceptionHandlingFunction() { + return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> { + if(clientResponse.statusCode().is4xxClientError() || clientResponse.statusCode().is5xxServerError()) { + return clientResponse.bodyToMono(String.class) + .flatMap(errorBody -> { + if (errorBody == null || errorBody.isBlank()) { + throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode()); + } + JavaApiError javaApiError = null; + try { + javaApiError = objectMapper.readValue(errorBody, JavaApiError.class); + javaApiError.setStatus(clientResponse.statusCode()); + } catch (IOException e) { + throw new CustomWebClientException("Error from external microservice. No specific message provided.", clientResponse.statusCode()); + } + throw new CustomWebClientException(javaApiError); + }); + } else { + return Mono.just(clientResponse); + } + }); + } + +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java index 0ea3b0079b55ffd814c8cfe8551ea2fc25861c4b..b02ce1dcf6f7c631c771a0cdcf531c10d9abd3a9 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/domain/phases/AbstractPhase.java @@ -1,15 +1,9 @@ package cz.muni.ics.kypo.training.adaptive.domain.phases; -import javax.persistence.Column; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Inheritance; -import javax.persistence.InheritanceType; -import javax.persistence.SequenceGenerator; -import javax.persistence.Table; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; + +import javax.persistence.*; import java.io.Serializable; @@ -31,7 +25,9 @@ public abstract class AbstractPhase implements Serializable { @Column(name = "order_in_training_definition", nullable = false) private Integer order; - private Long trainingDefinitionId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "training_definition_id") + private TrainingDefinition trainingDefinition; public String getTitle() { return title; @@ -57,12 +53,12 @@ public abstract class AbstractPhase implements Serializable { this.id = id; } - public Long getTrainingDefinitionId() { - return trainingDefinitionId; + public TrainingDefinition getTrainingDefinition() { + return trainingDefinition; } - public void setTrainingDefinitionId(Long trainingDefinition) { - this.trainingDefinitionId = trainingDefinition; + public void setTrainingDefinition(TrainingDefinition trainingDefinition) { + this.trainingDefinition = trainingDefinition; } @Override @@ -71,7 +67,7 @@ public abstract class AbstractPhase implements Serializable { "id=" + id + ", title='" + title + '\'' + ", order=" + order + - ", trainingDefinitionId=" + trainingDefinitionId + + ", trainingDefinitionId=" + trainingDefinition + '}'; } } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java index 4006b1a38e16405d0b528c81f0dc5d8876a7a0aa..45f926ade1e5b84097b7ff876b39e01b2ba78b20 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/dto/imports/ImportTrainingDefinitionDTO.java @@ -5,6 +5,7 @@ import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.AbstractPhaseImport import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import javax.validation.Valid; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotNull; import java.util.ArrayList; @@ -32,6 +33,7 @@ public class ImportTrainingDefinitionDTO { @ApiModelProperty(value = "Sign if stepper bar should be displayed.", example = "false") @NotNull(message = "{trainingdefinitionimport.showStepperBar.NotNull.message}") private boolean showStepperBar; + @Valid @ApiModelProperty(value = "Information about all levels in training definition.") private List<AbstractPhaseImportDTO> phases = new ArrayList<>(); @ApiModelProperty(value = "Estimated time it takes to finish runs created from this definition.", example = "5") diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/enums/RoleType.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/enums/RoleType.java new file mode 100644 index 0000000000000000000000000000000000000000..49983cb00caaa12fbd321ba385923b2d12744cd5 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/enums/RoleType.java @@ -0,0 +1,25 @@ +package cz.muni.ics.kypo.training.adaptive.enums; + +/** + * The enumeration of user roles. + * + */ +public enum RoleType { + + /** + * Role training administrator role. + */ + ROLE_TRAINING_ADMINISTRATOR, + /** + * Role training designer role. + */ + ROLE_TRAINING_DESIGNER, + /** + * Role training organizer role. + */ + ROLE_TRAINING_ORGANIZER, + /** + * Role training trainee role. + */ + ROLE_TRAINING_TRAINEE; +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ElasticsearchTrainingServiceLayerException.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ElasticsearchTrainingServiceLayerException.java new file mode 100644 index 0000000000000000000000000000000000000000..3c43ee11088d1c41f2141650020d34b2f80954d5 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/exceptions/ElasticsearchTrainingServiceLayerException.java @@ -0,0 +1,41 @@ +package cz.muni.ics.kypo.training.adaptive.exceptions; + +/** + * The type Elasticsearch training service layer exception. + */ +public class ElasticsearchTrainingServiceLayerException extends RuntimeException { + + /** + * Instantiates a new Elasticsearch training service layer exception. + */ + public ElasticsearchTrainingServiceLayerException() { + } + + /** + * Instantiates a new Elasticsearch training service layer exception. + * + * @param message the message + */ + public ElasticsearchTrainingServiceLayerException(String message) { + super(message); + } + + /** + * Instantiates a new Elasticsearch training service layer exception. + * + * @param message the message + * @param ex the exception + */ + public ElasticsearchTrainingServiceLayerException(String message, Throwable ex) { + super(message, ex); + } + + /** + * Instantiates a new Elasticsearch training service layer exception. + * + * @param ex the exception + */ + public ElasticsearchTrainingServiceLayerException(Throwable ex) { + super(ex); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java index 116b26e4cdd08650e3da07ac2a4ce7ab99c5492b..0acc8226734f40c1b063a35062ffc7581240fca2 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/mapper/mapstruct/QuestionMapper.java @@ -10,8 +10,6 @@ import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.questionnaire.Quest import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.questionnaire.QuestionImportDTO; import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionChoiceDTO; import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionDTO; -import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionUpdateDTO; -import cz.muni.ics.kypo.training.adaptive.dto.responses.PageResultResource; import org.mapstruct.Mapper; import org.mapstruct.ReportingPolicy; import org.springframework.data.domain.Page; @@ -28,7 +26,6 @@ import java.util.*; public interface QuestionMapper extends ParentMapper { // QUESTIONS Question mapToEntity(QuestionDTO dto); - Question mapToEntity(QuestionUpdateDTO dto); Question mapToEntity(QuestionImportDTO dto); QuestionArchiveDTO mapToQuestionArchiveDTO(Question entity); diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java index e2e274b580304414bd4737e41bae35c5f75cd20c..57828a7a83178a5af21f03ac3ec254f882d383fb 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/AbstractPhaseRepository.java @@ -7,26 +7,28 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; +import javax.persistence.NamedQuery; import java.util.List; +import java.util.Optional; @Repository public interface AbstractPhaseRepository extends JpaRepository<AbstractPhase, Long> { List<AbstractPhase> findAllByTrainingDefinitionIdOrderByOrder(long trainingDefinitionId); - @Query("SELECT COALESCE(MAX(l.order), -1) FROM AbstractPhase l WHERE l.trainingDefinitionId = :trainingDefinitionId") + @Query("SELECT COALESCE(MAX(l.order), -1) FROM AbstractPhase l WHERE l.trainingDefinition = :trainingDefinitionId") Integer getCurrentMaxOrder(@Param("trainingDefinitionId") Long trainingDefinitionId); @Modifying @Query("UPDATE AbstractPhase l SET l.order = l.order - 1 " + - "WHERE l.trainingDefinitionId = :trainingDefinitionId " + + "WHERE l.trainingDefinition = :trainingDefinitionId " + "AND l.order > :order ") void decreaseOrderAfterPhaseWasDeleted(@Param("trainingDefinitionId") Long trainingDefinitionId, @Param("order") int order); @Modifying @Query("UPDATE AbstractPhase l SET l.order = l.order + 1 " + - "WHERE l.trainingDefinitionId = :trainingDefinitionId " + + "WHERE l.trainingDefinition = :trainingDefinitionId " + "AND l.order >= :lowerBound " + "AND l.order < :upperBound ") void increaseOrderOfPhasesOnInterval(@Param("trainingDefinitionId") Long trainingDefinitionId, @@ -35,11 +37,23 @@ public interface AbstractPhaseRepository extends JpaRepository<AbstractPhase, Lo @Modifying @Query("UPDATE AbstractPhase l SET l.order = l.order - 1 " + - "WHERE l.trainingDefinitionId = :trainingDefinitionId " + + "WHERE l.trainingDefinition = :trainingDefinitionId " + "AND l.order > :lowerBound " + "AND l.order <= :upperBound ") void decreaseOrderOfPhasesOnInterval(@Param("trainingDefinitionId") Long trainingDefinitionId, @Param("lowerBound") int lowerBound, @Param("upperBound") int upperBound); + @Query("SELECT ap FROM AbstractPhase ap WHERE l.trainingDefinition.id = :trainingDefinitionId AND ap.id = :phaseId") + Optional<AbstractPhase> findPhaseInDefinition(@Param("trainingDefinitionId") Long trainingDefinitionId, + @Param("phaseId") Long phaseId); + + @Query("SELECT ap FROM AbstractPhase ap " + + "JOIN FETCH ap.trainingDefinition td " + + "JOIN FETCH td.authors " + + "WHERE ap.id = :phaseId") + Optional<AbstractPhase> findByIdWithDefinition(@Param("phaseId") Long phaseId); + + @Query("SELECT ap FROM AbstractPhase ap WHERE ap.trainingDefinition.id = :trainingDefinitionId AND ap.order = 0") + Optional<AbstractPhase> findFirstPhaseOfTrainingDefinition(@Param("trainingDefinitionId") Long trainingDefinitionId); } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java index f1023a478aba48a01cbfab56c7e14fd39f379382..32cb2f1a6ded7ab3c3ff882321e05fc4d994c8ab 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/phases/TrainingPhaseRepository.java @@ -11,7 +11,7 @@ import java.util.List; @Repository public interface TrainingPhaseRepository extends JpaRepository<TrainingPhase, Long> { - @Query("SELECT COUNT(p.id) FROM TrainingPhase p WHERE p.trainingDefinitionId = :trainingDefinitionId") + @Query("SELECT COUNT(p.id) FROM TrainingPhase p WHERE p.trainingDefinition = :trainingDefinitionId") int getNumberOfExistingPhases(@Param("trainingDefinitionId") Long trainingDefinitionId); List<TrainingPhase> findAllByTrainingDefinitionIdOrderByOrder(Long trainingDefinitionId); diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java index 76a3a16d97a3e1ef21d3e024fa0e6af4c2aed5a0..e6bd126e33d9fc9f3961b49600da701497ee5660 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/repository/training/TrainingRunRepository.java @@ -104,12 +104,12 @@ public interface TrainingRunRepository extends JpaRepository<TrainingRun, Long>, Page<TrainingRun> findAllByParticipantRefId(@Param("userRefId") Long userRefId, Pageable pageable); /** - * Find training run by id including current level + * Find training run by id including current phase * * @param trainingRunId the training run id * @return {@link TrainingRun} including {@link AbstractPhase} */ - Optional<TrainingRun> findByIdWithLevel(@Param("trainingRunId") Long trainingRunId); + Optional<TrainingRun> findByIdWithPhase(@Param("trainingRunId") Long trainingRunId); /** * Find all training runs by id of associated training definition that are accessible to participant by user ref id. diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java index e26b95796953f3c138fd22c9f958570cd6c83729..7bd5508c0e9c22bc55174310651ffe1a01e18853 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/InfoPhaseService.java @@ -1,55 +1,112 @@ package cz.muni.ics.kypo.training.adaptive.service; +import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState; +import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase; import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; import cz.muni.ics.kypo.training.adaptive.dto.info.InfoPhaseDTO; import cz.muni.ics.kypo.training.adaptive.dto.info.InfoPhaseUpdateDTO; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException; import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper; import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository; import cz.muni.ics.kypo.training.adaptive.repository.phases.InfoPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import java.time.Clock; +import java.time.LocalDateTime; + +import static cz.muni.ics.kypo.training.adaptive.service.PhaseService.PHASE_NOT_FOUND; +import static cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService.ARCHIVED_OR_RELEASED; + @Service public class InfoPhaseService { private static final Logger LOG = LoggerFactory.getLogger(InfoPhaseService.class); + private final TrainingInstanceRepository trainingInstanceRepository; + private final TrainingDefinitionRepository trainingDefinitionRepository; private final InfoPhaseRepository infoPhaseRepository; private final AbstractPhaseRepository abstractPhaseRepository; @Autowired - public InfoPhaseService(InfoPhaseRepository infoPhaseRepository, AbstractPhaseRepository abstractPhaseRepository) { + public InfoPhaseService(TrainingDefinitionRepository trainingDefinitionRepository, + TrainingInstanceRepository trainingInstanceRepository, + InfoPhaseRepository infoPhaseRepository, + AbstractPhaseRepository abstractPhaseRepository) { + this.trainingDefinitionRepository = trainingDefinitionRepository; + this.trainingInstanceRepository = trainingInstanceRepository; this.infoPhaseRepository = infoPhaseRepository; this.abstractPhaseRepository = abstractPhaseRepository; } public InfoPhaseDTO createDefaultInfoPhase(Long trainingDefinitionId) { + TrainingDefinition trainingDefinition = findDefinitionById(trainingDefinitionId); + checkIfCanBeUpdated(trainingDefinition); + InfoPhase infoPhase = new InfoPhase(); infoPhase.setContent("Content of info phase"); infoPhase.setTitle("Title of info phase"); - infoPhase.setTrainingDefinitionId(trainingDefinitionId); + infoPhase.setTrainingDefinition(trainingDefinition); infoPhase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(trainingDefinitionId) + 1); InfoPhase persistedEntity = infoPhaseRepository.save(infoPhase); + trainingDefinition.setLastEdited(getCurrentTimeInUTC()); return BeanMapper.INSTANCE.toDto(persistedEntity); } - public InfoPhaseDTO updateInfoPhase(Long definitionId, Long phaseId, InfoPhaseUpdateDTO infoPhaseUpdateDto) { - InfoPhase infoPhaseUpdate = BeanMapper.INSTANCE.toEntity(infoPhaseUpdateDto); - infoPhaseUpdate.setId(phaseId); - InfoPhase persistedInfoPhase = infoPhaseRepository.findById(infoPhaseUpdate.getId()) - .orElseThrow(() -> new RuntimeException("Info phase was not found")); - // TODO throw proper exception once kypo2-training is migrated + public InfoPhaseDTO updateInfoPhase(Long definitionId, Long phaseId, InfoPhase infoPhaseToUpdate) { + TrainingDefinition trainingDefinition = findDefinitionById(definitionId); + checkIfCanBeUpdated(trainingDefinition); + if (!existPhaseInDefinition(trainingDefinition, infoPhaseToUpdate.getId())) { + throw new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", infoPhaseToUpdate.getId().getClass(), + infoPhaseToUpdate.getId(), "Phase was not found in definition (id: " + definitionId + ").")); + } + infoPhaseToUpdate.setId(phaseId); + InfoPhase persistedInfoPhase = findInfoPhaseById(phaseId); + infoPhaseToUpdate.setTrainingDefinition(persistedInfoPhase.getTrainingDefinition()); + infoPhaseToUpdate.setOrder(persistedInfoPhase.getOrder()); - // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed) + InfoPhase savedEntity = infoPhaseRepository.save(infoPhaseToUpdate); + return BeanMapper.INSTANCE.toDto(savedEntity); + } - infoPhaseUpdate.setTrainingDefinitionId(persistedInfoPhase.getTrainingDefinitionId()); - infoPhaseUpdate.setOrder(persistedInfoPhase.getOrder()); + private InfoPhase findInfoPhaseById(Long phaseId) { + return infoPhaseRepository.findById(phaseId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND))); + } - InfoPhase savedEntity = infoPhaseRepository.save(infoPhaseUpdate); - return BeanMapper.INSTANCE.toDto(savedEntity); + private TrainingDefinition findDefinitionById(Long id) { + return trainingDefinitionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id))); + } + + private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) { + if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + ARCHIVED_OR_RELEASED)); + } + if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + "Cannot update training definition with already created training instance. " + + "Remove training instance/s before updating training definition.")); + } + } + + private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long levelId) { + return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), levelId) + .isPresent(); + } + + private LocalDateTime getCurrentTimeInUTC() { + return LocalDateTime.now(Clock.systemUTC()); } } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java index 70753bc9e7e3f5a4f1fb7ffbe7666a42e87ac7af..3f88010d511dff04885480201712b5d0a82904a7 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/PhaseService.java @@ -1,39 +1,72 @@ package cz.muni.ics.kypo.training.adaptive.service; +import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState; import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; import cz.muni.ics.kypo.training.adaptive.dto.AbstractPhaseDTO; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException; import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper; import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository; +import cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.Clock; +import java.time.LocalDateTime; import java.util.List; @Service @Transactional public class PhaseService { + public static final String PHASE_NOT_FOUND = "Phase not found."; + + private final AbstractPhaseRepository abstractPhaseRepository; private final TrainingPhaseService trainingPhaseService; + private final TrainingDefinitionRepository trainingDefinitionRepository; @Autowired - public PhaseService(AbstractPhaseRepository abstractPhaseRepository, TrainingPhaseService trainingPhaseService) { + public PhaseService(AbstractPhaseRepository abstractPhaseRepository, + TrainingPhaseService trainingPhaseService, + TrainingDefinitionRepository trainingDefinitionRepository) { this.abstractPhaseRepository = abstractPhaseRepository; this.trainingPhaseService = trainingPhaseService; + this.trainingDefinitionRepository = trainingDefinitionRepository; } - public void deletePhase(Long definitionId, Long phaseId) { - AbstractPhase phase = abstractPhaseRepository.findById(phaseId) - .orElseThrow(() -> new RuntimeException("Phase was not found")); - // TODO throw proper exception once kypo2-training is migrated + private TrainingDefinition findDefinitionById(Long id) { + return trainingDefinitionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id))); + } - // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed) + public AbstractPhase findPhaseById(Long phaseId) { + return abstractPhaseRepository.findById(phaseId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND))); + } - int phaseOrder = phase.getOrder(); - abstractPhaseRepository.decreaseOrderAfterPhaseWasDeleted(definitionId, phaseOrder); + /** + * Deletes specific phase based on id + * + * @param definitionId - id of definition containing phase to be deleted + * @param phaseId - id of phase to be deleted + * @throws EntityNotFoundException training definition or phase is not found. + * @throws EntityConflictException phase cannot be deleted in released or archived training definition. + */ + public void deletePhase(Long definitionId, Long phaseId) { + TrainingDefinition trainingDefinition = findDefinitionById(definitionId); + if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) + throw new EntityConflictException(new EntityErrorDetail(AbstractPhase.class, "id", definitionId.getClass(), definitionId, TrainingDefinitionService.ARCHIVED_OR_RELEASED)); - abstractPhaseRepository.delete(phase); + AbstractPhase phaseToDelete = findPhaseById(phaseId); + abstractPhaseRepository.delete(phaseToDelete); + int phaseOrder = phaseToDelete.getOrder(); + abstractPhaseRepository.decreaseOrderAfterPhaseWasDeleted(definitionId, phaseOrder); + trainingDefinition.setLastEdited(getCurrentTimeInUTC()); } @Transactional(readOnly = true) @@ -54,24 +87,39 @@ public class PhaseService { return BeanMapper.INSTANCE.toDtoList(phases); } - public void movePhaseToSpecifiedOrder(Long phaseIdFrom, int newPosition) { + /** + * Move phase to the different position and modify orders of phases between moved phase and new position. + * + * @param definitionId - Id of definition containing phases, this training definition is updating its last edited column. + * @param phaseIdFrom - id of the phase to be moved to the new position + * @param newPosition - position where phase will be moved + * @throws EntityNotFoundException training definition or one of the phases is not found. + * @throws EntityConflictException released or archived training definition cannot be modified. + */ + public void movePhaseToSpecifiedOrder(Long definitionId, Long phaseIdFrom, int newPosition) { + Integer maxOrderOfLevel = abstractPhaseRepository.getCurrentMaxOrder(definitionId); + if (newPosition < 0) { + newPosition = 0; + } else if (newPosition > maxOrderOfLevel) { + newPosition = maxOrderOfLevel; + } AbstractPhase phaseFrom = abstractPhaseRepository.findById(phaseIdFrom) - .orElseThrow(() -> new RuntimeException("Phase was not found")); - // TODO throw proper exception once kypo2-training is migrated - + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseIdFrom.getClass(), phaseIdFrom, PHASE_NOT_FOUND))); int fromOrder = phaseFrom.getOrder(); - if (fromOrder < newPosition) { - abstractPhaseRepository.decreaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinitionId(), fromOrder, newPosition); + if (fromOrder == newPosition) { + return; } else if (fromOrder > newPosition) { - abstractPhaseRepository.increaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinitionId(), newPosition, fromOrder); + abstractPhaseRepository.increaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinition().getId(), newPosition, fromOrder); } else { - // nothing should be changed, no further actions needed - return; + abstractPhaseRepository.decreaseOrderOfPhasesOnInterval(phaseFrom.getTrainingDefinition().getId(), fromOrder, newPosition); } - phaseFrom.setOrder(newPosition); abstractPhaseRepository.save(phaseFrom); - trainingPhaseService.alignDecisionMatrixForPhasesInTrainingDefinition(phaseFrom.getTrainingDefinitionId()); + trainingPhaseService.alignDecisionMatrixForPhasesInTrainingDefinition(phaseFrom.getTrainingDefinition().getId()); + } + + private LocalDateTime getCurrentTimeInUTC() { + return LocalDateTime.now(Clock.systemUTC()); } } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java index c2898101e8627ada78c7b5347aa406aec34fb493..b178de42d9367b0bb43bc043e446de59e946f96a 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/QuestionnairePhaseService.java @@ -1,37 +1,55 @@ package cz.muni.ics.kypo.training.adaptive.service; +import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState; +import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase; import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.Question; import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.QuestionPhaseRelation; import cz.muni.ics.kypo.training.adaptive.domain.phases.QuestionnairePhase; import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance; import cz.muni.ics.kypo.training.adaptive.dto.PhaseCreateDTO; +import cz.muni.ics.kypo.training.adaptive.dto.imports.phases.questionnaire.QuestionnairePhaseImportDTO; import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionPhaseRelationDTO; import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionnairePhaseDTO; import cz.muni.ics.kypo.training.adaptive.dto.questionnaire.QuestionnaireUpdateDTO; import cz.muni.ics.kypo.training.adaptive.enums.PhaseTypeCreate; import cz.muni.ics.kypo.training.adaptive.enums.QuestionnaireType; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException; import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper; import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository; import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionPhaseRelationRepository; import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionRepository; import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionnairePhaseRepository; import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; +import java.time.Clock; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; +import static cz.muni.ics.kypo.training.adaptive.service.PhaseService.PHASE_NOT_FOUND; +import static cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService.ARCHIVED_OR_RELEASED; + @Service public class QuestionnairePhaseService { private static final Logger LOG = LoggerFactory.getLogger(QuestionnairePhaseService.class); + private final TrainingDefinitionRepository trainingDefinitionRepository; + private final TrainingInstanceRepository trainingInstanceRepository; private final QuestionnairePhaseRepository questionnairePhaseRepository; private final AbstractPhaseRepository abstractPhaseRepository; private final QuestionRepository questionRepository; @@ -39,9 +57,15 @@ public class QuestionnairePhaseService { private final QuestionPhaseRelationRepository questionPhaseRelationRepository; @Autowired - public QuestionnairePhaseService(QuestionnairePhaseRepository questionnairePhaseRepository, AbstractPhaseRepository abstractPhaseRepository, - QuestionRepository questionRepository, TrainingPhaseRepository trainingPhaseRepository, + public QuestionnairePhaseService(TrainingDefinitionRepository trainingDefinitionRepository, + TrainingInstanceRepository trainingInstanceRepository, + QuestionnairePhaseRepository questionnairePhaseRepository, + AbstractPhaseRepository abstractPhaseRepository, + QuestionRepository questionRepository, + TrainingPhaseRepository trainingPhaseRepository, QuestionPhaseRelationRepository questionPhaseRelationRepository) { + this.trainingDefinitionRepository = trainingDefinitionRepository; + this.trainingInstanceRepository = trainingInstanceRepository; this.questionnairePhaseRepository = questionnairePhaseRepository; this.abstractPhaseRepository = abstractPhaseRepository; this.questionRepository = questionRepository; @@ -50,9 +74,12 @@ public class QuestionnairePhaseService { } public QuestionnairePhaseDTO createDefaultQuestionnairePhase(Long trainingDefinitionId, PhaseCreateDTO phaseCreateDTO) { + TrainingDefinition trainingDefinition = findDefinitionById(trainingDefinitionId); + checkIfCanBeUpdated(trainingDefinition); + QuestionnairePhase questionnairePhase = new QuestionnairePhase(); questionnairePhase.setTitle("Title of questionnaire phase"); - questionnairePhase.setTrainingDefinitionId(trainingDefinitionId); + questionnairePhase.setTrainingDefinition(trainingDefinition); questionnairePhase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(trainingDefinitionId) + 1); if (PhaseTypeCreate.QUESTIONNAIRE_ADAPTIVE.equals(phaseCreateDTO.getPhaseType())) { @@ -62,34 +89,35 @@ public class QuestionnairePhaseService { } QuestionnairePhase persistedEntity = questionnairePhaseRepository.save(questionnairePhase); + trainingDefinition.setLastEdited(getCurrentTimeInUTC()); return BeanMapper.INSTANCE.toDto(persistedEntity); } - public QuestionnairePhaseDTO updateQuestionnairePhase(Long definitionId, Long phaseId, QuestionnaireUpdateDTO questionnaireUpdateDto) { - QuestionnairePhase questionnairePhase = BeanMapper.INSTANCE.toEntity(questionnaireUpdateDto); - questionnairePhase.setId(phaseId); - - QuestionnairePhase persistedQuestionnairePhase = questionnairePhaseRepository.findById(questionnairePhase.getId()) - .orElseThrow(() -> new RuntimeException("Questionnaire phase was not found")); - // TODO throw proper exception once kypo2-training is migrated - - // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed); - - questionnairePhase.setTrainingDefinitionId(persistedQuestionnairePhase.getTrainingDefinitionId()); - questionnairePhase.setOrder(persistedQuestionnairePhase.getOrder()); - questionnairePhase.setQuestionnaireType(persistedQuestionnairePhase.getQuestionnaireType()); - - questionnairePhase.getQuestionPhaseRelations().clear(); - questionnairePhase.getQuestionPhaseRelations().addAll(resolveQuestionnairePhaseRelationsUpdate(questionnairePhase, questionnaireUpdateDto)); - - if (!CollectionUtils.isEmpty(questionnairePhase.getQuestions())) { - questionnairePhase.getQuestions().forEach(x -> x.setQuestionnairePhase(questionnairePhase)); - for (Question question : questionnairePhase.getQuestions()) { + public QuestionnairePhaseDTO updateQuestionnairePhase(Long definitionId, Long phaseId, QuestionnaireUpdateDTO questionnaireUpdate) { + QuestionnairePhase questionnairePhaseToUpdate = BeanMapper.INSTANCE.toEntity(questionnaireUpdate); + TrainingDefinition trainingDefinition = findDefinitionById(definitionId); + checkIfCanBeUpdated(trainingDefinition); + if (!existPhaseInDefinition(trainingDefinition, questionnairePhaseToUpdate.getId())) { + throw new EntityNotFoundException(new EntityErrorDetail(QuestionnairePhase.class, "id", questionnairePhaseToUpdate.getId().getClass(), + questionnairePhaseToUpdate.getId(), "Phase was not found in definition (id: " + definitionId + ").")); + } + QuestionnairePhase persistedQuestionnairePhase = findQuestionnairePhaseById(phaseId); + questionnairePhaseToUpdate.setId(phaseId); + questionnairePhaseToUpdate.setTrainingDefinition(persistedQuestionnairePhase.getTrainingDefinition()); + questionnairePhaseToUpdate.setOrder(persistedQuestionnairePhase.getOrder()); + questionnairePhaseToUpdate.setQuestionnaireType(persistedQuestionnairePhase.getQuestionnaireType()); + + questionnairePhaseToUpdate.getQuestionPhaseRelations().clear(); + questionnairePhaseToUpdate.getQuestionPhaseRelations().addAll(resolveQuestionnairePhaseRelationsUpdate(questionnairePhaseToUpdate, questionnaireUpdate)); + + if (!CollectionUtils.isEmpty(questionnairePhaseToUpdate.getQuestions())) { + questionnairePhaseToUpdate.getQuestions().forEach(x -> x.setQuestionnairePhase(questionnairePhaseToUpdate)); + for (Question question : questionnairePhaseToUpdate.getQuestions()) { question.getChoices().forEach(x -> x.setQuestion(question)); } } - QuestionnairePhase savedEntity = questionnairePhaseRepository.save(questionnairePhase); + QuestionnairePhase savedEntity = questionnairePhaseRepository.save(questionnairePhaseToUpdate); return BeanMapper.INSTANCE.toDto(savedEntity); } @@ -131,4 +159,35 @@ public class QuestionnairePhaseService { return questionnairePhaseRelations; } + private QuestionnairePhase findQuestionnairePhaseById(Long phaseId) { + return questionnairePhaseRepository.findById(phaseId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(QuestionnairePhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND))); + } + + private TrainingDefinition findDefinitionById(Long id) { + return trainingDefinitionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id))); + } + + private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) { + if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + ARCHIVED_OR_RELEASED)); + } + if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + "Cannot update training definition with already created training instance. " + + "Remove training instance/s before updating training definition.")); + } + } + + private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long levelId) { + return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), levelId) + .isPresent(); + } + + private LocalDateTime getCurrentTimeInUTC() { + return LocalDateTime.now(Clock.systemUTC()); + } + } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java index c30730dd93ad5eb111934a069d16866a527964f2..30ef420e0797214d26dfc79ddc42d57afb2d5197 100644 --- a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/TrainingPhaseService.java @@ -1,12 +1,19 @@ package cz.muni.ics.kypo.training.adaptive.service; +import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState; +import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase; import cz.muni.ics.kypo.training.adaptive.domain.phases.DecisionMatrixRow; import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; import cz.muni.ics.kypo.training.adaptive.dto.training.TrainingPhaseDTO; -import cz.muni.ics.kypo.training.adaptive.dto.training.TrainingPhaseUpdateDTO; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException; import cz.muni.ics.kypo.training.adaptive.mapper.BeanMapper; import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository; import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -14,12 +21,17 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; +import java.time.Clock; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; +import static cz.muni.ics.kypo.training.adaptive.service.PhaseService.PHASE_NOT_FOUND; +import static cz.muni.ics.kypo.training.adaptive.service.training.TrainingDefinitionService.ARCHIVED_OR_RELEASED; + @Service @Transactional public class TrainingPhaseService { @@ -27,48 +39,73 @@ public class TrainingPhaseService { private static final Logger LOG = LoggerFactory.getLogger(TrainingPhaseService.class); private final TrainingPhaseRepository trainingPhaseRepository; + private final TrainingInstanceRepository trainingInstanceRepository; private final AbstractPhaseRepository abstractPhaseRepository; + private final TrainingDefinitionRepository trainingDefinitionRepository; @Autowired - public TrainingPhaseService(TrainingPhaseRepository trainingPhaseRepository, AbstractPhaseRepository abstractPhaseRepository) { + public TrainingPhaseService(TrainingDefinitionRepository trainingDefinitionRepository, + TrainingInstanceRepository trainingInstanceRepository, + TrainingPhaseRepository trainingPhaseRepository, + AbstractPhaseRepository abstractPhaseRepository) { + this.trainingInstanceRepository = trainingInstanceRepository; + this.trainingDefinitionRepository = trainingDefinitionRepository; this.trainingPhaseRepository = trainingPhaseRepository; this.abstractPhaseRepository = abstractPhaseRepository; } public TrainingPhaseDTO createDefaultTrainingPhase(Long trainingDefinitionId) { + TrainingDefinition trainingDefinition = findDefinitionById(trainingDefinitionId); + checkIfCanBeUpdated(trainingDefinition); + TrainingPhase trainingPhase = new TrainingPhase(); trainingPhase.setTitle("Title of training phase"); - trainingPhase.setTrainingDefinitionId(trainingDefinitionId); + trainingPhase.setTrainingDefinition(trainingDefinition); trainingPhase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(trainingDefinitionId) + 1); trainingPhase.setDecisionMatrix(prepareDefaultDecisionMatrix(trainingDefinitionId, trainingPhase)); TrainingPhase persistedEntity = trainingPhaseRepository.save(trainingPhase); + trainingDefinition.setLastEdited(getCurrentTimeInUTC()); return BeanMapper.INSTANCE.toDto(persistedEntity); } - public TrainingPhaseDTO updateTrainingPhase(Long definitionId, Long phaseId, TrainingPhaseUpdateDTO trainingPhaseUpdate) { - TrainingPhase trainingPhase = BeanMapper.INSTANCE.toEntity(trainingPhaseUpdate); - trainingPhase.setId(phaseId); - - TrainingPhase persistedTrainingPhase = trainingPhaseRepository.findById(trainingPhase.getId()) - .orElseThrow(() -> new RuntimeException("Training phase was not found")); - // TODO throw proper exception once kypo2-training is migrated - // TODO add check to trainingDefinitionId and phaseId (field structure will be probably changed); - - trainingPhase.setTrainingDefinitionId(persistedTrainingPhase.getTrainingDefinitionId()); - trainingPhase.setOrder(persistedTrainingPhase.getOrder()); - trainingPhase.setTasks(persistedTrainingPhase.getTasks()); + /** + * Updates training phase in training definition + * + * @param definitionId - id of training definition containing phase to be updated + * @param trainingPhaseToUpdate phase to be updated + * @throws EntityNotFoundException training definition is not found. + * @throws EntityConflictException phase cannot be updated in released or archived training definition. + */ + public TrainingPhaseDTO updateTrainingPhase(Long definitionId, Long phaseId, TrainingPhase trainingPhaseToUpdate) { + TrainingDefinition trainingDefinition = findDefinitionById(definitionId); + checkIfCanBeUpdated(trainingDefinition); + if (!existPhaseInDefinition(trainingDefinition, trainingPhaseToUpdate.getId())) { + throw new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", trainingPhaseToUpdate.getId().getClass(), + trainingPhaseToUpdate.getId(), "Level was not found in definition (id: " + definitionId + ").")); + } - if (!CollectionUtils.isEmpty(trainingPhase.getDecisionMatrix())) { - trainingPhase.getDecisionMatrix().forEach(x -> x.setTrainingPhase(trainingPhase)); + TrainingPhase persistedTrainingPhase = findPhaseById(phaseId); + trainingPhaseToUpdate.setId(phaseId); + trainingPhaseToUpdate.setTrainingDefinition(persistedTrainingPhase.getTrainingDefinition()); + trainingPhaseToUpdate.setOrder(persistedTrainingPhase.getOrder()); + trainingPhaseToUpdate.setTasks(persistedTrainingPhase.getTasks()); + //TODO check that matrices are part of the persisted training phase + if (!CollectionUtils.isEmpty(trainingPhaseToUpdate.getDecisionMatrix())) { + trainingPhaseToUpdate.getDecisionMatrix().forEach(x -> x.setTrainingPhase(trainingPhaseToUpdate)); } - TrainingPhase savedEntity = trainingPhaseRepository.save(trainingPhase); + TrainingPhase savedEntity = trainingPhaseRepository.save(trainingPhaseToUpdate); return BeanMapper.INSTANCE.toDto(savedEntity); } + public TrainingPhase findPhaseById(Long phaseId) { + return trainingPhaseRepository.findById(phaseId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND))); + } + public void alignDecisionMatrixForPhasesInTrainingDefinition(Long trainingDefinitionId) { List<TrainingPhase> trainingPhases = trainingPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(trainingDefinitionId); @@ -140,4 +177,30 @@ public class TrainingPhaseService { .collect(Collectors.toList()); } + + private TrainingDefinition findDefinitionById(Long id) { + return trainingDefinitionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id))); + } + + private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) { + if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + ARCHIVED_OR_RELEASED)); + } + if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + "Cannot update training definition with already created training instance. " + + "Remove training instance/s before updating training definition.")); + } + } + + private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long levelId) { + return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), levelId) + .isPresent(); + } + + private LocalDateTime getCurrentTimeInUTC() { + return LocalDateTime.now(Clock.systemUTC()); + } } diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/ElasticsearchServiceApi.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/ElasticsearchServiceApi.java new file mode 100644 index 0000000000000000000000000000000000000000..c18311a25c770c6f3bf5259170b0282103440468 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/ElasticsearchServiceApi.java @@ -0,0 +1,137 @@ +package cz.muni.ics.kypo.training.adaptive.service.api; + +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun; +import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException; +import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; + +import java.util.List; +import java.util.Map; + +/** + * The type User service. + */ +@Service +public class ElasticsearchServiceApi { + + private static final Logger LOG = LoggerFactory.getLogger(ElasticsearchServiceApi.class); + private WebClient elasticsearchServiceWebClient; + + /** + * Instantiates a new ElasticSearchService API. + * + * @param elasticsearchServiceWebClient the web client + */ + public ElasticsearchServiceApi(@Qualifier("elasticsearchServiceWebClient") WebClient elasticsearchServiceWebClient) { + this.elasticsearchServiceWebClient = elasticsearchServiceWebClient; + } + + /** + * Deletes events from elasticsearch for particular training instance + * + * @param trainingInstanceId id of the training instance whose events to delete. + * @throws MicroserviceApiException error with specific message when calling elasticsearch microservice. + */ + public void deleteEventsByTrainingInstanceId(Long trainingInstanceId){ + try { + elasticsearchServiceWebClient + .delete() + .uri("/training-platform-events/training-instances/{instanceId}", trainingInstanceId) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (CustomWebClientException ex){ + throw new MicroserviceApiException("Error when calling Elasticsearch API to delete events for particular instance (ID: "+ trainingInstanceId +")", ex.getApiSubError()); + } + } + + + /** + * Obtain events from elasticsearch for particular training run + * + * @param trainingRun thee training run whose events to obtain. + * @throws MicroserviceApiException error with specific message when calling elasticsearch microservice. + */ + public List<Map<String, Object>> findAllEventsFromTrainingRun(TrainingRun trainingRun){ + try { + Long definitionId = trainingRun.getTrainingInstance().getTrainingDefinition().getId(); + Long instanceId = trainingRun.getTrainingInstance().getId(); + return elasticsearchServiceWebClient + .get() + .uri("/training-platform-events/training-definitions/{definitionId}/training-instances/{instanceId}/training-runs/{runId}", definitionId, instanceId, trainingRun.getId()) + .retrieve() + .bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {}) + .block(); + } catch (CustomWebClientException ex){ + throw new MicroserviceApiException("Error when calling Elasticsearch API for particular run (ID: "+ trainingRun.getId() +")", ex.getApiSubError()); + } + } + + /** + * Deletes events from elasticsearch for particular training run + * + * @param trainingInstanceId id of the training instance in which the training run is running. + * @param trainingRunId id of the training run whose events to delete. + * @throws MicroserviceApiException error with specific message when calling elasticsearch microservice. + */ + public void deleteEventsFromTrainingRun(Long trainingInstanceId, Long trainingRunId){ + try { + elasticsearchServiceWebClient + .delete() + .uri("/training-platform-events/training-instances/{instanceId}/training-runs/{runId}", trainingInstanceId, trainingRunId) + .retrieve() + .bodyToMono(Void.class) + .block(); + } catch (CustomWebClientException ex){ + throw new MicroserviceApiException("Error when calling Elasticsearch API to delete events for particular training run (ID: "+ trainingRunId +")", ex.getApiSubError()); + } + } + + public List<Map<String, Object>> findAllConsoleCommandsFromSandbox(Long sandboxId){ + try { + return elasticsearchServiceWebClient + .get() + .uri("/training-platform-commands/sandboxes/{sandboxId}", sandboxId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {}) + .block(); + }catch (CustomWebClientException ex){ + throw new MicroserviceApiException("Error when calling Elasticsearch API for particular sandbox (ID: "+ sandboxId +")", ex.getApiSubError()); + } + } + + public List<Map<String, Object>> findAllConsoleCommandsFromSandboxAndTimeRange(Integer sandboxId, Long from, Long to){ + try { + return elasticsearchServiceWebClient + .get() + .uri(uriBuilder -> uriBuilder.path("/training-platform-commands/sandboxes/{sandboxId}/ranges") + .queryParam("from", from) + .queryParam("to", to) + .build(sandboxId) + ) + .retrieve() + .bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {}) + .block(); + }catch (CustomWebClientException ex){ + throw new MicroserviceApiException("Error when calling Elasticsearch API for particular commands of sandbox (ID: "+ sandboxId +")", ex.getApiSubError()); + } + } + + public void deleteBashCommandsFromPool(Long poolId){ + try{ + elasticsearchServiceWebClient + .delete() + .uri("/training-platform-commands/pools/{poolId}", poolId) + .retrieve() + .bodyToMono(Void.class) + .block(); + }catch (CustomWebClientException ex){ + throw new MicroserviceApiException("Error when calling Elasticsearch API to delete bash commands for particular pool (ID: "+ poolId +")", ex.getApiSubError()); + } + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/SandboxServiceApi.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/SandboxServiceApi.java new file mode 100644 index 0000000000000000000000000000000000000000..a6f90baeac405412af2bf2d98bce2d5fb20814ea --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/api/SandboxServiceApi.java @@ -0,0 +1,128 @@ +package cz.muni.ics.kypo.training.adaptive.service.api; + +import cz.muni.ics.kypo.training.adaptive.dto.responses.LockedPoolInfo; +import cz.muni.ics.kypo.training.adaptive.dto.responses.PoolInfoDTO; +import cz.muni.ics.kypo.training.adaptive.dto.responses.SandboxDefinitionInfo; +import cz.muni.ics.kypo.training.adaptive.dto.responses.SandboxInfo; +import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException; +import cz.muni.ics.kypo.training.adaptive.exceptions.ForbiddenException; +import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException; +import cz.muni.ics.kypo.training.adaptive.exceptions.errors.PythonApiError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + + +/** + * The type User service. + */ +@Service +public class SandboxServiceApi { + + private static final Logger LOG = LoggerFactory.getLogger(SandboxServiceApi.class); + private WebClient sandboxServiceWebClient; + + + /** + * Instantiates a new SandboxService API. + * + * @param sandboxServiceWebClient the web client + */ + public SandboxServiceApi(@Qualifier("sandboxServiceWebClient") WebClient sandboxServiceWebClient) { + this.sandboxServiceWebClient = sandboxServiceWebClient; + } + + + public Long getAndLockSandboxForTrainingRun(Long poolId) { + try { + SandboxInfo sandboxInfo = sandboxServiceWebClient + .get() + .uri("/pools/{poolId}/sandboxes/get-and-lock", poolId) + .retrieve() + .bodyToMono(SandboxInfo.class) + .block(); + return sandboxInfo.getId(); + } catch (CustomWebClientException ex) { + if (ex.getStatusCode() == HttpStatus.CONFLICT) { + throw new ForbiddenException("There is no available sandbox, wait a minute and try again or ask organizer to allocate more sandboxes."); + } + throw new MicroserviceApiException("Error when calling OpenStack Sandbox Service API to get unlocked sandbox from pool (ID: " + poolId + ")", ex.getApiSubError()); + } + } + + /** + * Lock pool locked pool info. + * + * @param poolId the pool id + * @return the locked pool info + */ + public LockedPoolInfo lockPool(Long poolId) { + try { + return sandboxServiceWebClient + .post() + .uri("/pools/{poolId}/locks", poolId) + .body(Mono.just("{}"), String.class) + .retrieve() + .bodyToMono(LockedPoolInfo.class) + .block(); + } catch (CustomWebClientException ex) { + throw new MicroserviceApiException("Currently, it is not possible to lock and assign pool with (ID: " + poolId + ").", ex.getApiSubError()); + } + } + + /** + * Unlock pool. + * + * @param poolId the pool id + */ + public void unlockPool(Long poolId) { + try { + // get lock id from pool + PoolInfoDTO poolInfoDto = sandboxServiceWebClient + .get() + .uri("/pools/{poolId}", poolId) + .retrieve() + .bodyToMono(PoolInfoDTO.class) + .block(); + // unlock pool + if (poolInfoDto != null && poolInfoDto.getLockId() != null) { + sandboxServiceWebClient + .delete() + .uri("/pools/{poolId}/locks/{lockId}", poolId, poolInfoDto.getLockId()) + .retrieve() + .bodyToMono(Void.class) + .block(); + } + } catch (CustomWebClientException ex) { + if(ex.getStatusCode() != HttpStatus.NOT_FOUND){ + throw new MicroserviceApiException("Currently, it is not possible to unlock a pool with (ID: " + poolId + ").", ex.getApiSubError()); + } + } + } + + /** + * Gets sandbox definition id. + * + * @param poolId the pool id + * @return the sandbox definition id + */ + public SandboxDefinitionInfo getSandboxDefinitionId(Long poolId) { + try { + return sandboxServiceWebClient + .get() + .uri("/pools/{poolId}/definition", poolId) + .retrieve() + .bodyToMono(SandboxDefinitionInfo.class) + .block(); + } catch (CustomWebClientException ex) { + if (ex.getStatusCode() == HttpStatus.CONFLICT) { + throw new ForbiddenException("There is no available sandbox definition for particular pool (ID: " + poolId + ")."); + } + throw new MicroserviceApiException("Error when calling Python API to sandbox for particular pool (ID: " + poolId + ")", new PythonApiError(ex.getMessage())); + } + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditEventsService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditEventsService.java new file mode 100644 index 0000000000000000000000000000000000000000..4d86e261dc8a029b26c417d93b6632d950970e96 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditEventsService.java @@ -0,0 +1,202 @@ +package cz.muni.ics.kypo.training.adaptive.service.audit; + +import cz.muni.csirt.kypo.events.adaptive.trainings.*; +import cz.muni.csirt.kypo.events.adaptive.trainings.enums.PhaseType; +import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.QuestionnairePhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoUnit; + +/** + * The type Audit events service. + */ +@Service +public class AuditEventsService { + + private AuditService auditService; + + /** + * Instantiates a new Audit events service. + * + * @param auditService the audit service + */ + @Autowired + public AuditEventsService(AuditService auditService) { + this.auditService = auditService; + } + + + /** + * Audit training run started action. + * + * @param trainingRun the training run + */ + public void auditTrainingRunStartedAction(TrainingRun trainingRun) { + TrainingRunStarted.TrainingRunStartedBuilder<?, ?> trainingRunStartedBuilder = (TrainingRunStarted.TrainingRunStartedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, TrainingRunStarted.builder()); + + TrainingRunStarted trainingRunStarted = trainingRunStartedBuilder + .gameTime(0L) + .build(); + auditService.saveTrainingRunEvent(trainingRunStarted, 0L); + } + + /** + * Audit phase started action. + * + * @param trainingRun the training run + */ + public void auditPhaseStartedAction(TrainingRun trainingRun) { + PhaseStarted.PhaseStartedBuilder<?, ?> phaseStartedBuilder = (PhaseStarted.PhaseStartedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, PhaseStarted.builder()); + + PhaseStarted phaseStarted = phaseStartedBuilder + .phaseType(getPhaseType(trainingRun.getCurrentPhase())) + .phaseTitle(trainingRun.getCurrentPhase().getTitle()) + .build(); + auditService.saveTrainingRunEvent(phaseStarted, 10L); + } + + /** + * Audit phase completed action. + * + * @param trainingRun the training run + */ + public void auditPhaseCompletedAction(TrainingRun trainingRun) { + PhaseCompleted.PhaseCompletedBuilder<?, ?> phaseCompletedBuilder = (PhaseCompleted.PhaseCompletedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, PhaseCompleted.builder()); + + PhaseCompleted phaseCompleted = phaseCompletedBuilder + .phaseType(getPhaseType(trainingRun.getCurrentPhase())) + .build(); + auditService.saveTrainingRunEvent(phaseCompleted, 5L); + } + + /** + * Audit solution displayed action. + * + * @param trainingRun the training run + */ + public void auditSolutionDisplayedAction(TrainingRun trainingRun) { + SolutionDisplayed.SolutionDisplayedBuilder<?, ?> solutionDisplayedBuilder = (SolutionDisplayed.SolutionDisplayedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, SolutionDisplayed.builder()); + + SolutionDisplayed solutionDisplayed = solutionDisplayedBuilder + .build(); + auditService.saveTrainingRunEvent(solutionDisplayed, 0L); + } + + /** + * Audit correct flag submitted action. + * + * @param trainingRun the training run + * @param flag the flag + */ + public void auditCorrectFlagSubmittedAction(TrainingRun trainingRun, String flag) { + CorrectFlagSubmitted.CorrectFlagSubmittedBuilder<?, ?> correctFlagSubmittedBuilder = (CorrectFlagSubmitted.CorrectFlagSubmittedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, CorrectFlagSubmitted.builder()); + + CorrectFlagSubmitted correctFlagSubmitted = correctFlagSubmittedBuilder + .flagContent(flag) + .build(); + auditService.saveTrainingRunEvent(correctFlagSubmitted, 0L); + } + + /** + * Audit wrong flag submitted action. + * + * @param trainingRun the training run + * @param flag the flag + */ + public void auditWrongFlagSubmittedAction(TrainingRun trainingRun, String flag) { + WrongFlagSubmitted.WrongFlagSubmittedBuilder<?, ?> wrongFlagSubmittedBuilder = (WrongFlagSubmitted.WrongFlagSubmittedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, WrongFlagSubmitted.builder()); + + WrongFlagSubmitted wrongFlagSubmitted = wrongFlagSubmittedBuilder + .flagContent(flag) + .count(trainingRun.getIncorrectFlagCount()) + .build(); + auditService.saveTrainingRunEvent(wrongFlagSubmitted, 0L); + } + + /** + * Audit assessment answers action. + * + * @param trainingRun the training run + * @param answers the answers + */ + public void auditAssessmentAnswersAction(TrainingRun trainingRun, String answers) { + QuestionnaireAnswers.QuestionnaireAnswersBuilder<?, ?> questionnaireAnswersBuilder = (QuestionnaireAnswers.QuestionnaireAnswersBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, QuestionnaireAnswers.builder()); + + QuestionnaireAnswers questionnaireAnswers = questionnaireAnswersBuilder + .answers(answers) + .build(); + auditService.saveTrainingRunEvent(questionnaireAnswers, 0L); + } + + /** + * Audit training run ended action. + * + * @param trainingRun the training run + */ + public void auditTrainingRunEndedAction(TrainingRun trainingRun) { + TrainingRunEnded.TrainingRunEndedBuilder<?, ?> trainingRunEndedBuilder = (TrainingRunEnded.TrainingRunEndedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, TrainingRunEnded.builder()); + + TrainingRunEnded trainingRunEnded = trainingRunEndedBuilder + .startTime(trainingRun.getStartTime().atOffset(ZoneOffset.UTC).toInstant().toEpochMilli()) + .endTime(System.currentTimeMillis()) + .build(); + auditService.saveTrainingRunEvent(trainingRunEnded, 10L); + } + + /** + * Audit training run resumed action. + * + * @param trainingRun the training run + */ + public void auditTrainingRunResumedAction(TrainingRun trainingRun) { + TrainingRunResumed.TrainingRunResumedBuilder<?, ?> trainingRunResumedBuilder = (TrainingRunResumed.TrainingRunResumedBuilder<?, ?>) + fillInCommonBuilderFields(trainingRun, TrainingRunResumed.builder()); + TrainingRunResumed trainingRunResumed = trainingRunResumedBuilder.build(); + auditService.saveTrainingRunEvent(trainingRunResumed, 0L); + } + + private AbstractAuditAdaptivePOJO.AbstractAuditAdaptivePOJOBuilder<?, ?> fillInCommonBuilderFields(TrainingRun trainingRun, AbstractAuditAdaptivePOJO.AbstractAuditAdaptivePOJOBuilder<?, ?> builder) { + TrainingInstance trainingInstance = trainingRun.getTrainingInstance(); + builder.sandboxId(trainingRun.getSandboxInstanceRefId()) + .poolId(trainingInstance.getPoolId()) + .trainingRunId(trainingRun.getId()) + .trainingInstanceId(trainingInstance.getId()) + .trainingDefinitionId(trainingInstance.getTrainingDefinition().getId()) + .gameTime(computeGameTime(trainingRun.getStartTime())) + .userRefId(trainingRun.getParticipantRef().getUserRefId()) + .phaseId(trainingRun.getCurrentPhase().getId()); + return builder; + } + + private long computeGameTime(LocalDateTime gameStartedTime) { + return ChronoUnit.MILLIS.between(gameStartedTime, LocalDateTime.now(Clock.systemUTC())); + } + + private PhaseType getPhaseType(AbstractPhase abstractPhase) { + if (abstractPhase instanceof TrainingPhase) { + return PhaseType.TRAINING; + } else if (abstractPhase instanceof InfoPhase) { + return PhaseType.INFO; + } else if (abstractPhase instanceof QuestionnairePhase) { + return PhaseType.QUESTIONNAIRE; + } + return null; + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditService.java new file mode 100644 index 0000000000000000000000000000000000000000..6497b80273cbb8cf04620d73b0e9acbd91cae24e --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/audit/AuditService.java @@ -0,0 +1,55 @@ +package cz.muni.ics.kypo.training.adaptive.service.audit; + +import com.fasterxml.jackson.databind.ObjectMapper; +import cz.muni.csirt.kypo.events.AbstractAuditPOJO; +import cz.muni.csirt.kypo.events.adaptive.trainings.AbstractAuditAdaptivePOJO; +import cz.muni.ics.kypo.training.adaptive.exceptions.ElasticsearchTrainingServiceLayerException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.util.Assert; + +import java.io.IOException; + +/** + * The type Audit service. + */ +@Service +public class AuditService { + + private static Logger logger = LoggerFactory.getLogger(AuditService.class); + + private ObjectMapper objectMapper; + + /** + * Instantiates a new Audit service. + * + * @param objectMapper the object mapper + */ + @Autowired + public AuditService(@Qualifier("objMapperForElasticsearch") ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + /** + * Method for saving general class into Elasticsearch under specific index and type. + * + * @param <T> the type parameter + * @param pojoClass class saving to Elasticsearch + * @throws ElasticsearchTrainingServiceLayerException the elasticsearch training service layer exception + */ + public <T extends AbstractAuditAdaptivePOJO> void saveTrainingRunEvent(T pojoClass, long timestampDelay) throws ElasticsearchTrainingServiceLayerException { + Assert.notNull(pojoClass, "Null class could not be saved via audit method."); + try { + pojoClass.setTimestamp(System.currentTimeMillis() + timestampDelay); + pojoClass.setType(pojoClass.getClass().getName()); + + logger.info(objectMapper.writeValueAsString(pojoClass)); + } catch (IOException ex) { + throw new ElasticsearchTrainingServiceLayerException(ex); + } + } + +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/ExportImportService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/ExportImportService.java new file mode 100644 index 0000000000000000000000000000000000000000..2f5e5a729d13965ff80a12c42024666a8df6576b --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/ExportImportService.java @@ -0,0 +1,119 @@ +package cz.muni.ics.kypo.training.adaptive.service.training; + +import cz.muni.ics.kypo.training.adaptive.domain.phases.AbstractPhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.InfoPhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.QuestionnairePhase; +import cz.muni.ics.kypo.training.adaptive.domain.phases.TrainingPhase; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun; +import cz.muni.ics.kypo.training.adaptive.exceptions.*; +import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.InfoPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionnairePhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Set; + +/** + * The type Export import service. + */ +@Service +public class ExportImportService { + + private TrainingDefinitionRepository trainingDefinitionRepository; + private AbstractPhaseRepository abstractPhaseRepository; + private QuestionnairePhaseRepository questionnairePhaseRepository; + private InfoPhaseRepository infoPhaseRepository; + private TrainingPhaseRepository trainingPhaseRepository; + private TrainingInstanceRepository trainingInstanceRepository; + private TrainingRunRepository trainingRunRepository; + + /** + * Instantiates a new Export import service. + * + * @param trainingDefinitionRepository the training definition repository + * @param abstractPhaseRepository the abstract level repository + * @param questionnairePhaseRepository the assessment level repository + * @param infoPhaseRepository the info level repository + * @param trainingPhaseRepository the game level repository + * @param trainingInstanceRepository the training instance repository + * @param trainingRunRepository the training run repository + */ + @Autowired + public ExportImportService(TrainingDefinitionRepository trainingDefinitionRepository, + AbstractPhaseRepository abstractPhaseRepository, + QuestionnairePhaseRepository questionnairePhaseRepository, + InfoPhaseRepository infoPhaseRepository, + TrainingPhaseRepository trainingPhaseRepository, + TrainingInstanceRepository trainingInstanceRepository, + TrainingRunRepository trainingRunRepository) + { + this.trainingDefinitionRepository = trainingDefinitionRepository; + this.abstractPhaseRepository = abstractPhaseRepository; + this.questionnairePhaseRepository = questionnairePhaseRepository; + this.trainingPhaseRepository = trainingPhaseRepository; + this.infoPhaseRepository = infoPhaseRepository; + this.trainingInstanceRepository = trainingInstanceRepository; + this.trainingRunRepository = trainingRunRepository; + } + + /** + * Finds training definition with given id. + * + * @param trainingDefinitionId the id of definition to be found. + * @return the {@link TrainingDefinition} with the given id. + * @throws EntityNotFoundException if training definition was not found. + */ + public TrainingDefinition findById(Long trainingDefinitionId) { + return trainingDefinitionRepository.findById(trainingDefinitionId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinitionId.getClass(), + trainingDefinitionId))); + } + + /** + * Creates a phase and connects it with training definition. + * + * @param phase the {@link AbstractPhase} to be created. + * @param definition the {@link TrainingDefinition} to associate phase with. + */ + public void createLevel(AbstractPhase phase, TrainingDefinition definition) { + phase.setOrder(abstractPhaseRepository.getCurrentMaxOrder(definition.getId()) + 1); + phase.setTrainingDefinition(definition); + if (phase instanceof QuestionnairePhase) { + questionnairePhaseRepository.save((QuestionnairePhase) phase); + } else if (phase instanceof InfoPhase) { + infoPhaseRepository.save((InfoPhase) phase); + } else { + trainingPhaseRepository.save((TrainingPhase) phase); + } + } + + /** + * Finds training instance with given id. + * + * @param trainingInstanceId the id of instance to be found. + * @return the {@link TrainingInstance} with the given id. + * @throws EntityNotFoundException if training instance was not found. + */ + public TrainingInstance findInstanceById(Long trainingInstanceId) { + return trainingInstanceRepository.findById(trainingInstanceId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstanceId.getClass(), + trainingInstanceId))); + } + + /** + * Finds training runs associated with training instance with given id. + * + * @param trainingInstanceId the id of instance which runs are to be found. + * @return the set off all {@link TrainingRun} + */ + public Set<TrainingRun> findRunsByInstanceId(Long trainingInstanceId) { + return trainingRunRepository.findAllByTrainingInstanceId(trainingInstanceId); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/SecurityService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/SecurityService.java new file mode 100644 index 0000000000000000000000000000000000000000..f149e48e0563e9fd45b48fe9f3427f486b531864 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/SecurityService.java @@ -0,0 +1,184 @@ +package cz.muni.ics.kypo.training.adaptive.service.training; + +import cz.muni.ics.kypo.training.adaptive.annotations.transactions.TransactionalRO; +import cz.muni.ics.kypo.training.adaptive.domain.UserRef; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun; +import cz.muni.ics.kypo.training.adaptive.dto.UserRefDTO; +import cz.muni.ics.kypo.training.adaptive.enums.RoleTypeSecurity; +import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException; +import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.web.reactive.function.client.WebClient; + + +/** + * The type Security service. + */ +@Service +@TransactionalRO(propagation = Propagation.REQUIRES_NEW) +public class SecurityService { + + private TrainingRunRepository trainingRunRepository; + private TrainingDefinitionRepository trainingDefinitionRepository; + private TrainingInstanceRepository trainingInstanceRepository; + private WebClient userManagementWebClient; + + /** + * Instantiates a new Security service. + * + * @param trainingInstanceRepository the training instance repository + * @param trainingDefinitionRepository the training definition repository + * @param trainingRunRepository the training run repository + * @param userManagementWebClient the java rest template + */ + @Autowired + public SecurityService(TrainingInstanceRepository trainingInstanceRepository, + TrainingDefinitionRepository trainingDefinitionRepository, + TrainingRunRepository trainingRunRepository, + @Qualifier("userManagementServiceWebClient") WebClient userManagementWebClient) { + this.trainingDefinitionRepository = trainingDefinitionRepository; + this.trainingInstanceRepository = trainingInstanceRepository; + this.trainingRunRepository = trainingRunRepository; + this.userManagementWebClient = userManagementWebClient; + } + + /** + * Is trainee of given training run boolean. + * + * @param trainingRunId the training run id + * @return the boolean + */ + public boolean isTraineeOfGivenTrainingRun(Long trainingRunId) { + TrainingRun trainingRun = trainingRunRepository.findById(trainingRunId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), + trainingRunId, "The necessary permissions are required for a resource."))); + return trainingRun.getParticipantRef().getUserRefId().equals(getUserRefIdFromUserAndGroup()); + } + + /** + * Is organizer of given training instance boolean. + * + * @param instanceId the instance id + * @return the boolean + */ + public boolean isOrganizerOfGivenTrainingInstance(Long instanceId) { + TrainingInstance trainingInstance = trainingInstanceRepository.findById(instanceId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", instanceId.getClass(), + instanceId, "The necessary permissions are required for a resource."))); + return trainingInstance.getOrganizers().stream() + .anyMatch(o -> o.getUserRefId().equals(getUserRefIdFromUserAndGroup())); + } + + /** + * Is organizer of given training run. + * + * @param trainingRunId the run id + * @return the boolean + */ + public boolean isOrganizerOfGivenTrainingRun(Long trainingRunId) { + TrainingRun trainingRun = trainingRunRepository.findById(trainingRunId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), + trainingRunId, "The necessary permissions are required for a resource."))); + return trainingRun.getTrainingInstance().getOrganizers().stream() + .anyMatch(o -> o.getUserRefId().equals(getUserRefIdFromUserAndGroup())); + } + + /** + * Is designer of given training definition boolean. + * + * @param definitionId the definition id + * @return the boolean + */ + public boolean isDesignerOfGivenTrainingDefinition(Long definitionId) { + TrainingDefinition trainingDefinition = trainingDefinitionRepository.findById(definitionId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, + "id", definitionId.getClass(), definitionId, "The necessary permissions are required for a resource."))); + return trainingDefinition.getAuthors().stream() + .anyMatch(a -> a.getUserRefId().equals(getUserRefIdFromUserAndGroup())); + } + + /** + * Has role boolean. + * + * @param roleTypeSecurity the role type security + * @return the boolean + */ + public boolean hasRole(RoleTypeSecurity roleTypeSecurity) { + OAuth2Authentication authentication = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication(); + for (GrantedAuthority gA : authentication.getUserAuthentication().getAuthorities()) { + if (gA.getAuthority().equals(roleTypeSecurity.name())) { + return true; + } + } + return false; + } + + /** + * Gets user ref id from user and group. + * + * @return the user ref id from user and group + */ + public Long getUserRefIdFromUserAndGroup() { + try { + UserRefDTO userRefDTO = userManagementWebClient + .get() + .uri("/users/info") + .retrieve() + .bodyToMono(UserRefDTO.class) + .block(); + checkNonNull(userRefDTO, "Returned null response when calling user management service API to get info about logged in user."); + return userRefDTO.getUserRefId(); + } catch (CustomWebClientException ex) { + throw new MicroserviceApiException("Error when calling user management service API to get info about logged in user.", ex.getApiSubError()); + } + } + + /** + * Create user ref entity by info from user and group user ref. + * + * @return the user ref + */ + public UserRef createUserRefEntityByInfoFromUserAndGroup() { + try { + UserRefDTO userRefDTO = userManagementWebClient + .get() + .uri("/users/info") + .retrieve() + .bodyToMono(UserRefDTO.class) + .block(); + checkNonNull(userRefDTO, "Returned null response when calling user management service API to get info about logged in user."); + UserRef userRef = new UserRef(); + userRef.setUserRefId(userRefDTO.getUserRefId()); + return userRef; + } catch (CustomWebClientException ex) { + throw new MicroserviceApiException("Error when calling user management service API to get info about logged in user.", ex.getApiSubError()); + } + } + + /** + * Check if response from external API is not null. + * + * @param object object to check + * @param message exception message if response is null + * @throws MicroserviceApiException if response is null + */ + private void checkNonNull(Object object, String message) { + if (object == null) { + throw new MicroserviceApiException(message); + } + } + +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingDefinitionService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingDefinitionService.java new file mode 100644 index 0000000000000000000000000000000000000000..4857ddaf6ef91aa7aaa3cd7df87242d5b69aa722 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingDefinitionService.java @@ -0,0 +1,405 @@ +package cz.muni.ics.kypo.training.adaptive.service.training; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.querydsl.core.types.Predicate; +import cz.muni.ics.kypo.training.adaptive.domain.UserRef; +import cz.muni.ics.kypo.training.adaptive.domain.enums.TDState; +import cz.muni.ics.kypo.training.adaptive.domain.phases.*; +import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.Question; +import cz.muni.ics.kypo.training.adaptive.domain.phases.questions.QuestionPhaseRelation; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityConflictException; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException; +import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.InfoPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.QuestionnairePhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.TrainingPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingDefinitionRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + + +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * The type Training definition service. + */ +@Service +public class TrainingDefinitionService { + + private static final Logger LOG = LoggerFactory.getLogger(TrainingDefinitionService.class); + + private TrainingDefinitionRepository trainingDefinitionRepository; + private TrainingInstanceRepository trainingInstanceRepository; + private AbstractPhaseRepository abstractPhaseRepository; + private TrainingPhaseRepository trainingPhaseRepository; + private InfoPhaseRepository infoPhaseRepository; + private QuestionnairePhaseRepository questionnairePhaseRepository; + private UserRefRepository userRefRepository; + private SecurityService securityService; + private ObjectMapper objectMapper; + + public static final String ARCHIVED_OR_RELEASED = "Cannot edit released or archived training definition."; + private static final String PHASE_NOT_FOUND = "Phase not found."; + + /** + * Instantiates a new Training definition service. + * + * @param trainingDefinitionRepository the training definition repository + * @param abstractPhaseRepository the abstract phase repository + * @param infoPhaseRepository the info phase repository + * @param trainingPhaseRepository the training phase repository + * @param questionnairePhaseRepository the questionnaire phase repository + * @param trainingInstanceRepository the training instance repository + * @param userRefRepository the user ref repository + * @param securityService the security service + */ + @Autowired + public TrainingDefinitionService(TrainingDefinitionRepository trainingDefinitionRepository, + AbstractPhaseRepository abstractPhaseRepository, + InfoPhaseRepository infoPhaseRepository, + TrainingPhaseRepository trainingPhaseRepository, + QuestionnairePhaseRepository questionnairePhaseRepository, + TrainingInstanceRepository trainingInstanceRepository, + UserRefRepository userRefRepository, + SecurityService securityService, + ObjectMapper objectMapper) { + this.trainingDefinitionRepository = trainingDefinitionRepository; + this.abstractPhaseRepository = abstractPhaseRepository; + this.trainingPhaseRepository = trainingPhaseRepository; + this.infoPhaseRepository = infoPhaseRepository; + this.questionnairePhaseRepository = questionnairePhaseRepository; + this.trainingInstanceRepository = trainingInstanceRepository; + this.userRefRepository = userRefRepository; + this.securityService = securityService; + this.objectMapper = objectMapper; + } + + /** + * Finds specific Training Definition by id + * + * @param id of a Training Definition that would be returned + * @return specific {@link TrainingDefinition} by id + * @throws EntityNotFoundException training definition cannot be found + */ + public TrainingDefinition findById(Long id) { + return trainingDefinitionRepository.findById(id) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, id))); + } + + /** + * Find all Training Definitions by author if user is designer or all Training Definitions if user is admin. + * + * @param predicate represents a predicate (boolean-valued function) of one argument. + * @param pageable pageable parameter with information about pagination. + * @return all {@link TrainingDefinition}s + */ + public Page<TrainingDefinition> findAll(Predicate predicate, Pageable pageable) { + return trainingDefinitionRepository.findAll(predicate, pageable); + } + + /** + * Find all page. + * + * @param predicate the predicate + * @param pageable the pageable + * @param loggedInUserId the logged in user id + * @return the page + */ + public Page<TrainingDefinition> findAll(Predicate predicate, Pageable pageable, Long loggedInUserId) { + return trainingDefinitionRepository.findAll(predicate, pageable, loggedInUserId); + } + + /** + * Finds all Training Definitions accessible to users with the role of organizer. + * + * @param state represents a state of training definition if it is released or unreleased. + * @param pageable pageable parameter with information about pagination. + * @return all Training Definitions for organizers + */ + public Page<TrainingDefinition> findAllForOrganizers(TDState state, Pageable pageable) { + return trainingDefinitionRepository.findAllForOrganizers(state, pageable); + } + + /** + * Find all for designers and organizers unreleased page. + * + * @param loggedInUserId the logged in user id + * @param pageable the pageable + * @return the page + */ + public Page<TrainingDefinition> findAllForDesigners(Long loggedInUserId, Pageable pageable) { + return trainingDefinitionRepository.findAllForDesigners(loggedInUserId, pageable); + } + + /** + * creates new training definition + * + * @param trainingDefinition to be created + * @return new {@link TrainingDefinition} + */ + public TrainingDefinition create(TrainingDefinition trainingDefinition) { + addLoggedInUserToTrainingDefinitionAsAuthor(trainingDefinition); + LOG.info("Training definition with id: {} created.", trainingDefinition.getId()); + return trainingDefinitionRepository.save(trainingDefinition); + } + + /** + * Updates given Training Definition + * + * @param trainingDefinitionToUpdate to be updated + * @throws EntityNotFoundException training definition or one of the phases is not found. + * @throws EntityConflictException released or archived training definition cannot be modified. + */ + public void update(TrainingDefinition trainingDefinitionToUpdate) { + TrainingDefinition trainingDefinition = findById(trainingDefinitionToUpdate.getId()); + checkIfCanBeUpdated(trainingDefinition); + addLoggedInUserToTrainingDefinitionAsAuthor(trainingDefinitionToUpdate); + trainingDefinitionToUpdate.setEstimatedDuration(trainingDefinition.getEstimatedDuration()); + trainingDefinitionRepository.save(trainingDefinitionToUpdate); + LOG.info("Training definition with id: {} updated.", trainingDefinitionToUpdate.getId()); + } + + /** + * Creates new training definition by cloning existing one + * + * @param id of definition to be cloned + * @param title the title of the new cloned definition + * @return cloned {@link TrainingDefinition} + * @throws EntityNotFoundException training definition not found. + * @throws EntityConflictException cannot clone unreleased training definition. + */ + public TrainingDefinition clone(Long id, String title) { + TrainingDefinition trainingDefinition = findById(id); + TrainingDefinition clonedTrainingDefinition = objectMapper.convertValue(trainingDefinition, TrainingDefinition.class); + clonedTrainingDefinition.setId(null); + + clonedTrainingDefinition.setTitle(title); + clonedTrainingDefinition.setState(TDState.UNRELEASED); + clonedTrainingDefinition.setAuthors(new HashSet<>()); + + addLoggedInUserToTrainingDefinitionAsAuthor(clonedTrainingDefinition); + clonedTrainingDefinition = trainingDefinitionRepository.save(clonedTrainingDefinition); + clonePhasesFromTrainingDefinition(trainingDefinition.getId(), clonedTrainingDefinition); + + LOG.info("Training definition with id: {} cloned.", trainingDefinition.getId()); + return clonedTrainingDefinition; + } + + /** + * Deletes specific training definition based on id + * + * @param definitionId of definition to be deleted + * @throws EntityNotFoundException training definition or phase is not found. + * @throws EntityConflictException released training definition cannot be deleted. + */ + public void delete(Long definitionId) { + TrainingDefinition definition = findById(definitionId); + if (definition.getState().equals(TDState.RELEASED)) + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId, + "Cannot delete released training definition.")); + if (trainingInstanceRepository.existsAnyForTrainingDefinition(definitionId)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId, + "Cannot delete training definition with already created training instance. " + + "Remove training instance/s before deleting training definition.")); + } + List<AbstractPhase> abstractPhases = abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(definitionId); + abstractPhases.forEach(this::deletePhase); + trainingDefinitionRepository.delete(definition); + } + + + /** + * Finds all phases from single definition + * + * @param definitionId of definition + * @return list of {@link AbstractPhase} associated with training definition + */ + public List<AbstractPhase> findAllPhasesFromDefinition(Long definitionId) { + return abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(definitionId); + } + + /** + * Finds specific phase by id with associated training definition + * + * @param phaseId - id of wanted phase + * @return wanted {@link AbstractPhase} + * @throws EntityNotFoundException phase is not found. + */ + public AbstractPhase findPhaseByIdWithDefinition(Long phaseId) { + return abstractPhaseRepository.findByIdWithDefinition(phaseId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND))); + } + + /** + * Finds specific phase by id + * + * @param phaseId - id of wanted phase + * @return wanted {@link AbstractPhase} + * @throws EntityNotFoundException phase is not found. + */ + private AbstractPhase findPhaseById(Long phaseId) { + return abstractPhaseRepository.findById(phaseId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "id", phaseId.getClass(), phaseId, PHASE_NOT_FOUND))); + } + + /** + * Find all training instances associated with training definition by id. + * + * @param id the id of training definition + * @return the list of all {@link TrainingInstance}s associated with wanted {@link TrainingDefinition} + */ + public List<TrainingInstance> findAllTrainingInstancesByTrainingDefinitionId(Long id) { + return trainingInstanceRepository.findAllByTrainingDefinitionId(id); + } + + /** + * Switch development state of definition from unreleased to released, or from released to archived or back to unreleased. + * + * @param definitionId - id of training definition + * @param state - new state of training definition + */ + public void switchState(Long definitionId, TDState state) { + TrainingDefinition trainingDefinition = findById(definitionId); + if (trainingDefinition.getState().name().equals(state.name())) { + return; + } + switch (trainingDefinition.getState()) { + case UNRELEASED: + if (state.equals(TDState.RELEASED)) + trainingDefinition.setState(TDState.RELEASED); + else + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId, + "Cannot switch from" + trainingDefinition.getState() + " to " + state)); + break; + case RELEASED: + if (state.equals(TDState.ARCHIVED)) + trainingDefinition.setState(TDState.ARCHIVED); + else if (state.equals(TDState.UNRELEASED)) { + if (trainingInstanceRepository.existsAnyForTrainingDefinition(definitionId)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId, + "Cannot update training definition with already created training instance(s). " + + "Remove training instance(s) before changing the state from released to unreleased training definition.")); + } + trainingDefinition.setState((TDState.UNRELEASED)); + } + break; + default: + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", definitionId.getClass(), definitionId, + "Cannot switch from " + trainingDefinition.getState() + " to " + state)); + } + trainingDefinition.setLastEdited(getCurrentTimeInUTC()); + } + + private boolean existPhaseInDefinition(TrainingDefinition trainingDefinition, Long phaseId) { + return abstractPhaseRepository.findPhaseInDefinition(trainingDefinition.getId(), phaseId) + .isPresent(); + } + + private void clonePhasesFromTrainingDefinition(Long trainingDefinitionId, TrainingDefinition clonedTrainingDefinition) { + List<AbstractPhase> phases = abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(trainingDefinitionId); + if (phases == null || phases.isEmpty()) { + return; + } + phases.forEach(phase -> { + if (phase instanceof QuestionnairePhase) { + cloneQuestionnairePhase((QuestionnairePhase) phase, clonedTrainingDefinition); + } + if (phase instanceof InfoPhase) { + cloneInfoPhase((InfoPhase) phase, clonedTrainingDefinition); + } + if (phase instanceof TrainingPhase) { + cloneTrainingPhase((TrainingPhase) phase, clonedTrainingDefinition); + } + }); + } + + private void cloneInfoPhase(InfoPhase infoPhase, TrainingDefinition trainingDefinition) { + InfoPhase clonedInfoPhase = objectMapper.convertValue(infoPhase, InfoPhase.class); + clonedInfoPhase.setId(null); + clonedInfoPhase.setTrainingDefinition(trainingDefinition); + infoPhaseRepository.save(clonedInfoPhase); + } + + private void cloneQuestionnairePhase(QuestionnairePhase questionnairePhase, TrainingDefinition trainingDefinition) { + QuestionnairePhase clonedQuestionnairePhase = objectMapper.convertValue(questionnairePhase, QuestionnairePhase.class); + for (Question question: clonedQuestionnairePhase.getQuestions()) { + question.setQuestionnairePhase(clonedQuestionnairePhase); + } + QuestionPhaseRelation questionPhaseRelation = new QuestionPhaseRelation(); + clonedQuestionnairePhase.setId(null); + clonedQuestionnairePhase.setTrainingDefinition(trainingDefinition); + questionnairePhaseRepository.save(clonedQuestionnairePhase); + } + + private void cloneTrainingPhase(TrainingPhase trainingPhase, TrainingDefinition trainingDefinition) { + TrainingPhase clonedTrainingPhase = objectMapper.convertValue(trainingPhase, TrainingPhase.class); + clonedTrainingPhase.setId(null); + clonedTrainingPhase.setDecisionMatrix(trainingPhase.getDecisionMatrix() + .stream() + .map(matrix -> objectMapper.convertValue(matrix, DecisionMatrixRow.class)) + .collect(Collectors.toList())); + clonedTrainingPhase.setTasks(trainingPhase.getTasks() + .stream() + .map(task -> cloneTask(task, clonedTrainingPhase)) + .collect(Collectors.toList())); + clonedTrainingPhase.setTrainingDefinition(trainingDefinition); + trainingPhaseRepository.save(clonedTrainingPhase); + } + + private Task cloneTask(Task originalTask, TrainingPhase trainingPhase) { + Task clonedTask = objectMapper.convertValue(originalTask, Task.class); + clonedTask.setTrainingPhase(trainingPhase); + return clonedTask; + } + + private void checkIfCanBeUpdated(TrainingDefinition trainingDefinition) { + if (!trainingDefinition.getState().equals(TDState.UNRELEASED)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + ARCHIVED_OR_RELEASED)); + } + if (trainingInstanceRepository.existsAnyForTrainingDefinition(trainingDefinition.getId())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingDefinition.class, "id", trainingDefinition.getId().getClass(), trainingDefinition.getId(), + "Cannot update training definition with already created training instance. " + + "Remove training instance/s before updating training definition.")); + } + } + + private void deletePhase(AbstractPhase abstractPhase) { + if (abstractPhase instanceof QuestionnairePhase) { + questionnairePhaseRepository.delete((QuestionnairePhase) abstractPhase); + } else if (abstractPhase instanceof InfoPhase) { + infoPhaseRepository.delete((InfoPhase) abstractPhase); + } else { + trainingPhaseRepository.delete((TrainingPhase) abstractPhase); + } + } + + private LocalDateTime getCurrentTimeInUTC() { + return LocalDateTime.now(Clock.systemUTC()); + } + + private void addLoggedInUserToTrainingDefinitionAsAuthor(TrainingDefinition trainingDefinition) { + Optional<UserRef> user = userRefRepository.findUserByUserRefId(securityService.getUserRefIdFromUserAndGroup()); + if (user.isPresent()) { + trainingDefinition.addAuthor(user.get()); + } else { + UserRef newUser = securityService.createUserRefEntityByInfoFromUserAndGroup(); + trainingDefinition.addAuthor(newUser); + } + trainingDefinition.setLastEdited(getCurrentTimeInUTC()); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingInstanceService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingInstanceService.java new file mode 100644 index 0000000000000000000000000000000000000000..2fe455f074ee4168dfc643a580f4970e0dc21ede --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingInstanceService.java @@ -0,0 +1,305 @@ +package cz.muni.ics.kypo.training.adaptive.service.training; + +import com.querydsl.core.types.Predicate; +import cz.muni.ics.kypo.training.adaptive.domain.AccessToken; +import cz.muni.ics.kypo.training.adaptive.domain.UserRef; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun; +import cz.muni.ics.kypo.training.adaptive.dto.responses.LockedPoolInfo; +import cz.muni.ics.kypo.training.adaptive.dto.responses.PoolInfoDTO; +import cz.muni.ics.kypo.training.adaptive.exceptions.*; +import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.AccessTokenRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository; +import cz.muni.ics.kypo.training.adaptive.service.api.SandboxServiceApi; +import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Optional; +import java.util.Random; +import java.util.Set; + +/** + * The type Training instance service. + */ +@Service +public class TrainingInstanceService { + + private static final Logger LOG = LoggerFactory.getLogger(TrainingInstanceService.class); + + private TrainingInstanceRepository trainingInstanceRepository; + private TrainingRunRepository trainingRunRepository; + private AccessTokenRepository accessTokenRepository; + private UserRefRepository organizerRefRepository; + private SecurityService securityService; + private final Random random = new Random(); + + /** + * Instantiates a new Training instance service. + * + * @param trainingInstanceRepository the training instance repository + * @param accessTokenRepository the access token repository + * @param trainingRunRepository the training run repository + * @param organizerRefRepository the organizer ref repository + * @param securityService the security service + */ + @Autowired + + public TrainingInstanceService(TrainingInstanceRepository trainingInstanceRepository, + AccessTokenRepository accessTokenRepository, + TrainingRunRepository trainingRunRepository, + UserRefRepository organizerRefRepository, + SecurityService securityService) { + this.trainingInstanceRepository = trainingInstanceRepository; + this.trainingRunRepository = trainingRunRepository; + this.accessTokenRepository = accessTokenRepository; + this.organizerRefRepository = organizerRefRepository; + this.securityService = securityService; + } + + /** + * Finds basic info about Training Instance by id + * + * @param instanceId of a Training Instance that would be returned + * @return specific {@link TrainingInstance} by id + * @throws EntityNotFoundException training instance is not found. + */ + public TrainingInstance findById(Long instanceId) { + return trainingInstanceRepository.findById(instanceId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", instanceId.getClass(), instanceId))); + } + + /** + * Find specific Training instance by id including its associated Training definition. + * + * @param instanceId the instance id + * @return the {@link TrainingInstance} + */ + public TrainingInstance findByIdIncludingDefinition(Long instanceId) { + return trainingInstanceRepository.findByIdIncludingDefinition(instanceId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "id", instanceId.getClass(), instanceId))); + } + + /** + * Find all Training Instances. + * + * @param predicate represents a predicate (boolean-valued function) of one argument. + * @param pageable pageable parameter with information about pagination. + * @return all {@link TrainingInstance}s + */ + public Page<TrainingInstance> findAll(Predicate predicate, Pageable pageable) { + return trainingInstanceRepository.findAll(predicate, pageable); + } + + /** + * Find all training instances based on the logged in user. + * + * @param predicate the predicate + * @param pageable the pageable + * @param loggedInUserId the logged in user id + * @return the page + */ + public Page<TrainingInstance> findAll(Predicate predicate, Pageable pageable, Long loggedInUserId) { + return trainingInstanceRepository.findAll(predicate, pageable, loggedInUserId); + } + + /** + * Creates new training instance + * + * @param trainingInstance to be created + * @return created {@link TrainingInstance} + */ + public TrainingInstance create(TrainingInstance trainingInstance) { + trainingInstance.setAccessToken(generateAccessToken(trainingInstance.getAccessToken())); + if (trainingInstance.getStartTime().isAfter(trainingInstance.getEndTime())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstance.getId().getClass(), trainingInstance.getId(), + "End time must be later than start time.")); + } + addLoggedInUserAsOrganizerToTrainingInstance(trainingInstance); + return trainingInstanceRepository.save(trainingInstance); + } + + /** + * updates training instance + * + * @param trainingInstanceToUpdate to be updated + * @return new access token if it was changed + * @throws EntityNotFoundException training instance is not found. + * @throws EntityConflictException cannot be updated for some reason. + */ + public String update(TrainingInstance trainingInstanceToUpdate) { + validateStartAndEndTime(trainingInstanceToUpdate); + TrainingInstance trainingInstance = findById(trainingInstanceToUpdate.getId()); + //add original organizers and poolId to update + trainingInstanceToUpdate.setOrganizers(new HashSet<>(trainingInstance.getOrganizers())); + addLoggedInUserAsOrganizerToTrainingInstance(trainingInstanceToUpdate); + trainingInstanceToUpdate.setPoolId(trainingInstance.getPoolId()); + //check if TI is running, true - only title can be changed, false - any field can be changed + if (LocalDateTime.now(Clock.systemUTC()).isAfter(trainingInstance.getStartTime())) { + this.checkChangedFieldsOfRunningTrainingInstance(trainingInstanceToUpdate, trainingInstance); + } else { + //check if new access token should be generated, if not original is kept + if (shouldGenerateNewToken(trainingInstance.getAccessToken(), trainingInstanceToUpdate.getAccessToken())) { + trainingInstanceToUpdate.setAccessToken(generateAccessToken(trainingInstanceToUpdate.getAccessToken())); + } else { + trainingInstanceToUpdate.setAccessToken(trainingInstance.getAccessToken()); + } + } + trainingInstanceRepository.save(trainingInstanceToUpdate); + return trainingInstanceToUpdate.getAccessToken(); + } + + private void validateStartAndEndTime(TrainingInstance trainingInstance) { + if (trainingInstance.getStartTime().isAfter(trainingInstance.getEndTime())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", + trainingInstance.getId().getClass(), trainingInstance.getId(), + "End time must be later than start time.")); + } + } + + private void checkChangedFieldsOfRunningTrainingInstance(TrainingInstance trainingInstanceToUpdate, TrainingInstance currentTrainingInstance) { + if (!currentTrainingInstance.getStartTime().equals(trainingInstanceToUpdate.getStartTime())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", Long.class, trainingInstanceToUpdate.getId(), + "The start time of the running training instance cannot be changed. Only title can be updated.")); + } else if (!currentTrainingInstance.getEndTime().equals(trainingInstanceToUpdate.getEndTime())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", Long.class, trainingInstanceToUpdate.getId(), + "The end time of the running training instance cannot be changed. Only title can be updated.")); + } else if (!currentTrainingInstance.getAccessToken().equals(trainingInstanceToUpdate.getAccessToken())) { + throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", Long.class, trainingInstanceToUpdate.getId(), + "The access token of the running training instance cannot be changed. Only title can be updated.")); + } + } + + private boolean shouldGenerateNewToken(String originalToken, String newToken) { + //new token should not be generated if token in update equals original token or if token in update equals original token without PIN + String tokenWithoutPin = originalToken.substring(0, originalToken.length() - 5); + return !(newToken.equals(tokenWithoutPin) || originalToken.equals(newToken)); + } + + private String generateAccessToken(String accessToken) { + String newPass = ""; + boolean generated = false; + while (!generated) { + int firstNumber = this.random.nextInt(5) + 5; + String pin = firstNumber + RandomStringUtils.random(3, false, true); + newPass = accessToken + "-" + pin; + Optional<AccessToken> pW = accessTokenRepository.findOneByAccessToken(newPass); + if (!pW.isPresent()) { + generated = true; + } + } + AccessToken newTokenInstance = new AccessToken(); + newTokenInstance.setAccessToken(newPass); + accessTokenRepository.saveAndFlush(newTokenInstance); + return newPass; + } + + private void addLoggedInUserAsOrganizerToTrainingInstance(TrainingInstance trainingInstance) { + Optional<UserRef> authorOfTrainingInstance = organizerRefRepository.findUserByUserRefId(securityService.getUserRefIdFromUserAndGroup()); + if (authorOfTrainingInstance.isPresent()) { + trainingInstance.addOrganizer(authorOfTrainingInstance.get()); + } else { + UserRef userRef = securityService.createUserRefEntityByInfoFromUserAndGroup(); + trainingInstance.addOrganizer(organizerRefRepository.save(userRef)); + } + } + + /** + * deletes training instance + * + * @param trainingInstance the training instance to be deleted. + * @throws EntityNotFoundException training instance is not found. + * @throws EntityConflictException cannot be deleted for some reason. + */ + public void delete(TrainingInstance trainingInstance) { + trainingInstanceRepository.delete(trainingInstance); + LOG.debug("Training instance with id: {} deleted.", trainingInstance.getId()); + } + + /** + * deletes training instance + * + * @param id the training instance to be deleted. + * @throws EntityNotFoundException training instance is not found. + * @throws EntityConflictException cannot be deleted for some reason. + */ + public void deleteById(Long id) { + trainingInstanceRepository.deleteById(id); + LOG.debug("Training instance with id: {} deleted.", id); + } + + /** + * Update training instance pool training instance. + * + * @param trainingInstance the training instance + * @return the training instance + */ + public TrainingInstance updateTrainingInstancePool(TrainingInstance trainingInstance) { + return trainingInstanceRepository.saveAndFlush(trainingInstance); + } + + /** + * Finds all Training Runs of specific Training Instance. + * + * @param instanceId id of Training Instance whose Training Runs would be returned. + * @param isActive if isActive attribute is True, only active runs are returned + * @param pageable pageable parameter with information about pagination. + * @return {@link TrainingRun}s of specific {@link TrainingInstance} + */ + public Page<TrainingRun> findTrainingRunsByTrainingInstance(Long instanceId, Boolean isActive, Pageable pageable) { + // check if instance exists + this.findById(instanceId); + if (isActive == null) { + return trainingRunRepository.findAllByTrainingInstanceId(instanceId, pageable); + } else if (isActive) { + return trainingRunRepository.findAllActiveByTrainingInstanceId(instanceId, pageable); + } else { + return trainingRunRepository.findAllInactiveByTrainingInstanceId(instanceId, pageable); + } + } + + /** + * Find UserRefs by userRefId + * + * @param usersRefId of wanted UserRefs + * @return {@link UserRef}s with corresponding userRefIds + */ + public Set<UserRef> findUserRefsByUserRefIds(Set<Long> usersRefId) { + return organizerRefRepository.findUsers(usersRefId); + } + + /** + * Check if instance is finished. + * + * @param trainingInstanceId the training instance id + * @return true if instance is finished, false if not + */ + public boolean checkIfInstanceIsFinished(Long trainingInstanceId) { + return trainingInstanceRepository.isFinished(trainingInstanceId, LocalDateTime.now(Clock.systemUTC())); + } + + /** + * Find specific Training instance by its access token and with start time before current time and ending time after current time + * + * @param accessToken of Training instance + * @return Training instance + */ + public TrainingInstance findByStartTimeAfterAndEndTimeBeforeAndAccessToken(String accessToken) { + return trainingInstanceRepository.findByStartTimeAfterAndEndTimeBeforeAndAccessToken(LocalDateTime.now(Clock.systemUTC()), accessToken) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "accessToken", accessToken.getClass(), accessToken, + "There is no active game session matching access token."))); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java new file mode 100644 index 0000000000000000000000000000000000000000..49c620c1bb8102972c9fb34fb957e833f5a99bc4 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/TrainingRunService.java @@ -0,0 +1,530 @@ +package cz.muni.ics.kypo.training.adaptive.service.training; + +import com.querydsl.core.types.Predicate; +import cz.muni.ics.kypo.training.adaptive.annotations.transactions.TransactionalWO; +import cz.muni.ics.kypo.training.adaptive.domain.TRAcquisitionLock; +import cz.muni.ics.kypo.training.adaptive.domain.UserRef; +import cz.muni.ics.kypo.training.adaptive.domain.enums.TRState; +import cz.muni.ics.kypo.training.adaptive.domain.phases.*; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingDefinition; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingInstance; +import cz.muni.ics.kypo.training.adaptive.domain.training.TrainingRun; +import cz.muni.ics.kypo.training.adaptive.dto.responses.SandboxInfo; +import cz.muni.ics.kypo.training.adaptive.exceptions.*; +import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository; +import cz.muni.ics.kypo.training.adaptive.repository.phases.AbstractPhaseRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TRAcquisitionLockRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingInstanceRepository; +import cz.muni.ics.kypo.training.adaptive.repository.training.TrainingRunRepository; +import cz.muni.ics.kypo.training.adaptive.service.api.ElasticsearchServiceApi; +import cz.muni.ics.kypo.training.adaptive.service.api.SandboxServiceApi; +import cz.muni.ics.kypo.training.adaptive.service.audit.AuditEventsService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.web.reactive.function.client.WebClient; + +import java.time.Clock; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +/** + * The type Training run service. + */ +@Service +public class TrainingRunService { + + private static final Logger LOG = LoggerFactory.getLogger(TrainingRunService.class); + + private SandboxServiceApi sandboxServiceApi; + private TrainingRunRepository trainingRunRepository; + private AbstractPhaseRepository abstractPhaseRepository; + private TrainingInstanceRepository trainingInstanceRepository; + private UserRefRepository participantRefRepository; + private AuditEventsService auditEventsService; + private ElasticsearchServiceApi elasticsearchServiceApi; + private SecurityService securityService; + private TRAcquisitionLockRepository trAcquisitionLockRepository; + + /** + * Instantiates a new Training run service. + * + * @param trainingRunRepository the training run repository + * @param abstractPhaseRepository the abstract phase repository + * @param trainingInstanceRepository the training instance repository + * @param participantRefRepository the participant ref repository + * @param auditEventsService the audit events service + * @param securityService the security service + * @param trAcquisitionLockRepository the tr acquisition lock repository + */ + @Autowired + public TrainingRunService(SandboxServiceApi sandboxServiceApi, + TrainingRunRepository trainingRunRepository, + AbstractPhaseRepository abstractPhaseRepository, + TrainingInstanceRepository trainingInstanceRepository, + UserRefRepository participantRefRepository, + AuditEventsService auditEventsService, + ElasticsearchServiceApi elasticsearchServiceApi, + SecurityService securityService, + TRAcquisitionLockRepository trAcquisitionLockRepository) { + this.sandboxServiceApi = sandboxServiceApi; + this.trainingRunRepository = trainingRunRepository; + this.abstractPhaseRepository = abstractPhaseRepository; + this.trainingInstanceRepository = trainingInstanceRepository; + this.participantRefRepository = participantRefRepository; + this.auditEventsService = auditEventsService; + this.elasticsearchServiceApi = elasticsearchServiceApi; + this.securityService = securityService; + this.trAcquisitionLockRepository = trAcquisitionLockRepository; + } + + /** + * Finds specific Training Run by id. + * + * @param runId of a Training Run that would be returned + * @return specific {@link TrainingRun} by id + * @throws EntityNotFoundException training run is not found. + */ + public TrainingRun findById(Long runId) { + return trainingRunRepository.findById(runId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingRun.class, "id", runId.getClass(), runId))); + } + + /** + * /** + * Finds specific Training Run by id including current phase. + * + * @param runId of a Training Run with phase that would be returned + * @return specific {@link TrainingRun} by id + * @throws EntityNotFoundException training run is not found. + */ + public TrainingRun findByIdWithPhase(Long runId) { + return trainingRunRepository.findByIdWithPhase(runId).orElseThrow(() -> new EntityNotFoundException( + new EntityErrorDetail(TrainingRun.class, "id", runId.getClass(), runId))); + } + + /** + * Find all Training Runs. + * + * @param predicate specifies query to the database. + * @param pageable pageable parameter with information about pagination. + * @return all {@link TrainingRun}s + */ + public Page<TrainingRun> findAll(Predicate predicate, Pageable pageable) { + return trainingRunRepository.findAll(predicate, pageable); + } + + /** + * Delete selected training run. + * + * @param trainingRunId training run to delete + * @param forceDelete delete training run in a force manner + */ + public void deleteTrainingRun(Long trainingRunId, boolean forceDelete) { + TrainingRun trainingRun = findById(trainingRunId); + if (!forceDelete && trainingRun.getState().equals(TRState.RUNNING)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRun.getId().getClass(), trainingRun.getId(), + "Cannot delete training run that is running. Consider force delete.")); + } + elasticsearchServiceApi.deleteEventsFromTrainingRun(trainingRun.getTrainingInstance().getId(), trainingRunId); + trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(trainingRun.getParticipantRef().getUserRefId(), trainingRun.getTrainingInstance().getId()); + trainingRunRepository.delete(trainingRun); + } + + /** + * Checks whether any trainin runs exists for particular training instance + * + * @param trainingInstanceId the training instance id + * @return boolean boolean + */ + public boolean existsAnyForTrainingInstance(Long trainingInstanceId) { + return trainingRunRepository.existsAnyForTrainingInstance(trainingInstanceId); + } + + + /** + * Finds all Training Runs of logged in user. + * + * @param pageable pageable parameter with information about pagination. + * @return {@link TrainingRun}s of logged in user. + */ + public Page<TrainingRun> findAllByParticipantRefUserRefId(Pageable pageable) { + return trainingRunRepository.findAllByParticipantRefId(securityService.getUserRefIdFromUserAndGroup(), pageable); + } + + /** + * Finds all Training Runs of particular training instance. + * + * @param trainingInstanceId the training instance id + * @return the set + */ + public Set<TrainingRun> findAllByTrainingInstanceId(Long trainingInstanceId) { + return trainingRunRepository.findAllByTrainingInstanceId(trainingInstanceId); + } + + /** + * Gets next phase of given Training Run and set new current phase. + * + * @param runId id of Training Run whose next phase should be returned. + * @return {@link AbstractPhase} + * @throws EntityNotFoundException training run or phase is not found. + */ + public AbstractPhase getNextPhase(Long runId) { + TrainingRun trainingRun = findByIdWithPhase(runId); + int currentPhaseOrder = trainingRun.getCurrentPhase().getOrder(); + int maxPhaseOrder = abstractPhaseRepository.getCurrentMaxOrder(trainingRun.getCurrentPhase().getTrainingDefinition().getId()); + if (!trainingRun.isLevelAnswered()) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", runId.getClass(), runId, + "You need to answer the phase to move to the next phase.")); + } + if (currentPhaseOrder == maxPhaseOrder) { + throw new EntityNotFoundException(new EntityErrorDetail(AbstractPhase.class, "There is no next phase for current training run (ID: " + runId + ").")); + } + List<AbstractPhase> phases = abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(trainingRun.getCurrentPhase().getTrainingDefinition().getId()); + int nextPhaseIndex = phases.indexOf(trainingRun.getCurrentPhase()) + 1; + AbstractPhase abstractPhase = phases.get(nextPhaseIndex); + if (trainingRun.getCurrentPhase() instanceof InfoPhase) { + auditEventsService.auditPhaseCompletedAction(trainingRun); + } + trainingRun.setCurrentPhase(abstractPhase); + trainingRun.setIncorrectFlagCount(0); + trainingRunRepository.save(trainingRun); + auditEventsService.auditPhaseStartedAction(trainingRun); + + return abstractPhase; + } + + /** + * Finds all Training Runs of specific Training Definition of logged in user. + * + * @param definitionId id of Training Definition + * @param pageable pageable parameter with information about pagination. + * @return {@link TrainingRun}s of specific Training Definition of logged in user + */ + public Page<TrainingRun> findAllByTrainingDefinitionAndParticipant(Long definitionId, Pageable pageable) { + return trainingRunRepository.findAllByTrainingDefinitionIdAndParticipantUserRefId(definitionId, securityService.getUserRefIdFromUserAndGroup(), pageable); + } + + /** + * Finds all Training Runs of specific training definition. + * + * @param definitionId id of Training Definition whose Training Runs would be returned. + * @param pageable pageable parameter with information about pagination. + * @return {@link TrainingRun}s of specific Training Definition + */ + public Page<TrainingRun> findAllByTrainingDefinition(Long definitionId, Pageable pageable) { + return trainingRunRepository.findAllByTrainingDefinitionId(definitionId, pageable); + } + + /** + * Gets list of all phases in Training Definition. + * + * @param definitionId must be id of first phase of some Training Definition. + * @return List of {@link AbstractPhase}s + * @throws EntityNotFoundException one of the phases is not found. + */ + public List<AbstractPhase> getPhases(Long definitionId) { + return abstractPhaseRepository.findAllByTrainingDefinitionIdOrderByOrder(definitionId); + } + + /** + * Access training run based on given accessToken. + * + * @param trainingInstance the training instance + * @param participantRefId the participant ref id + * @return accessed {@link TrainingRun} + * @throws EntityNotFoundException no active training instance for given access token, no starting phase in training definition. + * @throws EntityConflictException pool of sandboxes is not created for training instance. + */ + public TrainingRun createTrainingRun(TrainingInstance trainingInstance, Long participantRefId) { + AbstractPhase initialPhase = findFirstPhaseForTrainingRun(trainingInstance.getTrainingDefinition().getId()); + TrainingRun trainingRun = getNewTrainingRun(initialPhase, trainingInstance, LocalDateTime.now(Clock.systemUTC()), trainingInstance.getEndTime(), participantRefId); + return trainingRunRepository.save(trainingRun); + } + + /** + * Find running training run of user optional. + * + * @param accessToken the access token + * @param participantRefId the participant ref id + * @return the optional + */ + public Optional<TrainingRun> findRunningTrainingRunOfUser(String accessToken, Long participantRefId) { + return trainingRunRepository.findRunningTrainingRunOfUser(accessToken, participantRefId); + } + + /** + * Gets training instance for particular access token. + * + * @param accessToken the access token + * @return the training instance for particular access token + */ + public TrainingInstance getTrainingInstanceForParticularAccessToken(String accessToken) { + TrainingInstance trainingInstance = trainingInstanceRepository.findByStartTimeAfterAndEndTimeBeforeAndAccessToken(LocalDateTime.now(Clock.systemUTC()), accessToken) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(TrainingInstance.class, "accessToken", accessToken.getClass(), accessToken, + "There is no active game session matching access token."))); + if (trainingInstance.getPoolId() == null) { + throw new EntityConflictException(new EntityErrorDetail(TrainingInstance.class, "id", trainingInstance.getId().getClass(), trainingInstance.getId(), + "At first organizer must allocate sandboxes for training instance.")); + } + return trainingInstance; + } + + /** + * Tr acquisition lock to prevent many requests from the same user. This method is called in a new transaction that means that the existing one is suspended. + * + * @param participantRefId the participant ref id + * @param trainingInstanceId the training instance id + * @param accessToken the access token + */ + @TransactionalWO(propagation = Propagation.REQUIRES_NEW) + public void trAcquisitionLockToPreventManyRequestsFromSameUser(Long participantRefId, Long trainingInstanceId, String accessToken) { + try { + trAcquisitionLockRepository.saveAndFlush(new TRAcquisitionLock(participantRefId, trainingInstanceId, LocalDateTime.now(Clock.systemUTC()))); + } catch (DataIntegrityViolationException ex) { + throw new TooManyRequestsException(new EntityErrorDetail(TrainingInstance.class, "accessToken", accessToken.getClass(), accessToken, + "Training run has been already accessed and cannot be created again. Please resume Training Run")); + } + } + + @TransactionalWO(propagation = Propagation.REQUIRES_NEW) + public void deleteTrAcquisitionLockToPreventManyRequestsFromSameUser(Long participantRefId, Long trainingInstanceId) { + trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(participantRefId, trainingInstanceId); + } + + private AbstractPhase findFirstPhaseForTrainingRun(Long trainingDefinitionId) { + return abstractPhaseRepository.findFirstPhaseOfTrainingDefinition(trainingDefinitionId) + .orElseThrow( () -> new EntityNotFoundException(new EntityErrorDetail(TrainingDefinition.class, "id", Long.class, trainingDefinitionId, + "No starting phase available for this training definition."))); + } + + private TrainingRun getNewTrainingRun(AbstractPhase currentPhase, TrainingInstance trainingInstance, LocalDateTime startTime, LocalDateTime endTime, Long participantRefId) { + TrainingRun newTrainingRun = new TrainingRun(); + newTrainingRun.setCurrentPhase(currentPhase); + + Optional<UserRef> userRefOpt = participantRefRepository.findUserByUserRefId(participantRefId); + if (userRefOpt.isPresent()) { + newTrainingRun.setParticipantRef(userRefOpt.get()); + } else { + newTrainingRun.setParticipantRef(participantRefRepository.save(securityService.createUserRefEntityByInfoFromUserAndGroup())); + } + newTrainingRun.setAssessmentResponses("[]"); + newTrainingRun.setState(TRState.RUNNING); + newTrainingRun.setTrainingInstance(trainingInstance); + newTrainingRun.setStartTime(startTime); + newTrainingRun.setEndTime(endTime); + return newTrainingRun; + } + + /** + * Connects available sandbox with given Training run. + * + * @param trainingRun that will be connected with sandbox + * @param poolId the pool id + * @return Training run with assigned sandbox + * @throws ForbiddenException no available sandbox. + * @throws MicroserviceApiException error calling OpenStack Sandbox Service API + */ + public TrainingRun assignSandbox(TrainingRun trainingRun, long poolId) { + Long sandboxInstanceRef = this.sandboxServiceApi.getAndLockSandboxForTrainingRun(poolId); + trainingRun.setSandboxInstanceRefId(sandboxInstanceRef); + auditEventsService.auditTrainingRunStartedAction(trainingRun); + auditEventsService.auditPhaseStartedAction(trainingRun); + return trainingRunRepository.save(trainingRun); + } + + /** + * Resume previously closed training run. + * + * @param trainingRunId id of training run to be resumed. + * @return {@link TrainingRun} + * @throws EntityNotFoundException training run is not found. + */ + public TrainingRun resumeTrainingRun(Long trainingRunId) { + TrainingRun trainingRun = findByIdWithPhase(trainingRunId); + if (trainingRun.getState().equals(TRState.FINISHED) || trainingRun.getState().equals(TRState.ARCHIVED)) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId, + "Cannot resume finished training run.")); + } + if (trainingRun.getTrainingInstance().getEndTime().isBefore(LocalDateTime.now(Clock.systemUTC()))) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId, + "Cannot resume training run after end of training instance.")); + } + if (trainingRun.getTrainingInstance().getPoolId() == null) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId, + "The pool assignment of the appropriate training instance has been probably canceled. Please contact the organizer.")); + } + + if (trainingRun.getSandboxInstanceRefId() == null) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId, + "Sandbox of this training run was already deleted, you have to start new game.")); + } + auditEventsService.auditTrainingRunResumedAction(trainingRun); + return trainingRun; + } + + /** + * Check given answer of given Training Run. + * + * @param runId id of Training Run to check answer. + * @param answer string which player submit. + * @return true if answer is correct, false if answer is wrong. + * @throws EntityNotFoundException training run is not found. + * @throws BadRequestException the current phase of training run is not training phase. + */ + public boolean isCorrectAnswer(Long runId, String answer) { + TrainingRun trainingRun = findByIdWithPhase(runId); + AbstractPhase currentPhase = trainingRun.getCurrentPhase(); + if (currentPhase instanceof TrainingPhase) { + if (trainingRun.isLevelAnswered()) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", Long.class, runId, "The answer of the current phase of training run has been already corrected.")); + } + Task currentTask = getCurrentTask(trainingRun, (TrainingPhase) currentPhase); + if (currentTask.getAnswer().equals(answer)) { + trainingRun.setLevelAnswered(true); + auditEventsService.auditCorrectFlagSubmittedAction(trainingRun, answer); + auditEventsService.auditPhaseCompletedAction(trainingRun); + return true; + } else if (currentTask.getIncorrectAnswerLimit() != trainingRun.getIncorrectFlagCount()) { + //TODO add increaseIncorrectAnswerCount + trainingRun.setIncorrectFlagCount(trainingRun.getIncorrectFlagCount() + 1); + } + auditEventsService.auditWrongFlagSubmittedAction(trainingRun, answer); + return false; + } else { + throw new BadRequestException("Current phase is not training phase and does not have answer."); + } + } + + private Task getCurrentTask(TrainingRun trainingRun, TrainingPhase trainingPhase) { + return trainingPhase.getTasks() + .stream() + .takeWhile(task -> task.getOrder().equals(trainingRun.getCurrentTaskOrder())) + .findFirst() + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(Task.class, "order", Integer.class, trainingRun.getCurrentTaskOrder(), "The task in order: " + trainingRun.getCurrentTaskOrder() + + " of training run (ID: " + trainingRun.getId() + ") hasn't been found."))); + } + + /** + * Gets remaining attempts to solve current phase of training run. + * + * @param trainingRunId the training run id + * @return the remaining attempts + */ + public int getRemainingAttempts(Long trainingRunId) { + TrainingRun trainingRun = findByIdWithPhase(trainingRunId); + AbstractPhase phase = trainingRun.getCurrentPhase(); + if (phase instanceof TrainingPhase) { + if (trainingRun.isSolutionTaken()) { + return 0; + } + Task currentTask = getCurrentTask(trainingRun, (TrainingPhase) phase); + return currentTask.getIncorrectAnswerLimit() - trainingRun.getIncorrectFlagCount(); + } + throw new BadRequestException("Current phase is not training phase and does not have an answer."); + } + + /** + * Gets solution of current phase of given Training Run. + * + * @param trainingRunId id of Training Run which current phase gets solution for. + * @return solution of current phase. + * @throws EntityNotFoundException training run is not found. + * @throws BadRequestException the current phase of training run is not training phase. + */ + public String getSolution(Long trainingRunId) { + TrainingRun trainingRun = findByIdWithPhase(trainingRunId); + AbstractPhase currentPhase = trainingRun.getCurrentPhase(); + if (currentPhase instanceof TrainingPhase) { + if (!trainingRun.isSolutionTaken()) { + trainingRun.setSolutionTaken(true); + trainingRunRepository.save(trainingRun); + auditEventsService.auditSolutionDisplayedAction(trainingRun); + } + Task currentTask = getCurrentTask(trainingRun, (TrainingPhase) currentPhase); + return currentTask.getSolution(); + } else { + throw new BadRequestException("Current phase is not training phase and does not have solution."); + } + } + + /** + * Gets max phase order of phases from definition. + * + * @param definitionId id of training definition. + * @return max order of phases. + */ + public int getMaxPhaseOrder(Long definitionId) { + return abstractPhaseRepository.getCurrentMaxOrder(definitionId); + } + + /** + * Finish training run. + * + * @param trainingRunId id of training run to be finished. + * @throws EntityNotFoundException training run is not found. + */ + public void finishTrainingRun(Long trainingRunId) { + TrainingRun trainingRun = findById(trainingRunId); + int maxOrder = abstractPhaseRepository.getCurrentMaxOrder(trainingRun.getCurrentPhase().getTrainingDefinition().getId()); + if (trainingRun.getCurrentPhase().getOrder() != maxOrder) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId, + "Cannot finish training run because current phase is not last.")); + } + if (!trainingRun.isLevelAnswered()) { + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId, + "Cannot finish training run because current phase is not answered.")); + } + trainingRun.setState(TRState.FINISHED); + trainingRun.setEndTime(LocalDateTime.now(Clock.systemUTC())); + trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(trainingRun.getParticipantRef().getUserRefId(), trainingRun.getTrainingInstance().getId()); + if (trainingRun.getCurrentPhase() instanceof InfoPhase) { + auditEventsService.auditPhaseCompletedAction(trainingRun); + } + auditEventsService.auditTrainingRunEndedAction(trainingRun); + } + + /** + * Archive training run. + * + * @param trainingRunId id of training run to be archived. + * @throws EntityNotFoundException training run is not found. + */ + public void archiveTrainingRun(Long trainingRunId) { + TrainingRun trainingRun = findById(trainingRunId); + trainingRun.setState(TRState.ARCHIVED); + trainingRun.setPreviousSandboxInstanceRefId(trainingRun.getSandboxInstanceRefId()); + trainingRun.setSandboxInstanceRefId(null); + trAcquisitionLockRepository.deleteByParticipantRefIdAndTrainingInstanceId(trainingRun.getParticipantRef().getUserRefId(), trainingRun.getTrainingInstance().getId()); + trainingRunRepository.save(trainingRun); + } + + /** + * Evaluate and store responses to assessment. + * + * @param trainingRunId id of training run to be finished. + * @param responsesAsString response to assessment to be evaluated + * @throws EntityNotFoundException training run is not found. + */ + public void evaluateResponsesToQuestionnaire(Long trainingRunId, String responsesAsString) { + TrainingRun trainingRun = findByIdWithPhase(trainingRunId); + if (!(trainingRun.getCurrentPhase() instanceof QuestionnairePhase)) { + throw new BadRequestException("Current phase is not questionnaire phase and cannot be evaluated."); + } + if (trainingRun.isLevelAnswered()) + throw new EntityConflictException(new EntityErrorDetail(TrainingRun.class, "id", trainingRunId.getClass(), trainingRunId, + "Current phase of the training run has been already answered.")); + //TODO complete the evaluation + auditEventsService.auditAssessmentAnswersAction(trainingRun, responsesAsString); + auditEventsService.auditPhaseCompletedAction(trainingRun); + } +} diff --git a/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/UserService.java b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/UserService.java new file mode 100644 index 0000000000000000000000000000000000000000..3ed56879c1cce3eaafdd29868aa2694caaacd630 --- /dev/null +++ b/src/main/java/cz/muni/ics/kypo/training/adaptive/service/training/UserService.java @@ -0,0 +1,209 @@ +package cz.muni.ics.kypo.training.adaptive.service.training; + +import cz.muni.ics.kypo.training.adaptive.annotations.transactions.TransactionalWO; +import cz.muni.ics.kypo.training.adaptive.domain.UserRef; +import cz.muni.ics.kypo.training.adaptive.dto.UserRefDTO; +import cz.muni.ics.kypo.training.adaptive.dto.responses.PageResultResource; +import cz.muni.ics.kypo.training.adaptive.enums.RoleType; +import cz.muni.ics.kypo.training.adaptive.exceptions.CustomWebClientException; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityErrorDetail; +import cz.muni.ics.kypo.training.adaptive.exceptions.EntityNotFoundException; +import cz.muni.ics.kypo.training.adaptive.exceptions.MicroserviceApiException; +import cz.muni.ics.kypo.training.adaptive.repository.UserRefRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.util.UriBuilder; + +import java.util.Collections; +import java.util.Set; + +/** + * The type User service. + */ +@Service +public class UserService { + + private static final Logger LOG = LoggerFactory.getLogger(UserService.class); + + private WebClient userManagementServiceWebClient; + private UserRefRepository userRefRepository; + + /** + * Instantiates a new User service. + * + * @param userManagementServiceWebClient the rest template + * @param userRefRepository the user ref repository + */ + public UserService(@Qualifier("userManagementServiceWebClient") WebClient userManagementServiceWebClient, + UserRefRepository userRefRepository) { + this.userManagementServiceWebClient = userManagementServiceWebClient; + this.userRefRepository = userRefRepository; + } + + /** + * Finds specific User reference by login + * + * @param userRefId of wanted User reference + * @return {@link UserRef} with corresponding login + * @throws EntityNotFoundException UserRef was not found + */ + public UserRef getUserByUserRefId(Long userRefId) { + return userRefRepository.findUserByUserRefId(userRefId) + .orElseThrow(() -> new EntityNotFoundException(new EntityErrorDetail(UserRef.class, "id", userRefId.getClass(), userRefId))); + } + + /** + * Finds specific User reference by login + * + * @param id of wanted User reference + * @return {@link UserRef} with corresponding login + * @throws EntityNotFoundException UserRef was not found + */ + public UserRefDTO getUserRefDTOByUserRefId(Long id) { + try { + return userManagementServiceWebClient + .get() + .uri("/users/{id}", id) + .retrieve() + .bodyToMono(UserRefDTO.class) + .block(); + } catch (CustomWebClientException ex) { + throw new MicroserviceApiException("Error when calling user management service API to obtain info about user(ID: " + id + ").", ex.getApiSubError()); + } + } + + /** + * Gets users with given user ref ids. + * + * @param userRefIds the user ref ids + * @param pageable pageable parameter with information about pagination. + * @param givenName optional parameter used for filtration + * @param familyName optional parameter used for filtration + * @return the users with given user ref ids + */ + public PageResultResource<UserRefDTO> getUsersRefDTOByGivenUserIds(Set<Long> userRefIds, Pageable pageable, String givenName, String familyName) { + if (userRefIds.isEmpty()) { + return new PageResultResource<>(Collections.emptyList(), new PageResultResource.Pagination(0, 0, pageable.getPageSize(), 0, 0)); + } + try { + return userManagementServiceWebClient + .get() + .uri(uriBuilder -> { + uriBuilder + .path("/users/ids") + .queryParam("ids", StringUtils.collectionToDelimitedString(userRefIds, ",")); + this.setCommonParams(givenName, familyName, pageable, uriBuilder); + return uriBuilder.build(); + } + ) + .retrieve() + .bodyToMono(new ParameterizedTypeReference<PageResultResource<UserRefDTO>>() {}) + .block(); + } catch (CustomWebClientException ex) { + throw new MicroserviceApiException("Error when calling user management service API to obtain users by IDs: " + userRefIds + ".", ex.getApiSubError()); + } + } + + /** + * Finds all logins of users that have role of designer + * + * @param roleType the role type + * @param pageable pageable parameter with information about pagination. + * @param givenName optional parameter used for filtration + * @param familyName optional parameter used for filtration + * @return list of users with given role + */ + public PageResultResource<UserRefDTO> getUsersByGivenRole(RoleType roleType, Pageable pageable, String givenName, String familyName) { + try { + return userManagementServiceWebClient + .get() + .uri(uriBuilder -> { + uriBuilder + .path("/roles/users") + .queryParam("roleType", roleType.name()); + this.setCommonParams(givenName, familyName, pageable, uriBuilder); + return uriBuilder.build(); + } + ) + .retrieve() + .bodyToMono(new ParameterizedTypeReference<PageResultResource<UserRefDTO>>() {}) + .block(); + } catch (CustomWebClientException ex) { + throw new MicroserviceApiException("Error when calling user management service API to obtain users with role " + roleType.name() + ".", ex.getApiSubError()); + } + } + + /** + * Finds all logins of users that have role of designer + * + * @param roleType the role type + * @param userRefIds ids of the users who should be excluded from the result set. + * @param pageable the pageable + * @param givenName optional parameter used for filtration + * @param familyName optional parameter used for filtration + * @return list of users with given role + */ + public PageResultResource<UserRefDTO> getUsersByGivenRoleAndNotWithGivenIds(RoleType roleType, Set<Long> userRefIds, Pageable pageable, String givenName, String familyName) { + try { + return userManagementServiceWebClient + .get() + .uri(uriBuilder -> { + uriBuilder + .path("/roles/users-not-with-ids") + .queryParam("roleType", roleType.name()) + .queryParam("ids", StringUtils.collectionToDelimitedString(userRefIds, ",")); + this.setCommonParams(givenName, familyName, pageable, uriBuilder); + return uriBuilder.build(); + } + ) + .retrieve() + .bodyToMono(new ParameterizedTypeReference<PageResultResource<UserRefDTO>>() {}) + .block(); + } catch (CustomWebClientException ex) { + throw new MicroserviceApiException("Error when calling user management service API to obtain users with role " + roleType.name() + " and IDs: " + userRefIds + ".", ex.getApiSubError()); + } + } + + /** + * Create new user reference + * + * @param userRefToCreate user reference to be created + * @return created {@link UserRef} + */ + @TransactionalWO + public UserRef createUserRef(UserRef userRefToCreate) { + UserRef userRef = userRefRepository.save(userRefToCreate); + LOG.info("User ref with user_ref_id: {} created.", userRef.getUserRefId()); + return userRef; + } + + private void setCommonParams(String givenName, String familyName, Pageable pageable, UriBuilder builder) { + if (givenName != null) { + builder.queryParam("givenName", givenName); + } + if (familyName != null) { + builder.queryParam("familyName", familyName); + } + builder.queryParam("page", pageable.getPageNumber()); + builder.queryParam("size", pageable.getPageSize()); + } + + /** + * Check if response from external API is not null. + * + * @param object object to check + * @param message exception message if response is null + * @throws MicroserviceApiException if response is null + */ + private void checkNonNull(Object object, String message) { + if (object == null) { + throw new MicroserviceApiException(message); + } + } +}