SEBSERV-15 #BulkActionService implementation, TODO reporting

This commit is contained in:
anhefti 2019-01-14 11:53:42 +01:00
parent ffeb663351
commit bb0269d3e8
19 changed files with 1062 additions and 332 deletions

View file

@ -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 + "]";
}
}

View file

@ -15,12 +15,14 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty; 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;
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.Domain.LMS_SETUP;
import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityType;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.GrantEntity; 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 { public enum LMSType {
MOCKUP, MOCKUP,
@ -67,6 +69,10 @@ public final class LmsSetup implements GrantEntity {
@Size(min = 8, max = 255, message = "lmsSetup:sebAuthSecret:size:{min}:{max}:${validatedValue}") @Size(min = 8, max = 255, message = "lmsSetup:sebAuthSecret:size:{min}:{max}:${validatedValue}")
public final String sebAuthSecret; public final String sebAuthSecret;
/** Indicates whether this LmsSetup is still active or not */
@JsonProperty(LMS_SETUP.ATTR_ACTIVE)
public final Boolean active;
@JsonCreator @JsonCreator
public LmsSetup( public LmsSetup(
@JsonProperty(Domain.ATTR_ID) final Long id, @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_URL) final String lmsApiUrl,
@JsonProperty(LMS_SETUP.ATTR_LMS_REST_API_TOKEN) final String lmsRestApiToken, @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_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.id = id;
this.institutionId = institutionId; this.institutionId = institutionId;
@ -90,6 +97,7 @@ public final class LmsSetup implements GrantEntity {
this.lmsRestApiToken = lmsRestApiToken; this.lmsRestApiToken = lmsRestApiToken;
this.sebAuthName = sebAuthName; this.sebAuthName = sebAuthName;
this.sebAuthSecret = sebAuthSecret; this.sebAuthSecret = sebAuthSecret;
this.active = active;
} }
@Override @Override
@ -107,6 +115,11 @@ public final class LmsSetup implements GrantEntity {
return this.id; return this.id;
} }
@Override
public boolean isActive() {
return this.active;
}
@JsonIgnore @JsonIgnore
@Override @Override
public String getModelId() { public String getModelId() {
@ -152,6 +165,10 @@ public final class LmsSetup implements GrantEntity {
return this.sebAuthSecret; return this.sebAuthSecret;
} }
public Boolean getActive() {
return this.active;
}
@Override @Override
public String toString() { public String toString() {
return "LmsSetup [id=" + this.id + ", institutionId=" + this.institutionId + ", name=" + this.name 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=" + ", lmsAuthName=" + this.lmsAuthName + ", lmsAuthSecret=" + this.lmsAuthSecret + ", lmsApiUrl="
+ this.lmsApiUrl + this.lmsApiUrl
+ ", lmsRestApiToken=" + this.lmsRestApiToken + ", sebAuthName=" + this.sebAuthName + ", sebAuthSecret=" + ", lmsRestApiToken=" + this.lmsRestApiToken + ", sebAuthName=" + this.sebAuthName + ", sebAuthSecret="
+ this.sebAuthSecret + "]"; + this.sebAuthSecret + ", active=" + this.active + "]";
} }
} }

View file

@ -32,6 +32,17 @@ import java.util.stream.Stream;
* } * }
* </pre> * </pre>
* *
* Or use the Results tryCatch that wraps the given code block in a try catch internally
*
* <pre>
* public Result<String> compute(String s1, String s2) {
* return Result.tryCatch(() -> {
* ... do some computation
* return result;
* });
* }
* </pre>
*
* If you are familiar with <code>java.util.Optional</code> think of Result like an Optional with the * If you are familiar with <code>java.util.Optional</code> think of Result like an Optional with the
* capability to report an error. * capability to report an error.
* *
@ -107,7 +118,11 @@ public final class Result<T> {
* @return mapped Result of type U */ * @return mapped Result of type U */
public <U> Result<U> map(final Function<? super T, ? extends U> mapf) { public <U> Result<U> map(final Function<? super T, ? extends U> mapf) {
if (this.error == null) { 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 { } else {
return Result.ofError(this.error); return Result.ofError(this.error);
} }
@ -126,7 +141,11 @@ public final class Result<T> {
* @return mapped Result of type U */ * @return mapped Result of type U */
public <U> Result<U> flatMap(final Function<? super T, Result<U>> mapf) { public <U> Result<U> flatMap(final Function<? super T, Result<U>> mapf) {
if (this.error == null) { if (this.error == null) {
return mapf.apply(this.value); try {
return mapf.apply(this.value);
} catch (final Throwable t) {
return Result.ofError(t);
}
} else { } else {
return Result.ofError(this.error); return Result.ofError(this.error);
} }
@ -215,4 +234,35 @@ public final class Result<T> {
return Stream.of(result.value); 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 + "]";
}
} }

View file

@ -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;
}
}

View file

@ -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<ActivatableEntityDAO<?>> activatableEntityDAOs;
private final ApplicationEventPublisher applicationEventPublisher;
private final UserActivityLogDAO userActivityLogDAO;
public EntityActivationService(
final Collection<ActivatableEntityDAO<?>> 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 <T extends Entity> Result<T> 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 <T extends Entity> Result<T> publishEvent(final T entity, final boolean activated) {
this.applicationEventPublisher.publishEvent(new EntityActivationEvent(entity, activated));
return Result.of(entity);
}
@SuppressWarnings("unchecked")
private <T extends Entity> ActivatableEntityDAO<T> getDAOForEntity(final T entity) {
for (final ActivatableEntityDAO<?> dao : this.activatableEntityDAOs) {
if (dao.entityType() == entity.entityType()) {
return (ActivatableEntityDAO<T>) dao;
}
}
return null;
}
}

View file

@ -13,7 +13,6 @@ import java.util.Collection;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
@ -37,14 +36,10 @@ public class UserServiceImpl implements UserService {
} }
private final Collection<ExtractUserFromAuthenticationStrategy> extractStrategies; private final Collection<ExtractUserFromAuthenticationStrategy> extractStrategies;
private final ApplicationEventPublisher applicationEventPublisher;
public UserServiceImpl( public UserServiceImpl(final Collection<ExtractUserFromAuthenticationStrategy> extractStrategies) {
final Collection<ExtractUserFromAuthenticationStrategy> extractStrategies,
final ApplicationEventPublisher applicationEventPublisher) {
this.extractStrategies = extractStrategies; this.extractStrategies = extractStrategies;
this.applicationEventPublisher = applicationEventPublisher;
} }
@Override @Override

View file

@ -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<EntityKey> sources;
final Set<EntityKey> dependencies;
final Set<Result<EntityKey>> result;
boolean alreadyProcessed = false;
public BulkAction(
final Type type,
final EntityType sourceType,
final Collection<EntityKey> 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<EntityKey> 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 + "]";
}
}

View file

@ -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<BulkActionSupport> supporter;
public BulkActionService(final Collection<BulkActionSupport> 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<BulkActionSupport> 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<BulkActionSupport> 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");
}
}
}

View file

@ -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<EntityKey> getDependencies(BulkAction bulkAction);
Collection<Result<EntityKey>> processBulkAction(BulkAction bulkAction);
}

View file

@ -9,8 +9,12 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao; package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; 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.Entity;
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
/** Interface of a DAO for an Entity that has activation feature. /** Interface of a DAO for an Entity that has activation feature.
@ -22,29 +26,16 @@ public interface ActivatableEntityDAO<T extends Entity> extends EntityDAO<T> {
* *
* @return A Result refer to a Collection of all active Entity instances for a concrete entity-domain * @return A Result refer to a Collection of all active Entity instances for a concrete entity-domain
* or refer to an error if happened */ * or refer to an error if happened */
Result<Collection<T>> allActive(); @Transactional(readOnly = true)
default Result<Collection<T>> 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 * @param active The active flag
* @return A Result refer to the Entity instance or refer to an error if happened */ * @return The Collection of Results refer to the EntityKey instance or refer to an error if happened */
Result<T> setActive(String entityId, boolean active); Collection<Result<EntityKey>> setActive(Set<EntityKey> all, 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);
} }

View file

@ -8,11 +8,14 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao; package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import ch.ethz.seb.sebserver.gbl.model.Entity; 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.EntityType;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -37,10 +40,10 @@ public interface EntityDAO<T extends Entity> {
* If you need a fast filtering implement a specific filtering in SQL level * If you need a fast filtering implement a specific filtering in SQL level
* *
* @param predicate Predicate expecting instance of type specific entity type * @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 * @return Result of Collection of Entity that matches a given predicate. Or an exception result on error
* case */ * case */
Result<Collection<T>> all(Predicate<T> predicate, boolean onlyActive); Result<Collection<T>> all(Predicate<T> predicate, Boolean active);
/** Use this to get a Collection of all entities of concrete type that matches a given predicate. /** Use this to get a Collection of all entities of concrete type that matches a given predicate.
* *
@ -52,7 +55,7 @@ public interface EntityDAO<T extends Entity> {
* @return Result of Collection of Entity that matches a given predicate. Or an exception result on error * @return Result of Collection of Entity that matches a given predicate. Or an exception result on error
* case */ * case */
default Result<Collection<T>> all(final Predicate<T> predicate) { default Result<Collection<T>> all(final Predicate<T> predicate) {
return all(predicate, false); return all(predicate, null);
} }
/** Use this to get a Collection of all active entities of concrete type /** Use this to get a Collection of all active entities of concrete type
@ -62,14 +65,12 @@ public interface EntityDAO<T extends Entity> {
return all(entity -> true); 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 all The Collection of EntityKey to delete
* @param archive indicates whether the Entity and all its relations should be archived (inactive and anonymous) or * @return Result of a collection of all entities that has been deleted or refer to an error if
* hard deleted
* @return Result of a collection of all entities that has been deleted (or archived) or refer to an error if
* happened */ * happened */
Result<EntityProcessingReport> delete(Long id, boolean archive); Collection<Result<EntityKey>> delete(Set<EntityKey> all);
/** Utility method to extract an expected single resource entry form a Collection of specified type. /** 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 * 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<T extends Entity> {
return Result.of(resources.iterator().next()); return Result.of(resources.iterator().next());
} }
default List<Long> extractIdsFromKeys(
final Collection<EntityKey> keys,
final Collection<Result<EntityKey>> result) {
final EntityType entityType = entityType();
final List<Long> 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;
}
} }

View file

@ -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<LmsSetup> {
@Transactional(readOnly = true)
default Result<Collection<LmsSetup>> allOfInstitution(final Long institutionId, final Boolean active) {
return allMatching(institutionId, null, null, active);
}
Result<Collection<LmsSetup>> allMatching(Long institutionId, String name, LMSType lmsType, Boolean active);
Result<LmsSetup> save(LmsSetup lmsSetup);
}

View file

@ -11,7 +11,9 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
import java.util.Collection; import java.util.Collection;
import java.util.function.Predicate; 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.UserFilter;
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo; import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
import ch.ethz.seb.sebserver.gbl.model.user.UserMod; import ch.ethz.seb.sebserver.gbl.model.user.UserMod;
@ -53,6 +55,7 @@ public interface UserDAO extends ActivatableEntityDAO<UserInfo> {
* *
* @param filter The UserFilter instance containing all filter criteria * @param filter The UserFilter instance containing all filter criteria
* @return a Result of Collection of filtered UserInfo. Or an exception result on error case */ * @return a Result of Collection of filtered UserInfo. Or an exception result on error case */
@Transactional(readOnly = true)
default Result<Collection<UserInfo>> all(final UserFilter filter) { default Result<Collection<UserInfo>> all(final UserFilter filter) {
return all(filter, userInfo -> true); return all(filter, userInfo -> true);
} }
@ -80,11 +83,10 @@ public interface UserDAO extends ActivatableEntityDAO<UserInfo> {
* exception on error case */ * exception on error case */
Result<UserInfo> save(UserMod userMod); Result<UserInfo> save(UserMod userMod);
/** Use this to get an EntityProcessingReport containing the user account entity itself /** Use this to get a Collection containing EntityKey's of all entities that belongs to a given User.
* and all user related data entities.
* *
* @param uuid The UUID of the user * @param uuid The UUID of the user
* @return an EntityProcessingReport containing all user related entity data */ * @return a Collection containing EntityKey's of all entities that belongs to a given User */
Result<EntityProcessingReport> getAllUserData(String uuid); Collection<EntityKey> getAllUserData(String uuid);
} }

View file

@ -9,9 +9,13 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl; package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl;
import static org.mybatis.dynamic.sql.SqlBuilder.isEqualTo; 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.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -23,8 +27,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional; 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.model.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityType;
import ch.ethz.seb.sebserver.gbl.model.institution.Institution; import ch.ethz.seb.sebserver.gbl.model.institution.Institution;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.InstitutionRecordMapper;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport; 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.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.InstitutionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ResourceNotFoundException;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.TransactionHandler;
@Lazy @Lazy
@Component @Component
public class InstitutionDAOImpl implements InstitutionDAO { public class InstitutionDAOImpl implements InstitutionDAO, BulkActionSupport {
private final InstitutionRecordMapper institutionRecordMapper; private final InstitutionRecordMapper institutionRecordMapper;
@ -61,19 +66,13 @@ public class InstitutionDAOImpl implements InstitutionDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<Institution>> allActive() { public Result<Collection<Institution>> all(final Predicate<Institution> predicate, final Boolean active) {
return allMatching(null, true);
}
@Override
@Transactional(readOnly = true)
public Result<Collection<Institution>> all(final Predicate<Institution> predicate, final boolean onlyActive) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<InstitutionRecord>>> example = final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<InstitutionRecord>>> example =
this.institutionRecordMapper.selectByExample(); this.institutionRecordMapper.selectByExample();
final List<InstitutionRecord> records = (onlyActive) final List<InstitutionRecord> records = (active != null)
? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true))) ? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(active)))
.build() .build()
.execute() .execute()
: example.build().execute(); : example.build().execute();
@ -113,42 +112,88 @@ public class InstitutionDAOImpl implements InstitutionDAO {
} }
return (institution.id != null) return (institution.id != null)
? updateUser(institution) ? update(institution)
.flatMap(InstitutionDAOImpl::toDomainModel) .flatMap(InstitutionDAOImpl::toDomainModel)
.onErrorDo(TransactionHandler::rollback) .onErrorDo(TransactionHandler::rollback)
: createNewUser(institution) : createNew(institution)
.flatMap(InstitutionDAOImpl::toDomainModel) .flatMap(InstitutionDAOImpl::toDomainModel)
.onErrorDo(TransactionHandler::rollback); .onErrorDo(TransactionHandler::rollback);
} }
@Override @Override
@Transactional @Transactional
public Result<Institution> setActive(final String entityId, final boolean active) { public Collection<Result<EntityKey>> setActive(final Set<EntityKey> all, final boolean active) {
return Result.tryCatch(() -> { final Collection<Result<EntityKey>> result = new ArrayList<>();
final Long institutionId = Long.valueOf(entityId);
this.institutionRecordMapper.updateByPrimaryKeySelective(new InstitutionRecord( final List<Long> ids = extractIdsFromKeys(all, result);
institutionId, null, null, BooleanUtils.toInteger(active), null)); final InstitutionRecord institutionRecord = new InstitutionRecord(
null, null, null, BooleanUtils.toInteger(active), null);
return this.institutionRecordMapper.selectByPrimaryKey(institutionId); try {
}).flatMap(InstitutionDAOImpl::toDomainModel); this.institutionRecordMapper.updateByExampleSelective(institutionRecord)
} .where(InstitutionRecordDynamicSqlSupport.id, isIn(ids))
.build()
.execute();
@Override return ids.stream()
public void notifyActivation(final Entity source) { .map(id -> Result.of(new EntityKey(id, EntityType.INSTITUTION)))
// No dependencies of activation on Institution .collect(Collectors.toList());
} } catch (final Exception e) {
return ids.stream()
@Override .map(id -> Result.<EntityKey> ofError(new RuntimeException(
public void notifyDeactivation(final Entity source) { "Activation failed on unexpected exception for Institution of id: " + id, e)))
// No dependencies of activation on Institution .collect(Collectors.toList());
}
} }
@Override @Override
@Transactional @Transactional
public Result<EntityProcessingReport> delete(final Long id, final boolean archive) { public Collection<Result<EntityKey>> delete(final Set<EntityKey> all) {
// TODO Auto-generated method stub final Collection<Result<EntityKey>> result = new ArrayList<>();
return null;
final List<Long> 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.<EntityKey> ofError(new RuntimeException(
"Deletion failed on unexpected exception for Institution of id: " + id, e)))
.collect(Collectors.toList());
}
}
@Override
@Transactional(readOnly = true)
public Set<EntityKey> 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<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) {
final Set<EntityKey> 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<InstitutionRecord> recordById(final Long id) { private Result<InstitutionRecord> recordById(final Long id) {
@ -163,7 +208,7 @@ public class InstitutionDAOImpl implements InstitutionDAO {
}); });
} }
private Result<InstitutionRecord> createNewUser(final Institution institution) { private Result<InstitutionRecord> createNew(final Institution institution) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final InstitutionRecord newRecord = new InstitutionRecord( final InstitutionRecord newRecord = new InstitutionRecord(
null, null,
@ -177,9 +222,9 @@ public class InstitutionDAOImpl implements InstitutionDAO {
}); });
} }
private Result<InstitutionRecord> updateUser(final Institution institution) { private Result<InstitutionRecord> update(final Institution institution) {
return recordById(institution.id) return recordById(institution.id)
.flatMap(record -> Result.tryCatch(() -> { .map(record -> {
final InstitutionRecord newRecord = new InstitutionRecord( final InstitutionRecord newRecord = new InstitutionRecord(
institution.id, institution.id,
@ -190,7 +235,7 @@ public class InstitutionDAOImpl implements InstitutionDAO {
this.institutionRecordMapper.updateByPrimaryKeySelective(newRecord); this.institutionRecordMapper.updateByPrimaryKeySelective(newRecord);
return this.institutionRecordMapper.selectByPrimaryKey(institution.id); return this.institutionRecordMapper.selectByPrimaryKey(institution.id);
})); });
} }
private static Result<Institution> toDomainModel(final InstitutionRecord record) { private static Result<Institution> toDomainModel(final InstitutionRecord record) {

View file

@ -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<LmsSetup> byId(final Long id) {
return recordById(id)
.flatMap(LmsSetupDAOImpl::toDomainModel);
}
@Override
@Transactional(readOnly = true)
public Result<Collection<LmsSetup>> all(final Predicate<LmsSetup> predicate, final Boolean active) {
return Result.tryCatch(() -> {
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<LmsSetupRecord>>> example =
this.lmsSetupRecordMapper.selectByExample();
final List<LmsSetupRecord> 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<Collection<LmsSetup>> 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<LmsSetup> 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<Result<EntityKey>> setActive(final Set<EntityKey> all, final boolean active) {
final Collection<Result<EntityKey>> result = new ArrayList<>();
final List<Long> 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.<EntityKey> ofError(new RuntimeException(
"Activation failed on unexpected exception for LmsSetup of id: " + id, e)))
.collect(Collectors.toList());
}
}
@Override
@Transactional
public Collection<Result<EntityKey>> delete(final Set<EntityKey> all) {
final Collection<Result<EntityKey>> result = new ArrayList<>();
final List<Long> 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.<EntityKey> ofError(new RuntimeException(
"Deletion failed on unexpected exception for LmsSetup of id: " + id, e)))
.collect(Collectors.toList());
}
}
@Override
@Transactional(readOnly = true)
public Set<EntityKey> getDependencies(final BulkAction bulkAction) {
// all of institution
if (bulkAction.sourceType == EntityType.INSTITUTION) {
final Set<EntityKey> 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<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) {
final Set<EntityKey> 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<LmsSetupRecord> 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<LmsSetup> 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<LmsSetupRecord> 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<LmsSetupRecord> 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);
});
}
}

View file

@ -8,8 +8,12 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.dao.impl; 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.Collection;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -22,7 +26,7 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import ch.ethz.seb.sebserver.gbl.model.Entity; 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.EntityType;
import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog; import ch.ethz.seb.sebserver.gbl.model.user.UserActivityLog;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
@ -176,16 +180,26 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
@Override @Override
@Transactional @Transactional
public Result<EntityProcessingReport> delete(final Long id, final boolean archive) { public Collection<Result<EntityKey>> delete(final Set<EntityKey> all) {
return Result.tryCatch(() -> { final Collection<Result<EntityKey>> result = new ArrayList<>();
final EntityProcessingReport report = new EntityProcessingReport();
final UserActivityLog log = byId(id).getOrThrow(); final List<Long> ids = extractIdsFromKeys(all, result);
this.userLogRecordMapper.deleteByPrimaryKey(id);
report.add(log);
return report; try {
}).onErrorDo(TransactionHandler::rollback); 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.<EntityKey> ofError(new RuntimeException(
"Deletion failed on unexpected exception for UserActivityLog of id: " + id, e)))
.collect(Collectors.toList());
}
} }
@Override @Override
@ -256,9 +270,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<UserActivityLog>> all( public Result<Collection<UserActivityLog>> all(final Predicate<UserActivityLog> predicate, final Boolean active) {
final Predicate<UserActivityLog> predicate,
final boolean onlyActive) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
// first check if there is a page limitation set. Otherwise set the default // first check if there is a page limitation set. Otherwise set the default

View file

@ -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 ch.ethz.seb.sebserver.gbl.util.Utils.toSQLWildcard;
import static org.mybatis.dynamic.sql.SqlBuilder.*; import static org.mybatis.dynamic.sql.SqlBuilder.*;
import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; 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.WebSecurityConfig;
import ch.ethz.seb.sebserver.gbl.model.APIMessage.APIMessageException; 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.APIMessage.ErrorMessage;
import ch.ethz.seb.sebserver.gbl.model.Entity; 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.EntityType;
import ch.ethz.seb.sebserver.gbl.model.user.UserFilter; 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.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.RoleRecord;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.model.UserRecord; 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.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.TransactionHandler;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
@Lazy @Lazy
@Component @Component
public class UserDaoImpl implements UserDAO { public class UserDaoImpl implements UserDAO, BulkActionSupport {
private static final Logger log = LoggerFactory.getLogger(UserDaoImpl.class); 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 UserRecordMapper userRecordMapper;
private final RoleRecordMapper roleRecordMapper; private final RoleRecordMapper roleRecordMapper;
@ -117,18 +121,18 @@ public class UserDaoImpl implements UserDAO {
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<UserInfo>> allActive() { public Result<Collection<UserInfo>> allActive() {
return all(new UserFilter(null, null, null, null, true, null)); return all(ALL_ACTIVE_ONLY_FILTER);
} }
@Override @Override
@Transactional(readOnly = true) @Transactional(readOnly = true)
public Result<Collection<UserInfo>> all(final Predicate<UserInfo> predicate, final boolean onlyActive) { public Result<Collection<UserInfo>> all(final Predicate<UserInfo> predicate, final Boolean active) {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<UserRecord>>> example = final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<UserRecord>>> example =
this.userRecordMapper.selectByExample(); this.userRecordMapper.selectByExample();
final List<UserRecord> records = (onlyActive) final List<UserRecord> records = (active != null)
? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true))) ? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(active)))
.build() .build()
.execute() .execute()
: example.build().execute(); : example.build().execute();
@ -185,77 +189,136 @@ public class UserDaoImpl implements UserDAO {
@Override @Override
@Transactional @Transactional
public Result<EntityProcessingReport> delete(final Long id, final boolean force) { public Collection<Result<EntityKey>> setActive(final Set<EntityKey> all, final boolean active) {
final Collection<Result<EntityKey>> result = new ArrayList<>();
// TODO clarify within discussion about deactivate, archive and delete user related data final List<Long> 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 return ids.stream()
@Transactional(readOnly = true) .map(id -> Result.of(new EntityKey(id, EntityType.USER)))
public Result<EntityProcessingReport> getAllUserData(final String uuid) { .collect(Collectors.toList());
} catch (final Exception e) {
// TODO return ids.stream()
.map(id -> Result.<EntityKey> ofError(new RuntimeException(
return Result.ofError(new RuntimeException("TODO")); "Activation failed on unexpected exception for User of id: " + id, e)))
.collect(Collectors.toList());
}
} }
@Override @Override
@Transactional @Transactional
public Result<UserInfo> setActive(final String entityId, final boolean active) { public Collection<Result<EntityKey>> delete(final Set<EntityKey> all) {
return Result.tryCatch(() -> { final Collection<Result<EntityKey>> result = new ArrayList<>();
return this.userRecordMapper.updateByExampleSelective( final List<Long> ids = extractIdsFromKeys(all, result);
new UserRecord(
null, null, null, null, null, null, null, null, null,
BooleanUtils.toIntegerObject(active)))
.where(UserRecordDynamicSqlSupport.uuid, isEqualTo(entityId))
.build()
.execute();
}).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 { try {
this.userRecordMapper.deleteByExample()
final UserRecord record = new UserRecord( .where(UserRecordDynamicSqlSupport.id, isIn(ids))
null, null, null, null, null, null, null, null, null,
BooleanUtils.toIntegerObject(active));
this.userRecordMapper.updateByExampleSelective(record)
.where(UserRecordDynamicSqlSupport.institutionId, isEqualTo(institutionId))
.build() .build()
.execute(); .execute();
return ids.stream()
.map(id -> Result.of(new EntityKey(id, EntityType.USER)))
.collect(Collectors.toList());
} catch (final Exception e) { } catch (final Exception e) {
log.error("Unexpected error while trying to set all active: {} for institution: {}", return ids.stream()
active, .map(id -> Result.<EntityKey> ofError(new RuntimeException(
institutionId, "Deletion failed on unexpected exception for User of id: " + id, e)))
e); .collect(Collectors.toList());
}
}
@Override
@Transactional(readOnly = true)
public Collection<EntityKey> getAllUserData(final String uuid) {
// TODO
return Collections.emptyList();
}
@Override
@Transactional(readOnly = true)
public Set<EntityKey> getDependencies(final BulkAction bulkAction) {
// all of institution
if (bulkAction.sourceType == EntityType.INSTITUTION) {
final Set<EntityKey> 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<Result<EntityKey>> processBulkAction(final BulkAction bulkAction) {
final Set<EntityKey> 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<Long> extractIdsFromKeys(
final Collection<EntityKey> keys,
final Collection<Result<EntityKey>> result) {
if (keys == null || keys.isEmpty() || keys.iterator().next().isIdPK) {
return UserDAO.super.extractIdsFromKeys(keys, result);
} else {
final List<String> 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<UserRecord> updateUser(final UserMod userMod) { private Result<UserRecord> updateUser(final UserMod userMod) {
return recordByUUID(userMod.uuid) return recordByUUID(userMod.uuid)
.flatMap(record -> Result.tryCatch(() -> { .map(record -> {
final boolean changePWD = userMod.passwordChangeRequest(); final boolean changePWD = userMod.passwordChangeRequest();
if (changePWD && !userMod.newPasswordMatch()) { if (changePWD && !userMod.newPasswordMatch()) {
throw new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH); throw new APIMessageException(ErrorMessage.PASSWORD_MISSMATCH);
@ -276,7 +339,7 @@ public class UserDaoImpl implements UserDAO {
this.userRecordMapper.updateByPrimaryKeySelective(newRecord); this.userRecordMapper.updateByPrimaryKeySelective(newRecord);
updateRolesForUser(record.getId(), userMod.roles); updateRolesForUser(record.getId(), userMod.roles);
return this.userRecordMapper.selectByPrimaryKey(record.getId()); return this.userRecordMapper.selectByPrimaryKey(record.getId());
})); });
} }
private Result<UserRecord> createNewUser(final UserMod userMod) { private Result<UserRecord> createNewUser(final UserMod userMod) {

View file

@ -22,16 +22,19 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import ch.ethz.seb.sebserver.gbl.model.EntityIdAndName; 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.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityType;
import ch.ethz.seb.sebserver.gbl.model.institution.Institution; import ch.ethz.seb.sebserver.gbl.model.institution.Institution;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Result; 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.AuthorizationGrantService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType; 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.SEBServerUser;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; 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.InstitutionDAO;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO; 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.UserActivityLogDAO.ActivityType;
@ -45,19 +48,19 @@ public class InstitutionController {
private final AuthorizationGrantService authorizationGrantService; private final AuthorizationGrantService authorizationGrantService;
private final UserService userService; private final UserService userService;
private final UserActivityLogDAO userActivityLogDAO; private final UserActivityLogDAO userActivityLogDAO;
private final EntityActivationService entityActivationService; private final BulkActionService bulkActionService;
public InstitutionController( public InstitutionController(
final InstitutionDAO institutionDAO, final InstitutionDAO institutionDAO,
final AuthorizationGrantService authorizationGrantService, final AuthorizationGrantService authorizationGrantService,
final UserService userService, final UserActivityLogDAO userActivityLogDAO, final UserService userService, final UserActivityLogDAO userActivityLogDAO,
final EntityActivationService entityActivationService) { final BulkActionService bulkActionService) {
this.institutionDAO = institutionDAO; this.institutionDAO = institutionDAO;
this.authorizationGrantService = authorizationGrantService; this.authorizationGrantService = authorizationGrantService;
this.userService = userService; this.userService = userService;
this.userActivityLogDAO = userActivityLogDAO; this.userActivityLogDAO = userActivityLogDAO;
this.entityActivationService = entityActivationService; this.bulkActionService = bulkActionService;
} }
@RequestMapping(path = "/self", method = RequestMethod.GET) @RequestMapping(path = "/self", method = RequestMethod.GET)
@ -151,37 +154,45 @@ public class InstitutionController {
@RequestMapping(path = "/{id}/delete", method = RequestMethod.DELETE) @RequestMapping(path = "/{id}/delete", method = RequestMethod.DELETE)
public EntityProcessingReport deleteUser(@PathVariable final Long id) { public EntityProcessingReport deleteUser(@PathVariable final Long id) {
return this.institutionDAO.delete(id, true) checkPrivilegeForInstitution(id, PrivilegeType.WRITE);
.flatMap(report -> this.userActivityLogDAO.log(
ActivityType.DELETE, return this.bulkActionService.createReport(new BulkAction(
EntityType.INSTITUTION, Type.DEACTIVATE,
String.valueOf(id), EntityType.INSTITUTION,
"soft-delete", new EntityKey(id, EntityType.INSTITUTION)));
report)) }
@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(); .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) { private Institution setActive(final Long id, final boolean active) {
checkPrivilegeForInstitution(id, PrivilegeType.MODIFY);
return this.institutionDAO this.bulkActionService.doBulkAction(new BulkAction(
.byId(id) (active) ? Type.ACTIVATE : Type.DEACTIVATE,
.flatMap(inst -> this.authorizationGrantService.checkGrantOnEntity(inst, PrivilegeType.WRITE)) EntityType.INSTITUTION,
.flatMap(inst -> this.entityActivationService.setActive(inst, active)) new EntityKey(id, EntityType.INSTITUTION)));
.getOrThrow();
return this.institutionDAO.byId(id).getOrThrow();
} }
private Result<Institution> _saveInstitution(final Institution institution, final PrivilegeType privilegeType) { private Result<Institution> _saveInstitution(final Institution institution, final PrivilegeType privilegeType) {

View file

@ -12,6 +12,7 @@ import java.util.Collection;
import javax.validation.Valid; import javax.validation.Valid;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; 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.RequestParam;
import org.springframework.web.bind.annotation.RestController; 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.EntityProcessingReport;
import ch.ethz.seb.sebserver.gbl.model.EntityType; import ch.ethz.seb.sebserver.gbl.model.EntityType;
import ch.ethz.seb.sebserver.gbl.model.Page; 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.gbl.util.Result;
import ch.ethz.seb.sebserver.webservice.datalayer.batis.mapper.UserRecordDynamicSqlSupport; 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.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.AuthorizationGrantService;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.PrivilegeType; 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.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;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserActivityLogDAO.ActivityType;
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.dao.UserDAO;
@ -48,7 +52,8 @@ public class UserAccountController {
private final UserService userService; private final UserService userService;
private final UserActivityLogDAO userActivityLogDAO; private final UserActivityLogDAO userActivityLogDAO;
private final PaginationService paginationService; private final PaginationService paginationService;
private final EntityActivationService entityActivationService; private final BulkActionService bulkActionService;
private final ApplicationEventPublisher applicationEventPublisher;
public UserAccountController( public UserAccountController(
final UserDAO userDao, final UserDAO userDao,
@ -56,14 +61,16 @@ public class UserAccountController {
final UserService userService, final UserService userService,
final UserActivityLogDAO userActivityLogDAO, final UserActivityLogDAO userActivityLogDAO,
final PaginationService paginationService, final PaginationService paginationService,
final EntityActivationService entityActivationService) { final BulkActionService bulkActionService,
final ApplicationEventPublisher applicationEventPublisher) {
this.userDao = userDao; this.userDao = userDao;
this.authorizationGrantService = authorizationGrantService; this.authorizationGrantService = authorizationGrantService;
this.userService = userService; this.userService = userService;
this.userActivityLogDAO = userActivityLogDAO; this.userActivityLogDAO = userActivityLogDAO;
this.paginationService = paginationService; this.paginationService = paginationService;
this.entityActivationService = entityActivationService; this.bulkActionService = bulkActionService;
this.applicationEventPublisher = applicationEventPublisher;
} }
@RequestMapping(method = RequestMethod.GET) @RequestMapping(method = RequestMethod.GET)
@ -155,42 +162,45 @@ public class UserAccountController {
@RequestMapping(path = "/{uuid}/delete", method = RequestMethod.DELETE) @RequestMapping(path = "/{uuid}/delete", method = RequestMethod.DELETE)
public EntityProcessingReport deleteUser(@PathVariable final String uuid) { public EntityProcessingReport deleteUser(@PathVariable final String uuid) {
return this.userDao.pkForUUID(uuid) checkPrivilegeForUser(uuid, PrivilegeType.WRITE);
.flatMap(pk -> this.userDao.delete(pk, true))
.flatMap(report -> this.userActivityLogDAO.log( return this.bulkActionService.createReport(new BulkAction(
ActivityType.DELETE, Type.DEACTIVATE,
EntityType.USER, EntityType.USER,
uuid, new EntityKey(uuid, EntityType.USER, false)));
"soft-delete",
report))
.getOrThrow();
} }
@RequestMapping(path = "/{uuid}/hard-delete", method = RequestMethod.DELETE) @RequestMapping(path = "/{uuid}/hard-delete", method = RequestMethod.DELETE)
public EntityProcessingReport hardDeleteUser(@PathVariable final String uuid) { public EntityProcessingReport hardDeleteUser(@PathVariable final String uuid) {
return this.userDao.pkForUUID(uuid) checkPrivilegeForUser(uuid, PrivilegeType.WRITE);
.flatMap(pk -> this.userDao.delete(pk, false))
.flatMap(report -> this.userActivityLogDAO.log( return this.bulkActionService.createReport(new BulkAction(
ActivityType.DELETE, Type.HARD_DELETE,
EntityType.USER, EntityType.USER,
uuid, new EntityKey(uuid, EntityType.USER, false)));
"hard-delete",
report))
.getOrThrow();
} }
@RequestMapping(path = "/{uuid}/relations", method = RequestMethod.GET) private void checkPrivilegeForUser(final String uuid, final PrivilegeType type) {
public EntityProcessingReport getAllUserRelatedData(@PathVariable final String uuid) { this.authorizationGrantService.checkHasAnyPrivilege(
return this.userDao.getAllUserData(uuid) EntityType.USER,
type);
this.userDao.byUuid(uuid)
.flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity(
userInfo,
type))
.getOrThrow(); .getOrThrow();
} }
private UserInfo setActive(final String uuid, final boolean active) { private UserInfo setActive(final String uuid, final boolean active) {
this.checkPrivilegeForUser(uuid, PrivilegeType.MODIFY);
return this.userDao.byUuid(uuid) this.bulkActionService.doBulkAction(new BulkAction(
.flatMap(userInfo -> this.authorizationGrantService.checkGrantOnEntity(userInfo, PrivilegeType.WRITE)) (active) ? Type.ACTIVATE : Type.DEACTIVATE,
.flatMap(userInfo -> this.entityActivationService.setActive(userInfo, active)) EntityType.USER,
.getOrThrow(); new EntityKey(uuid, EntityType.USER, false)));
return this.userDao.byUuid(uuid).getOrThrow();
} }
private Result<UserInfo> _saveUser(final UserMod userData, final PrivilegeType privilegeType) { private Result<UserInfo> _saveUser(final UserMod userData, final PrivilegeType privilegeType) {
@ -209,7 +219,7 @@ public class UserAccountController {
private Result<UserInfo> revokePassword(final UserMod userData, final UserInfo userInfo) { private Result<UserInfo> revokePassword(final UserMod userData, final UserInfo userInfo) {
// handle password change; revoke access tokens if password has changed // handle password change; revoke access tokens if password has changed
if (userData.passwordChangeRequest() && userData.newPasswordMatch()) { if (userData.passwordChangeRequest() && userData.newPasswordMatch()) {
this.entityActivationService.getApplicationEventPublisher().publishEvent( this.applicationEventPublisher.publishEvent(
new RevokeTokenEndpoint.RevokeTokenEvent(this, userInfo.username)); new RevokeTokenEndpoint.RevokeTokenEvent(this, userInfo.username));
} }
return Result.of(userInfo); return Result.of(userInfo);