diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKey.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKey.java new file mode 100644 index 00000000..66294db2 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/EntityKey.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.gbl.model; + +import javax.validation.constraints.NotNull; + +public class EntityKey { + + public final String entityId; + public final EntityType entityType; + public final boolean isIdPK; + + public EntityKey( + @NotNull final Long entityId, + @NotNull final EntityType entityType) { + + this.entityId = String.valueOf(entityId); + this.entityType = entityType; + this.isIdPK = true; + } + + public EntityKey( + @NotNull final String entityId, + @NotNull final EntityType entityType, + final boolean isIdPK) { + + this.entityId = entityId; + this.entityType = entityType; + this.isIdPK = isIdPK; + } + + public String getEntityId() { + return this.entityId; + } + + public EntityType getEntityType() { + return this.entityType; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.entityId == null) ? 0 : this.entityId.hashCode()); + result = prime * result + ((this.entityType == null) ? 0 : this.entityType.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final EntityKey other = (EntityKey) obj; + if (this.entityId == null) { + if (other.entityId != null) + return false; + } else if (!this.entityId.equals(other.entityId)) + return false; + if (this.entityType != other.entityType) + return false; + return true; + } + + @Override + public String toString() { + return "EntityKey [entityId=" + this.entityId + ", entityType=" + this.entityType + "]"; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java index 1152925c..1c20f76b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/model/institution/LmsSetup.java @@ -15,12 +15,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import ch.ethz.seb.sebserver.gbl.model.Activatable; import ch.ethz.seb.sebserver.gbl.model.Domain; +import ch.ethz.seb.sebserver.gbl.model.Domain.INSTITUTION; import ch.ethz.seb.sebserver.gbl.model.Domain.LMS_SETUP; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; -public final class LmsSetup implements GrantEntity { +public final class LmsSetup implements GrantEntity, Activatable { public enum LMSType { MOCKUP, @@ -67,6 +69,10 @@ public final class LmsSetup implements GrantEntity { @Size(min = 8, max = 255, message = "lmsSetup:sebAuthSecret:size:{min}:{max}:${validatedValue}") public final String sebAuthSecret; + /** Indicates whether this LmsSetup is still active or not */ + @JsonProperty(LMS_SETUP.ATTR_ACTIVE) + public final Boolean active; + @JsonCreator public LmsSetup( @JsonProperty(Domain.ATTR_ID) final Long id, @@ -78,7 +84,8 @@ public final class LmsSetup implements GrantEntity { @JsonProperty(LMS_SETUP.ATTR_LMS_URL) final String lmsApiUrl, @JsonProperty(LMS_SETUP.ATTR_LMS_REST_API_TOKEN) final String lmsRestApiToken, @JsonProperty(LMS_SETUP.ATTR_SEB_CLIENTNAME) final String sebAuthName, - @JsonProperty(LMS_SETUP.ATTR_SEB_CLIENTSECRET) final String sebAuthSecret) { + @JsonProperty(LMS_SETUP.ATTR_SEB_CLIENTSECRET) final String sebAuthSecret, + @JsonProperty(INSTITUTION.ATTR_ACTIVE) final Boolean active) { this.id = id; this.institutionId = institutionId; @@ -90,6 +97,7 @@ public final class LmsSetup implements GrantEntity { this.lmsRestApiToken = lmsRestApiToken; this.sebAuthName = sebAuthName; this.sebAuthSecret = sebAuthSecret; + this.active = active; } @Override @@ -107,6 +115,11 @@ public final class LmsSetup implements GrantEntity { return this.id; } + @Override + public boolean isActive() { + return this.active; + } + @JsonIgnore @Override public String getModelId() { @@ -152,6 +165,10 @@ public final class LmsSetup implements GrantEntity { return this.sebAuthSecret; } + public Boolean getActive() { + return this.active; + } + @Override public String toString() { return "LmsSetup [id=" + this.id + ", institutionId=" + this.institutionId + ", name=" + this.name @@ -159,7 +176,7 @@ public final class LmsSetup implements GrantEntity { + ", lmsAuthName=" + this.lmsAuthName + ", lmsAuthSecret=" + this.lmsAuthSecret + ", lmsApiUrl=" + this.lmsApiUrl + ", lmsRestApiToken=" + this.lmsRestApiToken + ", sebAuthName=" + this.sebAuthName + ", sebAuthSecret=" - + this.sebAuthSecret + "]"; + + this.sebAuthSecret + ", active=" + this.active + "]"; } } diff --git a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java index daabe704..b2f82b13 100644 --- a/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java +++ b/src/main/java/ch/ethz/seb/sebserver/gbl/util/Result.java @@ -32,6 +32,17 @@ import java.util.stream.Stream; * } * * + * Or use the Results tryCatch that wraps the given code block in a try catch internally + * + *
+ *      public Result compute(String s1, String s2) {
+ *          return Result.tryCatch(() -> {
+ *              ... do some computation
+ *              return result;
+ *          });
+ *      }
+ * 
+ * * If you are familiar with java.util.Optional think of Result like an Optional with the * capability to report an error. * @@ -107,7 +118,11 @@ public final class Result { * @return mapped Result of type U */ public Result map(final Function mapf) { if (this.error == null) { - return Result.of(mapf.apply(this.value)); + try { + return Result.of(mapf.apply(this.value)); + } catch (final Throwable t) { + return Result.ofError(t); + } } else { return Result.ofError(this.error); } @@ -126,7 +141,11 @@ public final class Result { * @return mapped Result of type U */ public Result flatMap(final Function> mapf) { if (this.error == null) { - return mapf.apply(this.value); + try { + return mapf.apply(this.value); + } catch (final Throwable t) { + return Result.ofError(t); + } } else { return Result.ofError(this.error); } @@ -215,4 +234,35 @@ public final class Result { return Stream.of(result.value); } } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.value == null) ? 0 : this.value.hashCode()); + return result; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final Result other = (Result) obj; + if (this.value == null) { + if (other.value != null) + return false; + } else if (!this.value.equals(other.value)) + return false; + return true; + } + + @Override + public String toString() { + return "Result [value=" + this.value + ", error=" + this.error + "]"; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationEvent.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationEvent.java deleted file mode 100644 index 577ea104..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationEvent.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package ch.ethz.seb.sebserver.webservice.servicelayer.activation; - -import org.springframework.context.ApplicationEvent; - -import ch.ethz.seb.sebserver.gbl.model.Entity; - -public final class EntityActivationEvent extends ApplicationEvent { - - private static final long serialVersionUID = -6712364320755441148L; - - public final boolean activated; - - public EntityActivationEvent(final Entity source, final boolean activated) { - super(source); - this.activated = activated; - } - - public Entity getEntity() { - return (Entity) this.source; - } - -} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationService.java deleted file mode 100644 index 5f34240b..00000000 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/activation/EntityActivationService.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) 2018 ETH Zürich, Educational Development and Technology (LET) - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -package ch.ethz.seb.sebserver.webservice.servicelayer.activation; - -import java.util.Collection; - -import org.springframework.context.ApplicationEventPublisher; -import org.springframework.context.event.EventListener; -import org.springframework.stereotype.Service; - -import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ActivatableEntityDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; -import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType; - -@Service -@WebServiceProfile -public class EntityActivationService { - - private final Collection> activatableEntityDAOs; - private final ApplicationEventPublisher applicationEventPublisher; - private final UserActivityLogDAO userActivityLogDAO; - - public EntityActivationService( - final Collection> activatableEntityDAOs, - final ApplicationEventPublisher applicationEventPublisher, - final UserActivityLogDAO userActivityLogDAO) { - - this.activatableEntityDAOs = activatableEntityDAOs; - this.applicationEventPublisher = applicationEventPublisher; - this.userActivityLogDAO = userActivityLogDAO; - } - - public ApplicationEventPublisher getApplicationEventPublisher() { - return this.applicationEventPublisher; - } - - @EventListener(EntityActivationEvent.class) - public void notifyActivationEvent(final EntityActivationEvent event) { - for (final ActivatableEntityDAO dao : this.activatableEntityDAOs) { - if (event.activated) { - dao.notifyActivation(event.getEntity()); - } else { - dao.notifyDeactivation(event.getEntity()); - } - } - } - - public Result setActive(final T entity, final boolean activated) { - - final ActivityType activityType = (activated) - ? ActivityType.ACTIVATE - : ActivityType.DEACTIVATE; - - return getDAOForEntity(entity) - .setActive(entity.getModelId(), activated) - .flatMap(e -> publishEvent(e, activated)) - .flatMap(e -> this.userActivityLogDAO.log(activityType, e)); - - } - - public Result publishEvent(final T entity, final boolean activated) { - this.applicationEventPublisher.publishEvent(new EntityActivationEvent(entity, activated)); - return Result.of(entity); - } - - @SuppressWarnings("unchecked") - private ActivatableEntityDAO getDAOForEntity(final T entity) { - for (final ActivatableEntityDAO dao : this.activatableEntityDAOs) { - if (dao.entityType() == entity.entityType()) { - return (ActivatableEntityDAO) dao; - } - } - - return null; - } - -} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserServiceImpl.java index aa84d460..c7ffe952 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/authorization/UserServiceImpl.java @@ -13,7 +13,6 @@ import java.util.Collection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Lazy; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -37,14 +36,10 @@ public class UserServiceImpl implements UserService { } private final Collection extractStrategies; - private final ApplicationEventPublisher applicationEventPublisher; - public UserServiceImpl( - final Collection extractStrategies, - final ApplicationEventPublisher applicationEventPublisher) { + public UserServiceImpl(final Collection extractStrategies) { this.extractStrategies = extractStrategies; - this.applicationEventPublisher = applicationEventPublisher; } @Override diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkAction.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkAction.java new file mode 100644 index 00000000..c0296019 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkAction.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.EntityType; +import ch.ethz.seb.sebserver.gbl.util.Result; + +public final class BulkAction { + + public enum Type { + HARD_DELETE, + DEACTIVATE, + ACTIVATE + } + + public final Type type; + public final EntityType sourceType; + public final Collection sources; + + final Set dependencies; + final Set> result; + + boolean alreadyProcessed = false; + + public BulkAction( + final Type type, + final EntityType sourceType, + final Collection sources) { + + this.type = type; + this.sourceType = sourceType; + this.sources = (sources != null) + ? Collections.unmodifiableCollection(new ArrayList<>(sources)) + : Collections.emptyList(); + this.dependencies = new HashSet<>(); + this.result = new HashSet<>(); + + check(); + } + + public BulkAction( + final Type type, + final EntityType sourceType, + final EntityKey... sources) { + + this(type, sourceType, (sources != null) ? Arrays.asList(sources) : Collections.emptyList()); + } + + private void check() { + for (final EntityKey source : this.sources) { + if (source.entityType != this.sourceType) { + throw new IllegalArgumentException( + "At least one EntityType in sources list has not the expected EntityType"); + } + } + + } + + public Set extractKeys(final EntityType type) { + if (this.sourceType == type) { + return Collections.unmodifiableSet(new HashSet<>(this.sources)); + } + + if (!this.dependencies.isEmpty()) { + return Collections.unmodifiableSet(new HashSet<>(this.dependencies + .stream() + .filter(key -> key.entityType == type) + .collect(Collectors.toList()))); + } + return null; + } + + @Override + public String toString() { + return "BulkAction [type=" + this.type + ", sourceType=" + this.sourceType + ", sources=" + this.sources + "]"; + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionService.java new file mode 100644 index 00000000..f06f54c2 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionService.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction; + +import java.util.Collection; +import java.util.List; + +import org.springframework.stereotype.Service; + +import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; + +@Service +@WebServiceProfile +public class BulkActionService { + + private final Collection supporter; + + public BulkActionService(final Collection supporter) { + this.supporter = supporter; + } + + public void collectDependencies(final BulkAction action) { + checkProcessing(action); + for (final BulkActionSupport sup : this.supporter) { + action.dependencies.addAll(sup.getDependencies(action)); + } + action.alreadyProcessed = true; + } + + public void doBulkAction(final BulkAction action) { + checkProcessing(action); + + final BulkActionSupport supportForSource = getSupporterForSource(action); + if (supportForSource == null) { + action.alreadyProcessed = true; + return; + } + + collectDependencies(action); + + if (!action.dependencies.isEmpty()) { + // process dependencies first... + final List dependantSupporterInHierarchicalOrder = + getDependantSupporterInHierarchicalOrder(action); + + for (final BulkActionSupport support : dependantSupporterInHierarchicalOrder) { + action.result.addAll(support.processBulkAction(action)); + } + } + + // process bulk action + action.result.addAll(supportForSource.processBulkAction(action)); + action.alreadyProcessed = true; + } + + public EntityProcessingReport createReport(final BulkAction action) { + if (!action.alreadyProcessed) { + doBulkAction(action); + } + + final EntityProcessingReport report = new EntityProcessingReport(); + + // TODO + + return report; + } + + private BulkActionSupport getSupporterForSource(final BulkAction action) { + for (final BulkActionSupport support : this.supporter) { + if (support.entityType() == action.sourceType) { + return support; + } + } + + return null; + } + + private List getDependantSupporterInHierarchicalOrder(final BulkAction action) { + + // TODO + + return null; + } + + private void checkProcessing(final BulkAction action) { + if (action.alreadyProcessed) { + throw new IllegalStateException("Given BulkAction has already been processed. Use a new one"); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionSupport.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionSupport.java new file mode 100644 index 00000000..449feb81 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/bulkaction/BulkActionSupport.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction; + +import java.util.Collection; +import java.util.Set; + +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.EntityType; +import ch.ethz.seb.sebserver.gbl.util.Result; + +public interface BulkActionSupport { + + /** Get the entity type for a concrete EntityDAO implementation. + * + * @return The EntityType for a concrete EntityDAO implementation */ + EntityType entityType(); + + Set getDependencies(BulkAction bulkAction); + + Collection> processBulkAction(BulkAction bulkAction); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java index 648c306d..bb03f6be 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ActivatableEntityDAO.java @@ -9,8 +9,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; import java.util.Collection; +import java.util.Set; + +import org.springframework.transaction.annotation.Transactional; import ch.ethz.seb.sebserver.gbl.model.Entity; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.util.Result; /** Interface of a DAO for an Entity that has activation feature. @@ -22,29 +26,16 @@ public interface ActivatableEntityDAO extends EntityDAO { * * @return A Result refer to a Collection of all active Entity instances for a concrete entity-domain * or refer to an error if happened */ - Result> allActive(); + @Transactional(readOnly = true) + default Result> allActive() { + return all(i -> true, true); + } - /** Set the entity with specified identifier active / inactive + /** Set all entities referred by the given Collection of EntityKey active / inactive * - * @param entityId The Entity identifier + * @param all The Collection of EntityKeys to set active or inactive * @param active The active flag - * @return A Result refer to the Entity instance or refer to an error if happened */ - Result setActive(String entityId, boolean active); - - /** Get notified if some Entity instance has been activated - * This can be used to take action in dependency of an activation of an Entity of different type. - * For example a user-account DAO want to react on a Institution activation to also activate all user - * accounts for this institution. - * - * @param source The source Entity that has been activated */ - void notifyActivation(Entity source); - - /** Get notified if some Entity instance has been deactivated - * This can be used to take action in dependency of an deactivation of an Entity of different type. - * For example a user-account DAO want to react on a Institution deactivation to also deactivate all user - * accounts for this institution. - * - * @param source The source Entity that has been deactivated */ - void notifyDeactivation(Entity source); + * @return The Collection of Results refer to the EntityKey instance or refer to an error if happened */ + Collection> setActive(Set all, boolean active); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java index d6b5704f..499726da 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/EntityDAO.java @@ -8,11 +8,14 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; +import java.util.Set; import java.util.function.Predicate; import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -37,10 +40,10 @@ public interface EntityDAO { * If you need a fast filtering implement a specific filtering in SQL level * * @param predicate Predicate expecting instance of type specific entity type - * @param onlyActive indicates if only active entities should be included (on SQL level) + * @param active indicates if only active entities should be included (on SQL level). Can be null. * @return Result of Collection of Entity that matches a given predicate. Or an exception result on error * case */ - Result> all(Predicate predicate, boolean onlyActive); + Result> all(Predicate predicate, Boolean active); /** Use this to get a Collection of all entities of concrete type that matches a given predicate. * @@ -52,7 +55,7 @@ public interface EntityDAO { * @return Result of Collection of Entity that matches a given predicate. Or an exception result on error * case */ default Result> all(final Predicate predicate) { - return all(predicate, false); + return all(predicate, null); } /** Use this to get a Collection of all active entities of concrete type @@ -62,14 +65,12 @@ public interface EntityDAO { return all(entity -> true); } - /** Use this to delete an Entity and all its relationships by id + /** Use this to delete a set Entity by a Collection of EntityKey * - * @param id the identifier if the entity to delete - * @param archive indicates whether the Entity and all its relations should be archived (inactive and anonymous) or - * hard deleted - * @return Result of a collection of all entities that has been deleted (or archived) or refer to an error if + * @param all The Collection of EntityKey to delete + * @return Result of a collection of all entities that has been deleted or refer to an error if * happened */ - Result delete(Long id, boolean archive); + Collection> delete(Set all); /** Utility method to extract an expected single resource entry form a Collection of specified type. * Gets a Result refer to an expected single resource entry form a Collection of specified type or refer @@ -93,4 +94,23 @@ public interface EntityDAO { return Result.of(resources.iterator().next()); } + default List extractIdsFromKeys( + final Collection keys, + final Collection> result) { + + final EntityType entityType = entityType(); + final List ids = new ArrayList<>(); + for (final EntityKey key : keys) { + if (key.entityType == entityType) { + try { + ids.add(Long.valueOf(key.entityId)); + } catch (final Exception e) { + result.add(Result.ofError(new IllegalArgumentException("Invalid id for EntityKey: " + key))); + } + } + } + + return ids; + } + } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java new file mode 100644 index 00000000..bd56eebb --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/LmsSetupDAO.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.dao; + +import java.util.Collection; + +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LMSType; +import ch.ethz.seb.sebserver.gbl.util.Result; + +public interface LmsSetupDAO extends ActivatableEntityDAO { + + @Transactional(readOnly = true) + default Result> allOfInstitution(final Long institutionId, final Boolean active) { + return allMatching(institutionId, null, null, active); + } + + Result> allMatching(Long institutionId, String name, LMSType lmsType, Boolean active); + + Result save(LmsSetup lmsSetup); + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java index 48b129be..61a23613 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/UserDAO.java @@ -11,7 +11,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao; import java.util.Collection; import java.util.function.Predicate; -import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.user.UserFilter; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserMod; @@ -53,6 +55,7 @@ public interface UserDAO extends ActivatableEntityDAO { * * @param filter The UserFilter instance containing all filter criteria * @return a Result of Collection of filtered UserInfo. Or an exception result on error case */ + @Transactional(readOnly = true) default Result> all(final UserFilter filter) { return all(filter, userInfo -> true); } @@ -80,11 +83,10 @@ public interface UserDAO extends ActivatableEntityDAO { * exception on error case */ Result save(UserMod userMod); - /** Use this to get an EntityProcessingReport containing the user account entity itself - * and all user related data entities. + /** Use this to get a Collection containing EntityKey's of all entities that belongs to a given User. * * @param uuid The UUID of the user - * @return an EntityProcessingReport containing all user related entity data */ - Result getAllUserData(String uuid); + * @return a Collection containing EntityKey's of all entities that belongs to a given User */ + Collection getAllUserData(String uuid); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/InstitutionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/InstitutionDAOImpl.java index f2cb25b8..8913417b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/InstitutionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/InstitutionDAOImpl.java @@ -9,9 +9,13 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl; import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; +import static org.mybatis.dynamic.sql.SqlBuilder.isIn; +import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -23,8 +27,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.institution.Institution; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -33,13 +36,15 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.InstitutionRecord import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.InstitutionRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.InstitutionRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; @Lazy @Component -public class InstitutionDAOImpl implements InstitutionDAO { +public class InstitutionDAOImpl implements InstitutionDAO, BulkActionSupport { private final InstitutionRecordMapper institutionRecordMapper; @@ -61,19 +66,13 @@ public class InstitutionDAOImpl implements InstitutionDAO { @Override @Transactional(readOnly = true) - public Result> allActive() { - return allMatching(null, true); - } - - @Override - @Transactional(readOnly = true) - public Result> all(final Predicate predicate, final boolean onlyActive) { + public Result> all(final Predicate predicate, final Boolean active) { return Result.tryCatch(() -> { final QueryExpressionDSL>> example = this.institutionRecordMapper.selectByExample(); - final List records = (onlyActive) - ? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true))) + final List records = (active != null) + ? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(active))) .build() .execute() : example.build().execute(); @@ -113,42 +112,88 @@ public class InstitutionDAOImpl implements InstitutionDAO { } return (institution.id != null) - ? updateUser(institution) + ? update(institution) .flatMap(InstitutionDAOImpl::toDomainModel) .onErrorDo(TransactionHandler::rollback) - : createNewUser(institution) + : createNew(institution) .flatMap(InstitutionDAOImpl::toDomainModel) .onErrorDo(TransactionHandler::rollback); } @Override @Transactional - public Result setActive(final String entityId, final boolean active) { - return Result.tryCatch(() -> { - final Long institutionId = Long.valueOf(entityId); + public Collection> setActive(final Set all, final boolean active) { + final Collection> result = new ArrayList<>(); - this.institutionRecordMapper.updateByPrimaryKeySelective(new InstitutionRecord( - institutionId, null, null, BooleanUtils.toInteger(active), null)); + final List ids = extractIdsFromKeys(all, result); + final InstitutionRecord institutionRecord = new InstitutionRecord( + null, null, null, BooleanUtils.toInteger(active), null); - return this.institutionRecordMapper.selectByPrimaryKey(institutionId); - }).flatMap(InstitutionDAOImpl::toDomainModel); - } + try { + this.institutionRecordMapper.updateByExampleSelective(institutionRecord) + .where(InstitutionRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); - @Override - public void notifyActivation(final Entity source) { - // No dependencies of activation on Institution - } - - @Override - public void notifyDeactivation(final Entity source) { - // No dependencies of activation on Institution + return ids.stream() + .map(id -> Result.of(new EntityKey(id, EntityType.INSTITUTION))) + .collect(Collectors.toList()); + } catch (final Exception e) { + return ids.stream() + .map(id -> Result. ofError(new RuntimeException( + "Activation failed on unexpected exception for Institution of id: " + id, e))) + .collect(Collectors.toList()); + } } @Override @Transactional - public Result delete(final Long id, final boolean archive) { - // TODO Auto-generated method stub - return null; + public Collection> delete(final Set all) { + final Collection> result = new ArrayList<>(); + + final List ids = extractIdsFromKeys(all, result); + + try { + this.institutionRecordMapper.deleteByExample() + .where(InstitutionRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> Result.of(new EntityKey(id, EntityType.INSTITUTION))) + .collect(Collectors.toList()); + } catch (final Exception e) { + return ids.stream() + .map(id -> Result. ofError(new RuntimeException( + "Deletion failed on unexpected exception for Institution of id: " + id, e))) + .collect(Collectors.toList()); + } + } + + @Override + @Transactional(readOnly = true) + public Set getDependencies(final BulkAction bulkAction) { + // NOTE since Institution is the top most Entity, there are no other Entity for that an Institution depends on. + return Collections.emptySet(); + } + + @Override + @Transactional + public Collection> processBulkAction(final BulkAction bulkAction) { + + final Set all = bulkAction.extractKeys(EntityType.INSTITUTION); + + switch (bulkAction.type) { + case ACTIVATE: + return setActive(all, true); + case DEACTIVATE: + return setActive(all, false); + case HARD_DELETE: + return delete(all); + } + + // should never happen + throw new UnsupportedOperationException("Unsupported Bulk Action: " + bulkAction); } private Result recordById(final Long id) { @@ -163,7 +208,7 @@ public class InstitutionDAOImpl implements InstitutionDAO { }); } - private Result createNewUser(final Institution institution) { + private Result createNew(final Institution institution) { return Result.tryCatch(() -> { final InstitutionRecord newRecord = new InstitutionRecord( null, @@ -177,9 +222,9 @@ public class InstitutionDAOImpl implements InstitutionDAO { }); } - private Result updateUser(final Institution institution) { + private Result update(final Institution institution) { return recordById(institution.id) - .flatMap(record -> Result.tryCatch(() -> { + .map(record -> { final InstitutionRecord newRecord = new InstitutionRecord( institution.id, @@ -190,7 +235,7 @@ public class InstitutionDAOImpl implements InstitutionDAO { this.institutionRecordMapper.updateByPrimaryKeySelective(newRecord); return this.institutionRecordMapper.selectByPrimaryKey(institution.id); - })); + }); } private static Result toDomainModel(final InstitutionRecord record) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java new file mode 100644 index 00000000..0602ec3c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/LmsSetupDAOImpl.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET) + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl; + +import static ch.ethz.seb.sebserver.gbl.util.Utils.toSQLWildcard; +import static org.mybatis.dynamic.sql.SqlBuilder.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.BooleanUtils; +import org.mybatis.dynamic.sql.select.MyBatis3SelectModelAdapter; +import org.mybatis.dynamic.sql.select.QueryExpressionDSL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Lazy; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import ch.ethz.seb.sebserver.gbl.model.EntityKey; +import ch.ethz.seb.sebserver.gbl.model.EntityType; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup; +import ch.ethz.seb.sebserver.gbl.model.institution.LmsSetup.LMSType; +import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordDynamicSqlSupport; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.LmsSetupRecordMapper; +import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.LmsSetupRecord; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupport; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.LmsSetupDAO; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; +import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; + +@Lazy +@Component +public class LmsSetupDAOImpl implements LmsSetupDAO, BulkActionSupport { + + private static final Logger log = LoggerFactory.getLogger(LmsSetupDAOImpl.class); + + private final LmsSetupRecordMapper lmsSetupRecordMapper; + + public LmsSetupDAOImpl(final LmsSetupRecordMapper lmsSetupRecordMapper) { + this.lmsSetupRecordMapper = lmsSetupRecordMapper; + } + + @Override + public EntityType entityType() { + return EntityType.LMS_SETUP; + } + + @Override + @Transactional(readOnly = true) + public Result byId(final Long id) { + return recordById(id) + .flatMap(LmsSetupDAOImpl::toDomainModel); + } + + @Override + @Transactional(readOnly = true) + public Result> all(final Predicate predicate, final Boolean active) { + return Result.tryCatch(() -> { + final QueryExpressionDSL>> example = + this.lmsSetupRecordMapper.selectByExample(); + + final List records = (active != null) + ? example + .where(LmsSetupRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(active))) + .build() + .execute() + : example.build().execute(); + + return records.stream() + .map(LmsSetupDAOImpl::toDomainModel) + .flatMap(Result::skipOnError) + .filter(predicate) + .collect(Collectors.toList()); + }); + } + + @Override + @Transactional(readOnly = true) + public Result> allMatching( + final Long institutionId, + final String name, + final LMSType lmsType, + final Boolean active) { + + return Result.tryCatch(() -> { + + final String _lmsType = (lmsType != null) ? lmsType.name() : null; + return this.lmsSetupRecordMapper + .selectByExample() + .where(LmsSetupRecordDynamicSqlSupport.institutionId, isEqualToWhenPresent(institutionId)) + .and(LmsSetupRecordDynamicSqlSupport.name, isLikeWhenPresent(toSQLWildcard(name))) + .and(LmsSetupRecordDynamicSqlSupport.lmsType, isEqualToWhenPresent(_lmsType)) + .and(LmsSetupRecordDynamicSqlSupport.active, + isEqualToWhenPresent(BooleanUtils.toIntegerObject(active))) + .build() + .execute() + .stream() + .map(LmsSetupDAOImpl::toDomainModel) + .flatMap(Result::skipOnError) + .collect(Collectors.toList()); + }); + } + + @Override + @Transactional + public Result save(final LmsSetup lmsSetup) { + if (lmsSetup == null) { + return Result.ofError(new NullPointerException("lmsSetup has null-reference")); + } + + return (lmsSetup.id != null) + ? update(lmsSetup) + .flatMap(LmsSetupDAOImpl::toDomainModel) + .onErrorDo(TransactionHandler::rollback) + : createNew(lmsSetup) + .flatMap(LmsSetupDAOImpl::toDomainModel) + .onErrorDo(TransactionHandler::rollback); + } + + @Override + @Transactional + public Collection> setActive(final Set all, final boolean active) { + final Collection> result = new ArrayList<>(); + + final List ids = extractIdsFromKeys(all, result); + final LmsSetupRecord lmsSetupRecord = new LmsSetupRecord( + null, null, null, null, null, null, null, null, null, null, + BooleanUtils.toIntegerObject(active)); + + try { + this.lmsSetupRecordMapper.updateByExampleSelective(lmsSetupRecord) + .where(LmsSetupRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> Result.of(new EntityKey(id, EntityType.LMS_SETUP))) + .collect(Collectors.toList()); + } catch (final Exception e) { + return ids.stream() + .map(id -> Result. ofError(new RuntimeException( + "Activation failed on unexpected exception for LmsSetup of id: " + id, e))) + .collect(Collectors.toList()); + } + } + + @Override + @Transactional + public Collection> delete(final Set all) { + final Collection> result = new ArrayList<>(); + + final List ids = extractIdsFromKeys(all, result); + + try { + this.lmsSetupRecordMapper.deleteByExample() + .where(LmsSetupRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> Result.of(new EntityKey(id, EntityType.LMS_SETUP))) + .collect(Collectors.toList()); + } catch (final Exception e) { + return ids.stream() + .map(id -> Result. ofError(new RuntimeException( + "Deletion failed on unexpected exception for LmsSetup of id: " + id, e))) + .collect(Collectors.toList()); + } + } + + @Override + @Transactional(readOnly = true) + public Set getDependencies(final BulkAction bulkAction) { + // all of institution + if (bulkAction.sourceType == EntityType.INSTITUTION) { + final Set result = new HashSet<>(); + for (final EntityKey sourceKey : bulkAction.sources) { + try { + result.addAll(this.lmsSetupRecordMapper.selectIdsByExample() + .where(LmsSetupRecordDynamicSqlSupport.institutionId, + isEqualTo(Long.valueOf(sourceKey.entityId))) + .build() + .execute() + .stream() + .map(id -> new EntityKey(id, EntityType.LMS_SETUP)) + .collect(Collectors.toList())); + } catch (final Exception e) { + log.error("Unexpected error: ", e); + return Collections.emptySet(); + } + } + return result; + } + + return Collections.emptySet(); + } + + @Override + @Transactional + public Collection> processBulkAction(final BulkAction bulkAction) { + final Set all = bulkAction.extractKeys(EntityType.LMS_SETUP); + + switch (bulkAction.type) { + case ACTIVATE: + return setActive(all, true); + case DEACTIVATE: + return setActive(all, false); + case HARD_DELETE: + return delete(all); + } + + // should never happen + throw new UnsupportedOperationException("Unsupported Bulk Action: " + bulkAction); + } + + private Result recordById(final Long id) { + return Result.tryCatch(() -> { + final LmsSetupRecord record = this.lmsSetupRecordMapper.selectByPrimaryKey(id); + if (record == null) { + throw new ResourceNotFoundException( + EntityType.LMS_SETUP, + String.valueOf(id)); + } + return record; + }); + } + + private static Result toDomainModel(final LmsSetupRecord record) { + return Result.tryCatch(() -> new LmsSetup( + record.getId(), + record.getInstitutionId(), + record.getName(), + LMSType.valueOf(record.getLmsType()), + record.getLmsClientname(), + record.getLmsClientsecret(), + record.getLmsUrl(), + record.getLmsRestApiToken(), + record.getSebClientname(), + record.getSebClientsecret(), + BooleanUtils.toBooleanObject(record.getActive()))); + } + + private Result createNew(final LmsSetup lmsSetup) { + return Result.tryCatch(() -> { + + final LmsSetupRecord newRecord = new LmsSetupRecord( + null, + lmsSetup.institutionId, + lmsSetup.name, + (lmsSetup.lmsType != null) ? lmsSetup.lmsType.name() : null, + lmsSetup.lmsApiUrl, + lmsSetup.lmsAuthName, + lmsSetup.lmsAuthSecret, + lmsSetup.lmsRestApiToken, + lmsSetup.sebAuthName, + lmsSetup.sebAuthSecret, + BooleanUtils.toInteger(false)); + + this.lmsSetupRecordMapper.insert(newRecord); + return newRecord; + }); + } + + private Result update(final LmsSetup lmsSetup) { + return recordById(lmsSetup.id) + .map(record -> { + + final LmsSetupRecord newRecord = new LmsSetupRecord( + lmsSetup.id, + lmsSetup.institutionId, + lmsSetup.name, + (lmsSetup.lmsType != null) ? lmsSetup.lmsType.name() : null, + lmsSetup.lmsApiUrl, + lmsSetup.lmsAuthName, + lmsSetup.lmsAuthSecret, + lmsSetup.lmsRestApiToken, + lmsSetup.sebAuthName, + lmsSetup.sebAuthSecret, + null); + + this.lmsSetupRecordMapper.updateByPrimaryKeySelective(newRecord); + return this.lmsSetupRecordMapper.selectByPrimaryKey(lmsSetup.id); + }); + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java index 16db6de9..5baa88bb 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserActivityLogDAOImpl.java @@ -8,8 +8,12 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl; +import static org.mybatis.dynamic.sql.SqlBuilder.isIn; + +import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -22,7 +26,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -176,16 +180,26 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Override @Transactional - public Result delete(final Long id, final boolean archive) { - return Result.tryCatch(() -> { - final EntityProcessingReport report = new EntityProcessingReport(); + public Collection> delete(final Set all) { + final Collection> result = new ArrayList<>(); - final UserActivityLog log = byId(id).getOrThrow(); - this.userLogRecordMapper.deleteByPrimaryKey(id); - report.add(log); + final List ids = extractIdsFromKeys(all, result); - return report; - }).onErrorDo(TransactionHandler::rollback); + try { + this.userLogRecordMapper.deleteByExample() + .where(UserActivityLogRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); + + return ids.stream() + .map(id -> Result.of(new EntityKey(id, EntityType.USER_ACTIVITY_LOG))) + .collect(Collectors.toList()); + } catch (final Exception e) { + return ids.stream() + .map(id -> Result. ofError(new RuntimeException( + "Deletion failed on unexpected exception for UserActivityLog of id: " + id, e))) + .collect(Collectors.toList()); + } } @Override @@ -256,9 +270,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO { @Override @Transactional(readOnly = true) - public Result> all( - final Predicate predicate, - final boolean onlyActive) { + public Result> all(final Predicate predicate, final Boolean active) { return Result.tryCatch(() -> { // first check if there is a page limitation set. Otherwise set the default diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java index d7de86fb..4d4047d7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/UserDaoImpl.java @@ -11,8 +11,10 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl; import static ch.ethz.seb.sebserver.gbl.util.Utils.toSQLWildcard; import static org.mybatis.dynamic.sql.SqlBuilder.*; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -37,8 +39,7 @@ import org.springframework.transaction.annotation.Transactional; import ch.ethz.seb.sebserver.WebSecurityConfig; import ch.ethz.seb.sebserver.gbl.model.APIMessage.APIMessageException; import ch.ethz.seb.sebserver.gbl.model.APIMessage.ErrorMessage; -import ch.ethz.seb.sebserver.gbl.model.Entity; -import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.user.UserFilter; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; @@ -51,14 +52,17 @@ import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordMapper; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.RoleRecord; import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.UserRecord; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.SEBServerUser; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; @Lazy @Component -public class UserDaoImpl implements UserDAO { +public class UserDaoImpl implements UserDAO, BulkActionSupport { private static final Logger log = LoggerFactory.getLogger(UserDaoImpl.class); + private static final UserFilter ALL_ACTIVE_ONLY_FILTER = new UserFilter(null, null, null, null, true, null); private final UserRecordMapper userRecordMapper; private final RoleRecordMapper roleRecordMapper; @@ -117,18 +121,18 @@ public class UserDaoImpl implements UserDAO { @Override @Transactional(readOnly = true) public Result> allActive() { - return all(new UserFilter(null, null, null, null, true, null)); + return all(ALL_ACTIVE_ONLY_FILTER); } @Override @Transactional(readOnly = true) - public Result> all(final Predicate predicate, final boolean onlyActive) { + public Result> all(final Predicate predicate, final Boolean active) { return Result.tryCatch(() -> { final QueryExpressionDSL>> example = this.userRecordMapper.selectByExample(); - final List records = (onlyActive) - ? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true))) + final List records = (active != null) + ? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(active))) .build() .execute() : example.build().execute(); @@ -185,77 +189,136 @@ public class UserDaoImpl implements UserDAO { @Override @Transactional - public Result delete(final Long id, final boolean force) { + public Collection> setActive(final Set all, final boolean active) { + final Collection> result = new ArrayList<>(); - // TODO clarify within discussion about deactivate, archive and delete user related data + final List ids = extractIdsFromKeys(all, result); + final UserRecord userRecord = new UserRecord( + null, null, null, null, null, null, null, null, null, + BooleanUtils.toIntegerObject(active)); - return Result.ofError(new RuntimeException("TODO")); - } + try { + this.userRecordMapper.updateByExampleSelective(userRecord) + .where(UserRecordDynamicSqlSupport.id, isIn(ids)) + .build() + .execute(); - @Override - @Transactional(readOnly = true) - public Result getAllUserData(final String uuid) { - - // TODO - - return Result.ofError(new RuntimeException("TODO")); + return ids.stream() + .map(id -> Result.of(new EntityKey(id, EntityType.USER))) + .collect(Collectors.toList()); + } catch (final Exception e) { + return ids.stream() + .map(id -> Result. ofError(new RuntimeException( + "Activation failed on unexpected exception for User of id: " + id, e))) + .collect(Collectors.toList()); + } } @Override @Transactional - public Result setActive(final String entityId, final boolean active) { - return Result.tryCatch(() -> { + public Collection> delete(final Set all) { + final Collection> result = new ArrayList<>(); - return this.userRecordMapper.updateByExampleSelective( - new UserRecord( - null, null, null, null, null, null, null, null, null, - BooleanUtils.toIntegerObject(active))) - .where(UserRecordDynamicSqlSupport.uuid, isEqualTo(entityId)) - .build() - .execute(); + final List ids = extractIdsFromKeys(all, result); - }).flatMap(count -> byUuid(entityId)); - } - - @Override - public void notifyActivation(final Entity source) { - // If an Institution has been deactivated, all its user accounts gets also be deactivated - if (source.entityType() == EntityType.INSTITUTION) { - setAllActiveForInstitution(Long.parseLong(source.getModelId()), true); - } - } - - @Override - public void notifyDeactivation(final Entity source) { - // If an Institution has been deactivated, all its user accounts gets also be deactivated - if (source.entityType() == EntityType.INSTITUTION) { - setAllActiveForInstitution(Long.parseLong(source.getModelId()), false); - } - } - - private void setAllActiveForInstitution(final Long institutionId, final boolean active) { try { - - final UserRecord record = new UserRecord( - null, null, null, null, null, null, null, null, null, - BooleanUtils.toIntegerObject(active)); - - this.userRecordMapper.updateByExampleSelective(record) - .where(UserRecordDynamicSqlSupport.institutionId, isEqualTo(institutionId)) + this.userRecordMapper.deleteByExample() + .where(UserRecordDynamicSqlSupport.id, isIn(ids)) .build() .execute(); + return ids.stream() + .map(id -> Result.of(new EntityKey(id, EntityType.USER))) + .collect(Collectors.toList()); } catch (final Exception e) { - log.error("Unexpected error while trying to set all active: {} for institution: {}", - active, - institutionId, - e); + return ids.stream() + .map(id -> Result. ofError(new RuntimeException( + "Deletion failed on unexpected exception for User of id: " + id, e))) + .collect(Collectors.toList()); + } + } + + @Override + @Transactional(readOnly = true) + public Collection getAllUserData(final String uuid) { + + // TODO + + return Collections.emptyList(); + } + + @Override + @Transactional(readOnly = true) + public Set getDependencies(final BulkAction bulkAction) { + // all of institution + if (bulkAction.sourceType == EntityType.INSTITUTION) { + final Set result = new HashSet<>(); + for (final EntityKey sourceKey : bulkAction.sources) { + try { + result.addAll(this.userRecordMapper.selectIdsByExample() + .where(UserRecordDynamicSqlSupport.institutionId, + isEqualTo(Long.valueOf(sourceKey.entityId))) + .build() + .execute() + .stream() + .map(id -> new EntityKey(id, EntityType.LMS_SETUP)) + .collect(Collectors.toList())); + } catch (final Exception e) { + log.error("Unexpected error: ", e); + return Collections.emptySet(); + } + } + return result; + } + + return Collections.emptySet(); + } + + @Override + @Transactional + public Collection> processBulkAction(final BulkAction bulkAction) { + final Set all = bulkAction.extractKeys(EntityType.USER); + + switch (bulkAction.type) { + case ACTIVATE: + return setActive(all, true); + case DEACTIVATE: + return setActive(all, false); + case HARD_DELETE: + return delete(all); + } + + // should never happen + throw new UnsupportedOperationException("Unsupported Bulk Action: " + bulkAction); + } + + @Override + public List extractIdsFromKeys( + final Collection keys, + final Collection> result) { + + if (keys == null || keys.isEmpty() || keys.iterator().next().isIdPK) { + return UserDAO.super.extractIdsFromKeys(keys, result); + } else { + final List uuids = keys.stream() + .map(key -> key.entityId) + .collect(Collectors.toList()); + + try { + return this.userRecordMapper.selectIdsByExample() + .where(UserRecordDynamicSqlSupport.uuid, isIn(uuids)) + .build() + .execute(); + } catch (final Exception e) { + log.error("Unexpected error: ", e); + return Collections.emptyList(); + } } } private Result updateUser(final UserMod userMod) { return recordByUUID(userMod.uuid) - .flatMap(record -> Result.tryCatch(() -> { + .map(record -> { final boolean changePWD = userMod.passwordChangeRequest(); if (changePWD && !userMod.newPasswordMatch()) { throw new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH); @@ -276,7 +339,7 @@ public class UserDaoImpl implements UserDAO { this.userRecordMapper.updateByPrimaryKeySelective(newRecord); updateRolesForUser(record.getId(), userMod.roles); return this.userRecordMapper.selectByPrimaryKey(record.getId()); - })); + }); } private Result createNewUser(final UserMod userMod) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java index ed169184..bd6e97aa 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/InstitutionController.java @@ -22,16 +22,19 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import ch.ethz.seb.sebserver.gbl.model.EntityIdAndName; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.institution.Institution; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.webservice.servicelayer.activation.EntityActivationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationGrantService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.SEBServerUser; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction.Type; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.InstitutionDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType; @@ -45,19 +48,19 @@ public class InstitutionController { private final AuthorizationGrantService authorizationGrantService; private final UserService userService; private final UserActivityLogDAO userActivityLogDAO; - private final EntityActivationService entityActivationService; + private final BulkActionService bulkActionService; public InstitutionController( final InstitutionDAO institutionDAO, final AuthorizationGrantService authorizationGrantService, final UserService userService, final UserActivityLogDAO userActivityLogDAO, - final EntityActivationService entityActivationService) { + final BulkActionService bulkActionService) { this.institutionDAO = institutionDAO; this.authorizationGrantService = authorizationGrantService; this.userService = userService; this.userActivityLogDAO = userActivityLogDAO; - this.entityActivationService = entityActivationService; + this.bulkActionService = bulkActionService; } @RequestMapping(path = "/self", method = RequestMethod.GET) @@ -151,37 +154,45 @@ public class InstitutionController { @RequestMapping(path = "/{id}/delete", method = RequestMethod.DELETE) public EntityProcessingReport deleteUser(@PathVariable final Long id) { - return this.institutionDAO.delete(id, true) - .flatMap(report -> this.userActivityLogDAO.log( - ActivityType.DELETE, - EntityType.INSTITUTION, - String.valueOf(id), - "soft-delete", - report)) + checkPrivilegeForInstitution(id, PrivilegeType.WRITE); + + return this.bulkActionService.createReport(new BulkAction( + Type.DEACTIVATE, + EntityType.INSTITUTION, + new EntityKey(id, EntityType.INSTITUTION))); + } + + @RequestMapping(path = "/{id}/hard-delete", method = RequestMethod.DELETE) + public EntityProcessingReport hardDeleteUser(@PathVariable final Long id) { + checkPrivilegeForInstitution(id, PrivilegeType.WRITE); + + return this.bulkActionService.createReport(new BulkAction( + Type.HARD_DELETE, + EntityType.INSTITUTION, + new EntityKey(id, EntityType.INSTITUTION))); + } + + private void checkPrivilegeForInstitution(final Long id, final PrivilegeType type) { + this.authorizationGrantService.checkHasAnyPrivilege( + EntityType.INSTITUTION, + type); + + this.institutionDAO.byId(id) + .flatMap(institution -> this.authorizationGrantService.checkGrantOnEntity( + institution, + type)) .getOrThrow(); } -// TODO do we need a hard-delete for an institution? this may be dangerous? -// @RequestMapping(path = "/{id}/hard-delete", method = RequestMethod.DELETE) -// public EntityProcessingReport hardDeleteUser(@PathVariable final Long id) { -// return this.userDao.pkForUUID(uuid) -// .flatMap(pk -> this.userDao.delete(pk, false)) -// .flatMap(report -> this.userActivityLogDAO.log( -// ActivityType.DELETE, -// EntityType.USER, -// uuid, -// "hard-delete", -// report)) -// .getOrThrow(); -// } - private Institution setActive(final Long id, final boolean active) { + checkPrivilegeForInstitution(id, PrivilegeType.MODIFY); - return this.institutionDAO - .byId(id) - .flatMap(inst -> this.authorizationGrantService.checkGrantOnEntity(inst, PrivilegeType.WRITE)) - .flatMap(inst -> this.entityActivationService.setActive(inst, active)) - .getOrThrow(); + this.bulkActionService.doBulkAction(new BulkAction( + (active) ? Type.ACTIVATE : Type.DEACTIVATE, + EntityType.INSTITUTION, + new EntityKey(id, EntityType.INSTITUTION))); + + return this.institutionDAO.byId(id).getOrThrow(); } private Result _saveInstitution(final Institution institution, final PrivilegeType privilegeType) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java index e74a2308..888c6f6b 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/UserAccountController.java @@ -12,6 +12,7 @@ import java.util.Collection; import javax.validation.Valid; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import ch.ethz.seb.sebserver.gbl.model.EntityKey; import ch.ethz.seb.sebserver.gbl.model.EntityProcessingReport; import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.Page; @@ -29,10 +31,12 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport; import ch.ethz.seb.sebserver.webservice.servicelayer.PaginationService; -import ch.ethz.seb.sebserver.webservice.servicelayer.activation.EntityActivationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationGrantService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkAction.Type; +import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionService; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; @@ -48,7 +52,8 @@ public class UserAccountController { private final UserService userService; private final UserActivityLogDAO userActivityLogDAO; private final PaginationService paginationService; - private final EntityActivationService entityActivationService; + private final BulkActionService bulkActionService; + private final ApplicationEventPublisher applicationEventPublisher; public UserAccountController( final UserDAO userDao, @@ -56,14 +61,16 @@ public class UserAccountController { final UserService userService, final UserActivityLogDAO userActivityLogDAO, final PaginationService paginationService, - final EntityActivationService entityActivationService) { + final BulkActionService bulkActionService, + final ApplicationEventPublisher applicationEventPublisher) { this.userDao = userDao; this.authorizationGrantService = authorizationGrantService; this.userService = userService; this.userActivityLogDAO = userActivityLogDAO; this.paginationService = paginationService; - this.entityActivationService = entityActivationService; + this.bulkActionService = bulkActionService; + this.applicationEventPublisher = applicationEventPublisher; } @RequestMapping(method = RequestMethod.GET) @@ -155,42 +162,45 @@ public class UserAccountController { @RequestMapping(path = "/{uuid}/delete", method = RequestMethod.DELETE) public EntityProcessingReport deleteUser(@PathVariable final String uuid) { - return this.userDao.pkForUUID(uuid) - .flatMap(pk -> this.userDao.delete(pk, true)) - .flatMap(report -> this.userActivityLogDAO.log( - ActivityType.DELETE, - EntityType.USER, - uuid, - "soft-delete", - report)) - .getOrThrow(); + checkPrivilegeForUser(uuid, PrivilegeType.WRITE); + + return this.bulkActionService.createReport(new BulkAction( + Type.DEACTIVATE, + EntityType.USER, + new EntityKey(uuid, EntityType.USER, false))); } @RequestMapping(path = "/{uuid}/hard-delete", method = RequestMethod.DELETE) public EntityProcessingReport hardDeleteUser(@PathVariable final String uuid) { - return this.userDao.pkForUUID(uuid) - .flatMap(pk -> this.userDao.delete(pk, false)) - .flatMap(report -> this.userActivityLogDAO.log( - ActivityType.DELETE, - EntityType.USER, - uuid, - "hard-delete", - report)) - .getOrThrow(); + checkPrivilegeForUser(uuid, PrivilegeType.WRITE); + + return this.bulkActionService.createReport(new BulkAction( + Type.HARD_DELETE, + EntityType.USER, + new EntityKey(uuid, EntityType.USER, false))); } - @RequestMapping(path = "/{uuid}/relations", method = RequestMethod.GET) - public EntityProcessingReport getAllUserRelatedData(@PathVariable final String uuid) { - return this.userDao.getAllUserData(uuid) + private void checkPrivilegeForUser(final String uuid, final PrivilegeType type) { + this.authorizationGrantService.checkHasAnyPrivilege( + EntityType.USER, + type); + + this.userDao.byUuid(uuid) + .flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity( + userInfo, + type)) .getOrThrow(); } private UserInfo setActive(final String uuid, final boolean active) { + this.checkPrivilegeForUser(uuid, PrivilegeType.MODIFY); - return this.userDao.byUuid(uuid) - .flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity(userInfo, PrivilegeType.WRITE)) - .flatMap(userInfo -> this.entityActivationService.setActive(userInfo, active)) - .getOrThrow(); + this.bulkActionService.doBulkAction(new BulkAction( + (active) ? Type.ACTIVATE : Type.DEACTIVATE, + EntityType.USER, + new EntityKey(uuid, EntityType.USER, false))); + + return this.userDao.byUuid(uuid).getOrThrow(); } private Result _saveUser(final UserMod userData, final PrivilegeType privilegeType) { @@ -209,7 +219,7 @@ public class UserAccountController { private Result revokePassword(final UserMod userData, final UserInfo userInfo) { // handle password change; revoke access tokens if password has changed if (userData.passwordChangeRequest() && userData.newPasswordMatch()) { - this.entityActivationService.getApplicationEventPublisher().publishEvent( + this.applicationEventPublisher.publishEvent( new RevokeTokenEndpoint.RevokeTokenEvent(this, userInfo.username)); } return Result.of(userInfo);