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

View file

@ -32,6 +32,17 @@ import java.util.stream.Stream;
* }
* </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
* capability to report an error.
*
@ -107,7 +118,11 @@ public final class Result<T> {
* @return mapped Result of type U */
public <U> Result<U> map(final Function<? super T, ? extends U> 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<T> {
* @return mapped Result of type U */
public <U> Result<U> flatMap(final Function<? super T, Result<U>> 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<T> {
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.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<ExtractUserFromAuthenticationStrategy> extractStrategies;
private final ApplicationEventPublisher applicationEventPublisher;
public UserServiceImpl(
final Collection<ExtractUserFromAuthenticationStrategy> extractStrategies,
final ApplicationEventPublisher applicationEventPublisher) {
public UserServiceImpl(final Collection<ExtractUserFromAuthenticationStrategy> extractStrategies) {
this.extractStrategies = extractStrategies;
this.applicationEventPublisher = applicationEventPublisher;
}
@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;
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<T extends Entity> extends EntityDAO<T> {
*
* @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<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
* @return A Result refer to the Entity instance or refer to an error if happened */
Result<T> 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<Result<EntityKey>> setActive(Set<EntityKey> all, boolean active);
}

View file

@ -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<T extends Entity> {
* 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<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.
*
@ -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
* case */
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
@ -62,14 +65,12 @@ public interface EntityDAO<T extends Entity> {
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<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.
* 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());
}
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.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<UserInfo> {
*
* @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<Collection<UserInfo>> all(final UserFilter filter) {
return all(filter, userInfo -> true);
}
@ -80,11 +83,10 @@ public interface UserDAO extends ActivatableEntityDAO<UserInfo> {
* exception on error case */
Result<UserInfo> 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<EntityProcessingReport> getAllUserData(String uuid);
* @return a Collection containing EntityKey's of all entities that belongs to a given User */
Collection<EntityKey> getAllUserData(String uuid);
}

View file

@ -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<Collection<Institution>> allActive() {
return allMatching(null, true);
}
@Override
@Transactional(readOnly = true)
public Result<Collection<Institution>> all(final Predicate<Institution> predicate, final boolean onlyActive) {
public Result<Collection<Institution>> all(final Predicate<Institution> predicate, final Boolean active) {
return Result.tryCatch(() -> {
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<InstitutionRecord>>> example =
this.institutionRecordMapper.selectByExample();
final List<InstitutionRecord> records = (onlyActive)
? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true)))
final List<InstitutionRecord> 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<Institution> setActive(final String entityId, final boolean active) {
return Result.tryCatch(() -> {
final Long institutionId = Long.valueOf(entityId);
public Collection<Result<EntityKey>> setActive(final Set<EntityKey> all, final boolean active) {
final Collection<Result<EntityKey>> result = new ArrayList<>();
this.institutionRecordMapper.updateByPrimaryKeySelective(new InstitutionRecord(
institutionId, null, null, BooleanUtils.toInteger(active), null));
final List<Long> 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.<EntityKey> ofError(new RuntimeException(
"Activation failed on unexpected exception for Institution of id: " + id, e)))
.collect(Collectors.toList());
}
}
@Override
@Transactional
public Result<EntityProcessingReport> delete(final Long id, final boolean archive) {
// TODO Auto-generated method stub
return null;
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.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) {
@ -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(() -> {
final InstitutionRecord newRecord = new InstitutionRecord(
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)
.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<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;
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<EntityProcessingReport> delete(final Long id, final boolean archive) {
return Result.tryCatch(() -> {
final EntityProcessingReport report = new EntityProcessingReport();
public Collection<Result<EntityKey>> delete(final Set<EntityKey> all) {
final Collection<Result<EntityKey>> result = new ArrayList<>();
final UserActivityLog log = byId(id).getOrThrow();
this.userLogRecordMapper.deleteByPrimaryKey(id);
report.add(log);
final List<Long> 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.<EntityKey> 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<Collection<UserActivityLog>> all(
final Predicate<UserActivityLog> predicate,
final boolean onlyActive) {
public Result<Collection<UserActivityLog>> all(final Predicate<UserActivityLog> predicate, final Boolean active) {
return Result.tryCatch(() -> {
// 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 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<Collection<UserInfo>> allActive() {
return all(new UserFilter(null, null, null, null, true, null));
return all(ALL_ACTIVE_ONLY_FILTER);
}
@Override
@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(() -> {
final QueryExpressionDSL<MyBatis3SelectModelAdapter<List<UserRecord>>> example =
this.userRecordMapper.selectByExample();
final List<UserRecord> records = (onlyActive)
? example.where(UserRecordDynamicSqlSupport.active, isEqualTo(BooleanUtils.toInteger(true)))
final List<UserRecord> 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<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
@Transactional(readOnly = true)
public Result<EntityProcessingReport> 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.<EntityKey> ofError(new RuntimeException(
"Activation failed on unexpected exception for User of id: " + id, e)))
.collect(Collectors.toList());
}
}
@Override
@Transactional
public Result<UserInfo> setActive(final String entityId, final boolean active) {
return Result.tryCatch(() -> {
public Collection<Result<EntityKey>> delete(final Set<EntityKey> all) {
final Collection<Result<EntityKey>> 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<Long> 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.<EntityKey> ofError(new RuntimeException(
"Deletion failed on unexpected exception for User of id: " + id, 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) {
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<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 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<Institution> _saveInstitution(final Institution institution, final PrivilegeType privilegeType) {

View file

@ -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<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) {
// 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);