more javadoc
This commit is contained in:
parent
d404498475
commit
6e020b8e6f
31 changed files with 205 additions and 57 deletions
|
@ -13,18 +13,29 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.API;
|
import ch.ethz.seb.sebserver.gbl.api.API;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
|
||||||
|
/** Defines generic interface for all types of Entity. */
|
||||||
public interface Entity extends ModelIdAware {
|
public interface Entity extends ModelIdAware {
|
||||||
|
|
||||||
public static final String FILTER_ATTR_INSTITUTION = API.PARAM_INSTITUTION_ID;
|
public static final String FILTER_ATTR_INSTITUTION = API.PARAM_INSTITUTION_ID;
|
||||||
public static final String FILTER_ATTR_ACTIVE = "active";
|
public static final String FILTER_ATTR_ACTIVE = "active";
|
||||||
public static final String FILTER_ATTR_NAME = "name";
|
public static final String FILTER_ATTR_NAME = "name";
|
||||||
|
|
||||||
|
/** Get the type of the entity.
|
||||||
|
*
|
||||||
|
* @return the type of the entity */
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
EntityType entityType();
|
EntityType entityType();
|
||||||
|
|
||||||
|
/** Get the name of the entity
|
||||||
|
*
|
||||||
|
* @return the name of the entity */
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
String getName();
|
String getName();
|
||||||
|
|
||||||
|
/** Get an unique EntityKey for the entity consisting of the model identifier of the entity
|
||||||
|
* and the type of the entity.
|
||||||
|
*
|
||||||
|
* @return unique EntityKey for the entity */
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
default EntityKey getEntityKey() {
|
default EntityKey getEntityKey() {
|
||||||
final String modelId = getModelId();
|
final String modelId = getModelId();
|
||||||
|
@ -34,6 +45,10 @@ public interface Entity extends ModelIdAware {
|
||||||
return new EntityKey(modelId, entityType());
|
return new EntityKey(modelId, entityType());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates an EntityName instance from a given Entity.
|
||||||
|
*
|
||||||
|
* @param entity The Entity instance
|
||||||
|
* @return EntityName instance created form given Entity */
|
||||||
public static EntityName toName(final Entity entity) {
|
public static EntityName toName(final Entity entity) {
|
||||||
return new EntityName(
|
return new EntityName(
|
||||||
entity.entityType(),
|
entity.entityType(),
|
||||||
|
|
|
@ -17,17 +17,25 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
|
|
||||||
|
/** A EntityKey uniquely identifies a domain entity within the SEB Server's domain model.
|
||||||
|
* A EntityKey consists of the model identifier of a domain entity and the type of the entity. */
|
||||||
public class EntityKey implements Serializable {
|
public class EntityKey implements Serializable {
|
||||||
|
|
||||||
private static final long serialVersionUID = -2368065921846821061L;
|
private static final long serialVersionUID = -2368065921846821061L;
|
||||||
|
|
||||||
|
/** The model identifier of the entity */
|
||||||
@JsonProperty(value = "modelId", required = true)
|
@JsonProperty(value = "modelId", required = true)
|
||||||
@NotNull
|
@NotNull
|
||||||
public final String modelId;
|
public final String modelId;
|
||||||
|
|
||||||
|
/** The type of the entity */
|
||||||
@JsonProperty(value = "entityType", required = true)
|
@JsonProperty(value = "entityType", required = true)
|
||||||
@NotNull
|
@NotNull
|
||||||
public final EntityType entityType;
|
public final EntityType entityType;
|
||||||
|
|
||||||
|
/** pre-calculated hash value. Since EntityKey is fully immutable this is a valid optimization */
|
||||||
|
private final int hash;
|
||||||
|
|
||||||
@JsonCreator
|
@JsonCreator
|
||||||
public EntityKey(
|
public EntityKey(
|
||||||
@JsonProperty(value = "modelId", required = true) final String modelId,
|
@JsonProperty(value = "modelId", required = true) final String modelId,
|
||||||
|
@ -42,31 +50,41 @@ public class EntityKey implements Serializable {
|
||||||
|
|
||||||
this.modelId = modelId;
|
this.modelId = modelId;
|
||||||
this.entityType = entityType;
|
this.entityType = entityType;
|
||||||
|
|
||||||
|
final int prime = 31;
|
||||||
|
int result = 1;
|
||||||
|
result = prime * result + ((this.entityType == null) ? 0 : this.entityType.hashCode());
|
||||||
|
result = prime * result + ((this.modelId == null) ? 0 : this.modelId.hashCode());
|
||||||
|
this.hash = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EntityKey(
|
public EntityKey(
|
||||||
final Long pk,
|
final Long pk,
|
||||||
final EntityType entityType) {
|
final EntityType entityType) {
|
||||||
|
|
||||||
this.modelId = String.valueOf(pk);
|
this(String.valueOf(pk), entityType);
|
||||||
this.entityType = entityType;
|
if (pk == null) {
|
||||||
|
throw new IllegalArgumentException("modelId has null reference");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the model identifier of this EntityKey
|
||||||
|
*
|
||||||
|
* @return the model identifier of this EntityKey */
|
||||||
public String getModelId() {
|
public String getModelId() {
|
||||||
return this.modelId;
|
return this.modelId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the entity type EntityKey
|
||||||
|
*
|
||||||
|
* @return the model identifier of this EntityKey */
|
||||||
public EntityType getEntityType() {
|
public EntityType getEntityType() {
|
||||||
return this.entityType;
|
return this.entityType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
final int prime = 31;
|
return this.hash;
|
||||||
int result = 1;
|
|
||||||
result = prime * result + ((this.entityType == null) ? 0 : this.entityType.hashCode());
|
|
||||||
result = prime * result + ((this.modelId == null) ? 0 : this.modelId.hashCode());
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -10,8 +10,12 @@ package ch.ethz.seb.sebserver.gbl.model;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
|
||||||
|
/** Interface for all domain model objects that has a model identifier */
|
||||||
public interface ModelIdAware {
|
public interface ModelIdAware {
|
||||||
|
|
||||||
|
/** Get the model identifier of the domain model object
|
||||||
|
*
|
||||||
|
* @return the model identifier of the domain model object */
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
String getModelId();
|
String getModelId();
|
||||||
|
|
||||||
|
|
|
@ -84,6 +84,12 @@ public final class Result<T> {
|
||||||
return this.error != null ? errorHandler.apply(this.error) : this.value;
|
return this.error != null ? errorHandler.apply(this.error) : this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Use this to get the referenced result element or on error case, use the given error handler
|
||||||
|
* to handle the error and use a given supplier to get an alternative element for further processing
|
||||||
|
*
|
||||||
|
* @param errorHandler the error handler to handle an error if happened
|
||||||
|
* @param supplier supplies an alternative result element on error case
|
||||||
|
* @return returns the referenced result element or the alternative element given by the supplier on error */
|
||||||
public T get(final Consumer<Throwable> errorHandler, final Supplier<T> supplier) {
|
public T get(final Consumer<Throwable> errorHandler, final Supplier<T> supplier) {
|
||||||
if (this.error != null) {
|
if (this.error != null) {
|
||||||
errorHandler.accept(this.error);
|
errorHandler.accept(this.error);
|
||||||
|
@ -93,6 +99,9 @@ public final class Result<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Apply a given error handler that consumes the error if there is one.
|
||||||
|
*
|
||||||
|
* @param errorHandler the error handler */
|
||||||
public void handleError(final Consumer<Throwable> errorHandler) {
|
public void handleError(final Consumer<Throwable> errorHandler) {
|
||||||
if (this.error != null) {
|
if (this.error != null) {
|
||||||
errorHandler.accept(this.error);
|
errorHandler.accept(this.error);
|
||||||
|
@ -120,6 +129,9 @@ public final class Result<T> {
|
||||||
return this.value;
|
return this.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get the referenced result element or in error case, throws the referenced error
|
||||||
|
*
|
||||||
|
* @return the referenced result element */
|
||||||
public T getOrThrow() {
|
public T getOrThrow() {
|
||||||
if (this.error != null) {
|
if (this.error != null) {
|
||||||
if (this.error instanceof RuntimeException) {
|
if (this.error instanceof RuntimeException) {
|
||||||
|
@ -145,6 +157,9 @@ public final class Result<T> {
|
||||||
return this.error;
|
return this.error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Indicates whether this Result refers to an error or not.
|
||||||
|
*
|
||||||
|
* @return true if this Result refers to an error */
|
||||||
public boolean hasError() {
|
public boolean hasError() {
|
||||||
return this.error != null;
|
return this.error != null;
|
||||||
}
|
}
|
||||||
|
@ -210,9 +225,14 @@ public final class Result<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Result<T> onErrorDo(final Consumer<Throwable> block) {
|
/** Uses a given error handler to apply an error if there is one and returning itself again
|
||||||
|
* for further processing.
|
||||||
|
*
|
||||||
|
* @param errorHandler the error handler
|
||||||
|
* @return self reference */
|
||||||
|
public Result<T> onErrorDo(final Consumer<Throwable> errorHandler) {
|
||||||
if (this.error != null) {
|
if (this.error != null) {
|
||||||
block.accept(this.error);
|
errorHandler.accept(this.error);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,12 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.gbl.util;
|
package ch.ethz.seb.sebserver.gbl.util;
|
||||||
|
|
||||||
|
/** A tuple of two elements of the same type */
|
||||||
public class Tuple<T> {
|
public class Tuple<T> {
|
||||||
|
|
||||||
|
/** The first element of the tuple */
|
||||||
public final T _1;
|
public final T _1;
|
||||||
|
/** The second element of the tuple */
|
||||||
public final T _2;
|
public final T _2;
|
||||||
|
|
||||||
public Tuple(final T _1, final T _2) {
|
public Tuple(final T _1, final T _2) {
|
||||||
|
|
|
@ -29,12 +29,20 @@ import ch.ethz.seb.sebserver.gbl.Constants;
|
||||||
|
|
||||||
public final class Utils {
|
public final class Utils {
|
||||||
|
|
||||||
|
/** Get an immutable List from a Collection of elements
|
||||||
|
*
|
||||||
|
* @param collection Collection of elements
|
||||||
|
* @return immutable List */
|
||||||
public static <T> List<T> immutableListOf(final Collection<T> collection) {
|
public static <T> List<T> immutableListOf(final Collection<T> collection) {
|
||||||
return (collection != null)
|
return (collection != null)
|
||||||
? Collections.unmodifiableList(new ArrayList<>(collection))
|
? Collections.unmodifiableList(new ArrayList<>(collection))
|
||||||
: Collections.emptyList();
|
: Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a immutable Collection from a Collection of elements
|
||||||
|
*
|
||||||
|
* @param collection Collection of elements
|
||||||
|
* @return immutable Collection */
|
||||||
public static <T> Collection<T> immutableCollectionOf(final Collection<T> collection) {
|
public static <T> Collection<T> immutableCollectionOf(final Collection<T> collection) {
|
||||||
return (collection != null)
|
return (collection != null)
|
||||||
? Collections.unmodifiableCollection(collection)
|
? Collections.unmodifiableCollection(collection)
|
||||||
|
@ -53,10 +61,18 @@ public final class Utils {
|
||||||
return Collections.unmodifiableCollection(Arrays.asList(values));
|
return Collections.unmodifiableCollection(Arrays.asList(values));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a immutable Set from a Collection of elements
|
||||||
|
*
|
||||||
|
* @param collection Collection of elements
|
||||||
|
* @return immutable Set */
|
||||||
public static <T> Set<T> immutableSetOf(final Collection<T> collection) {
|
public static <T> Set<T> immutableSetOf(final Collection<T> collection) {
|
||||||
return immutableSetOf(new HashSet<>(collection));
|
return immutableSetOf(new HashSet<>(collection));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a immutable Set from a Set of elements
|
||||||
|
*
|
||||||
|
* @param set Set of elements
|
||||||
|
* @return immutable Set */
|
||||||
public static <T> Set<T> immutableSetOf(final Set<T> set) {
|
public static <T> Set<T> immutableSetOf(final Set<T> set) {
|
||||||
return (set != null)
|
return (set != null)
|
||||||
? Collections.unmodifiableSet(set)
|
? Collections.unmodifiableSet(set)
|
||||||
|
|
|
@ -15,8 +15,11 @@ public class PermissionDeniedException extends RuntimeException {
|
||||||
|
|
||||||
private static final long serialVersionUID = 5333137812363042580L;
|
private static final long serialVersionUID = 5333137812363042580L;
|
||||||
|
|
||||||
|
/** The EntityType of the denied permission check */
|
||||||
public final EntityType entityType;
|
public final EntityType entityType;
|
||||||
|
/** The PrivilegeType of the denied permission check */
|
||||||
public final PrivilegeType privilegeType;
|
public final PrivilegeType privilegeType;
|
||||||
|
/** The user identifier of the denied permission check */
|
||||||
public final String userId;
|
public final String userId;
|
||||||
|
|
||||||
public PermissionDeniedException(
|
public PermissionDeniedException(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
* Copyright (c) 2019 ETH Zürich, Educational Development and Technology (LET)
|
||||||
*
|
*
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
* 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
|
* 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/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
@ -8,16 +8,20 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.client;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.client;
|
||||||
|
|
||||||
|
/** Defines a simple data bean holding (encrypted) client credentials */
|
||||||
public final class ClientCredentials {
|
public final class ClientCredentials {
|
||||||
|
/** The client id or client name parameter */
|
||||||
public final String clientId;
|
public final String clientId;
|
||||||
|
/** The client secret parameter */
|
||||||
public final String secret;
|
public final String secret;
|
||||||
|
/** An client access token if supported */
|
||||||
public final String accessToken;
|
public final String accessToken;
|
||||||
|
|
||||||
public ClientCredentials(
|
public ClientCredentials(
|
||||||
final String clientId,
|
final String clientId,
|
||||||
final String secret,
|
final String secret,
|
||||||
final String accessToken) {
|
final String accessToken) {
|
||||||
|
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.secret = secret;
|
this.secret = secret;
|
||||||
this.accessToken = accessToken;
|
this.accessToken = accessToken;
|
||||||
|
|
|
@ -16,7 +16,7 @@ import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.ModelIdAware;
|
import ch.ethz.seb.sebserver.gbl.model.ModelIdAware;
|
||||||
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 Data Access Object for an Entity that has activation feature.
|
||||||
*
|
*
|
||||||
* @param <T> the type of Entity */
|
* @param <T> the type of Entity */
|
||||||
public interface ActivatableEntityDAO<T extends Entity, M extends ModelIdAware> extends EntityDAO<T, M> {
|
public interface ActivatableEntityDAO<T extends Entity, M extends ModelIdAware> extends EntityDAO<T, M> {
|
||||||
|
|
|
@ -24,6 +24,10 @@ import ch.ethz.seb.sebserver.gbl.model.EntityName;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.ModelIdAware;
|
import ch.ethz.seb.sebserver.gbl.model.ModelIdAware;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
|
||||||
|
/** Defines generic interface for all Entity based Data Access Objects
|
||||||
|
*
|
||||||
|
* @param <T> The specific type of the Entity domain model
|
||||||
|
* @param <M> The specific type of the Entity domain model to create a new Entity */
|
||||||
public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
|
public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
|
||||||
|
|
||||||
/** Get the entity type for a concrete EntityDAO implementation.
|
/** Get the entity type for a concrete EntityDAO implementation.
|
||||||
|
@ -34,7 +38,7 @@ public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
|
||||||
/** Use this to get an Entity instance of concrete type by database identifier/primary-key (PK)
|
/** Use this to get an Entity instance of concrete type by database identifier/primary-key (PK)
|
||||||
*
|
*
|
||||||
* @param id the data base identifier of the entity
|
* @param id the data base identifier of the entity
|
||||||
* @return Result refer the Entity instance with the specified database identifier or refer to an error if
|
* @return Result referring the Entity instance with the specified database identifier or refer to an error if
|
||||||
* happened */
|
* happened */
|
||||||
Result<T> byPK(Long id);
|
Result<T> byPK(Long id);
|
||||||
|
|
||||||
|
@ -44,7 +48,7 @@ public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
|
||||||
* but usually they are the same.
|
* but usually they are the same.
|
||||||
*
|
*
|
||||||
* @param id the model identifier
|
* @param id the model identifier
|
||||||
* @return Result refer the Entity instance with the specified model identifier or refer to an error if
|
* @return Result referring the Entity instance with the specified model identifier or refer to an error if
|
||||||
* happened */
|
* happened */
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
default Result<T> byModelId(final String id) {
|
default Result<T> byModelId(final String id) {
|
||||||
|
@ -53,12 +57,20 @@ public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
|
||||||
}).flatMap(this::byPK);
|
}).flatMap(this::byPK);
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<Collection<T>> loadEntities(Collection<EntityKey> keys);
|
/** Get a collection of all entities for the given Set of entity keys.
|
||||||
|
*
|
||||||
|
* @param keys the Set of EntityKey to get the Entity's for
|
||||||
|
* @return Result referring the collection or an error if happened */
|
||||||
|
Result<Collection<T>> byEntityKeys(Set<EntityKey> keys);
|
||||||
|
|
||||||
|
/** Get a collection of all EntityName for the given Set of EntityKey.
|
||||||
|
*
|
||||||
|
* @param keys the Set of EntityKey to get the EntityName's for
|
||||||
|
* @return Result referring the collection or an error if happened */
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
default Result<Collection<EntityName>> loadEntityNames(final Collection<EntityKey> keys) {
|
default Result<Collection<EntityName>> getEntityNames(final Set<EntityKey> keys) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
return loadEntities(keys)
|
return byEntityKeys(keys)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
.map(entity -> new EntityName(
|
.map(entity -> new EntityName(
|
||||||
|
@ -69,27 +81,56 @@ public interface EntityDAO<T extends Entity, M extends ModelIdAware> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Create a new Entity from the given entity domain model data.
|
||||||
|
*
|
||||||
|
* @param data The entity domain model data
|
||||||
|
* @return Result referring to the newly created Entity or an error if happened */
|
||||||
Result<T> createNew(M data);
|
Result<T> createNew(M data);
|
||||||
|
|
||||||
/** Use this to save/modify an entity.
|
/** Use this to save/modify an entity.
|
||||||
*
|
*
|
||||||
* @param data entity instance containing all data that should be saved
|
* @param data entity instance containing all data that should be saved
|
||||||
* @return A Result of the entity instance where the successfully saved/modified entity data is available or a
|
* @return A Result referring the entity instance where the successfully saved/modified entity data is available or
|
||||||
|
* a
|
||||||
* reported exception on error case */
|
* reported exception on error case */
|
||||||
Result<T> save(T data);
|
Result<T> save(T data);
|
||||||
|
|
||||||
/** Use this to delete a set Entity by a Collection of EntityKey
|
/** Use this to delete a set Entity by a Collection of EntityKey
|
||||||
*
|
*
|
||||||
* @param all The Collection of EntityKey to delete
|
* @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
|
* @return Result referring a collection of all entities that has been deleted or refer to an error if
|
||||||
* happened */
|
* happened */
|
||||||
Result<Collection<EntityKey>> delete(Set<EntityKey> all);
|
Result<Collection<EntityKey>> delete(Set<EntityKey> all);
|
||||||
|
|
||||||
|
/** Get a (unordered) collection of all Entities that matches the given filter criteria.
|
||||||
|
* The possible filter criteria for a specific Entity type is defined by the entity type.
|
||||||
|
*
|
||||||
|
* This adds filtering in SQL level by creating the select where clause from related
|
||||||
|
* filter criteria of the specific Entity type. If the filterMap contains a value for
|
||||||
|
* a particular filter criteria the value is extracted from the map and added to the where
|
||||||
|
* clause of the SQL select statement.
|
||||||
|
*
|
||||||
|
* @param filterMap FilterMap instance containing all the relevant filter criteria
|
||||||
|
* @return Result referring to collection of all matching entities or an error if happened */
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
default Result<Collection<T>> allMatching(final FilterMap filterMap) {
|
default Result<Collection<T>> allMatching(final FilterMap filterMap) {
|
||||||
return allMatching(filterMap, e -> true);
|
return allMatching(filterMap, e -> true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Get a (unordered) collection of all Entities that matches a given filter criteria
|
||||||
|
* and a given predicate.
|
||||||
|
*
|
||||||
|
* The possible filter criteria for a specific Entity type is defined by the entity type.
|
||||||
|
* This adds filtering in SQL level by creating the select where clause from related
|
||||||
|
* filter criteria of the specific Entity type. If the filterMap contains a value for
|
||||||
|
* a particular filter criteria the value is extracted from the map and added to the where
|
||||||
|
* clause of the SQL select statement.
|
||||||
|
*
|
||||||
|
* The predicate is applied after the SQL query by filtering the resulting list with the
|
||||||
|
* predicate after on the SQL query result, before returning.
|
||||||
|
*
|
||||||
|
* @param filterMap FilterMap instance containing all the relevant filter criteria
|
||||||
|
* @return Result referring to collection of all matching entities or an error if happened */
|
||||||
Result<Collection<T>> allMatching(FilterMap filterMap, Predicate<T> predicate);
|
Result<Collection<T>> allMatching(FilterMap filterMap, Predicate<T> predicate);
|
||||||
|
|
||||||
/** Context based utility method to extract an expected single resource entry form a Collection of specified type.
|
/** Context based utility method to extract an expected single resource entry form a Collection of specified type.
|
||||||
|
|
|
@ -11,10 +11,6 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
||||||
|
|
||||||
|
/** Concrete EntityDAO interface of Exam entities */
|
||||||
public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSupportDAO<Exam> {
|
public interface ExamDAO extends ActivatableEntityDAO<Exam, Exam>, BulkActionSupportDAO<Exam> {
|
||||||
|
|
||||||
// Result<Exam> importFromQuizData(Long institutionId, Long lmsSetupId, QuizData quizData);
|
|
||||||
//
|
|
||||||
// Result<Exam> byQuizId(String quizId);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,12 @@ import ch.ethz.seb.sebserver.gbl.model.institution.SebClientConfig;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
import ch.ethz.seb.sebserver.gbl.model.user.UserInfo;
|
||||||
import ch.ethz.seb.sebserver.webservice.datalayer.batis.JodaTimeTypeResolver;
|
import ch.ethz.seb.sebserver.webservice.datalayer.batis.JodaTimeTypeResolver;
|
||||||
|
|
||||||
|
/** A Map containing various filter criteria from a certain API request.
|
||||||
|
* This is used as a data object that can be used to collect API request parameter
|
||||||
|
* data on one side and supply filter criteria based access to concrete Entity filtering
|
||||||
|
* on the other side.
|
||||||
|
*
|
||||||
|
* All text based filter criteria are used as SQL wildcard's */
|
||||||
public class FilterMap extends POSTMapper {
|
public class FilterMap extends POSTMapper {
|
||||||
|
|
||||||
public FilterMap() {
|
public FilterMap() {
|
||||||
|
|
|
@ -14,8 +14,13 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Indicator;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
||||||
|
|
||||||
|
/** Concrete EntityDAO interface of Indicator entities */
|
||||||
public interface IndicatorDAO extends EntityDAO<Indicator, Indicator>, BulkActionSupportDAO<Indicator> {
|
public interface IndicatorDAO extends EntityDAO<Indicator, Indicator>, BulkActionSupportDAO<Indicator> {
|
||||||
|
|
||||||
|
/** Get a collection of all Indicator entities for a specified exam.
|
||||||
|
*
|
||||||
|
* @param examId the Exam identifier to get the Indicators for
|
||||||
|
* @return Result referring to the collection of Indicators of an Exam or to an error if happened */
|
||||||
Result<Collection<Indicator>> allForExam(Long examId);
|
Result<Collection<Indicator>> allForExam(Long examId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.institution.Institution;
|
import ch.ethz.seb.sebserver.gbl.model.institution.Institution;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
||||||
|
|
||||||
public interface InstitutionDAO
|
/** Concrete EntityDAO interface of Institution entities */
|
||||||
extends ActivatableEntityDAO<Institution, Institution>, BulkActionSupportDAO<Institution> {
|
public interface InstitutionDAO extends
|
||||||
|
ActivatableEntityDAO<Institution, Institution>,
|
||||||
boolean exists(String name);
|
BulkActionSupportDAO<Institution> {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,13 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
||||||
|
|
||||||
|
/** Concrete EntityDAO interface of LmsSetup entities */
|
||||||
public interface LmsSetupDAO extends ActivatableEntityDAO<LmsSetup, LmsSetup>, BulkActionSupportDAO<LmsSetup> {
|
public interface LmsSetupDAO extends ActivatableEntityDAO<LmsSetup, LmsSetup>, BulkActionSupportDAO<LmsSetup> {
|
||||||
|
|
||||||
|
/** Get the configured ClientCredentials for a given LmsSetup.
|
||||||
|
* The ClientCredentials are still encoded as they are on DB storage
|
||||||
|
*
|
||||||
|
* @param lmsSetupId the identifier of the LmsSetup to get the ClientCredentials for
|
||||||
|
* @return the configured ClientCredentials for a given LmsSetup */
|
||||||
Result<ClientCredentials> getLmsAPIAccessCredentials(String lmsSetupId);
|
Result<ClientCredentials> getLmsAPIAccessCredentials(String lmsSetupId);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
import ch.ethz.seb.sebserver.gbl.api.EntityType;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
import ch.ethz.seb.sebserver.gbl.model.EntityKey;
|
||||||
|
|
||||||
|
/** Thrown by Data Access Object if an requested Entity or other requested resource wasn't found */
|
||||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||||
public final class ResourceNotFoundException extends RuntimeException {
|
public final class ResourceNotFoundException extends RuntimeException {
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,16 @@ import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.bulkaction.BulkActionSupportDAO;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.client.ClientCredentials;
|
||||||
|
|
||||||
|
/** Concrete EntityDAO interface of SebClientConfig entities */
|
||||||
public interface SebClientConfigDAO extends
|
public interface SebClientConfigDAO extends
|
||||||
ActivatableEntityDAO<SebClientConfig, SebClientConfig>,
|
ActivatableEntityDAO<SebClientConfig, SebClientConfig>,
|
||||||
BulkActionSupportDAO<SebClientConfig> {
|
BulkActionSupportDAO<SebClientConfig> {
|
||||||
|
|
||||||
|
/** Get the configured ClientCredentials for a given SebClientConfig.
|
||||||
|
* The ClientCredentials are still encoded as they are on DB storage
|
||||||
|
*
|
||||||
|
* @param modelId the model identifier of the SebClientConfig to get the ClientCredentials for
|
||||||
|
* @return the configured ClientCredentials for a given SebClientConfig */
|
||||||
Result<ClientCredentials> getSebClientCredentials(String modelId);
|
Result<ClientCredentials> getSebClientCredentials(String modelId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.dao;
|
||||||
|
|
||||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||||
|
|
||||||
|
/** Defines some static Spring based transaction handling functionality for rollback handling */
|
||||||
public interface TransactionHandler {
|
public interface TransactionHandler {
|
||||||
|
|
||||||
/** Use this to mark the current transaction within the calling thread as "to rollback".
|
/** Use this to mark the current transaction within the calling thread as "to rollback".
|
||||||
|
|
|
@ -17,6 +17,7 @@ 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;
|
||||||
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.SEBServerUser;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.SEBServerUser;
|
||||||
|
|
||||||
|
/** Concrete EntityDAO interface of UserActivityLog entities */
|
||||||
public interface UserActivityLogDAO extends
|
public interface UserActivityLogDAO extends
|
||||||
EntityDAO<UserActivityLog, UserActivityLog>,
|
EntityDAO<UserActivityLog, UserActivityLog>,
|
||||||
UserRelatedEntityDAO<UserActivityLog> {
|
UserRelatedEntityDAO<UserActivityLog> {
|
||||||
|
|
|
@ -13,8 +13,7 @@ import java.util.Collection;
|
||||||
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
import ch.ethz.seb.sebserver.gbl.model.Entity;
|
||||||
import ch.ethz.seb.sebserver.gbl.util.Result;
|
import ch.ethz.seb.sebserver.gbl.util.Result;
|
||||||
|
|
||||||
/** Interface for a DAo handling an Entity with relations to an user (account)
|
/** Interface for all Data Access Objects handling an Entity with relations to an user (account)
|
||||||
*
|
|
||||||
*
|
*
|
||||||
* @param <T> the concrete type of the Entity */
|
* @param <T> the concrete type of the Entity */
|
||||||
public interface UserRelatedEntityDAO<T extends Entity> {
|
public interface UserRelatedEntityDAO<T extends Entity> {
|
||||||
|
|
|
@ -285,7 +285,7 @@ public class ExamDAOImpl implements ExamDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<Exam>> loadEntities(final Collection<EntityKey> keys) {
|
public Result<Collection<Exam>> byEntityKeys(final Set<EntityKey> keys) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final List<Long> ids = extractPKsFromKeys(keys);
|
final List<Long> ids = extractPKsFromKeys(keys);
|
||||||
return this.examRecordMapper.selectByExample()
|
return this.examRecordMapper.selectByExample()
|
||||||
|
|
|
@ -105,7 +105,7 @@ public class IndicatorDAOImpl implements IndicatorDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<Indicator>> loadEntities(final Collection<EntityKey> keys) {
|
public Result<Collection<Indicator>> byEntityKeys(final Set<EntityKey> keys) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final List<Long> ids = extractPKsFromKeys(keys);
|
final List<Long> ids = extractPKsFromKeys(keys);
|
||||||
|
|
||||||
|
|
|
@ -56,21 +56,6 @@ public class InstitutionDAOImpl implements InstitutionDAO {
|
||||||
return EntityType.INSTITUTION;
|
return EntityType.INSTITUTION;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
@Transactional(readOnly = true)
|
|
||||||
public boolean exists(final String name) {
|
|
||||||
if (StringUtils.isBlank(name)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Long count = this.institutionRecordMapper.countByExample()
|
|
||||||
.where(InstitutionRecordDynamicSqlSupport.name, isEqualTo(name))
|
|
||||||
.build()
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return count != null && count.longValue() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Institution> byPK(final Long id) {
|
public Result<Institution> byPK(final Long id) {
|
||||||
|
@ -248,7 +233,7 @@ public class InstitutionDAOImpl implements InstitutionDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<Institution>> loadEntities(final Collection<EntityKey> keys) {
|
public Result<Collection<Institution>> byEntityKeys(final Set<EntityKey> keys) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final List<Long> ids = extractPKsFromKeys(keys);
|
final List<Long> ids = extractPKsFromKeys(keys);
|
||||||
|
|
||||||
|
|
|
@ -252,7 +252,7 @@ public class LmsSetupDAOImpl implements LmsSetupDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<LmsSetup>> loadEntities(final Collection<EntityKey> keys) {
|
public Result<Collection<LmsSetup>> byEntityKeys(final Set<EntityKey> keys) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final List<Long> ids = extractPKsFromKeys(keys);
|
final List<Long> ids = extractPKsFromKeys(keys);
|
||||||
|
|
||||||
|
|
|
@ -223,7 +223,7 @@ public class SebClientConfigDAOImpl implements SebClientConfigDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<SebClientConfig>> loadEntities(final Collection<EntityKey> keys) {
|
public Result<Collection<SebClientConfig>> byEntityKeys(final Set<EntityKey> keys) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final List<Long> ids = extractPKsFromKeys(keys);
|
final List<Long> ids = extractPKsFromKeys(keys);
|
||||||
|
|
||||||
|
|
|
@ -317,7 +317,7 @@ public class UserActivityLogDAOImpl implements UserActivityLogDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<UserActivityLog>> loadEntities(final Collection<EntityKey> keys) {
|
public Result<Collection<UserActivityLog>> byEntityKeys(final Set<EntityKey> keys) {
|
||||||
// TODO Auto-generated method stub
|
// TODO Auto-generated method stub
|
||||||
return Result.ofTODO();
|
return Result.ofTODO();
|
||||||
}
|
}
|
||||||
|
|
|
@ -343,7 +343,7 @@ public class UserDAOImpl implements UserDAO {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Result<Collection<UserInfo>> loadEntities(final Collection<EntityKey> keys) {
|
public Result<Collection<UserInfo>> byEntityKeys(final Set<EntityKey> keys) {
|
||||||
return Result.tryCatch(() -> {
|
return Result.tryCatch(() -> {
|
||||||
final List<Long> ids = extractPKsFromKeys(keys);
|
final List<Long> ids = extractPKsFromKeys(keys);
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,11 @@ 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.dao.ActivatableEntityDAO;
|
import ch.ethz.seb.sebserver.webservice.servicelayer.dao.ActivatableEntityDAO;
|
||||||
|
|
||||||
|
/** This service can be used to 'manually' validate a Bean that is annotated within bean
|
||||||
|
* validation annotations.
|
||||||
|
*
|
||||||
|
* On validation error BeanValidationException is used to collect all validation issues
|
||||||
|
* and report them within the Result. */
|
||||||
@Service
|
@Service
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
public class BeanValidationService {
|
public class BeanValidationService {
|
||||||
|
@ -41,6 +46,14 @@ public class BeanValidationService {
|
||||||
dao -> dao));
|
dao -> dao));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Validates a given bean that is annotated with Java bean validation annotations
|
||||||
|
*
|
||||||
|
* On validation error BeanValidationException is used to collect all validation issues
|
||||||
|
* and report them within the Result.
|
||||||
|
*
|
||||||
|
* @param bean the Bean to validate
|
||||||
|
* @return Result referring the Bean if there are no validation issues or to a BeanValidationException
|
||||||
|
* containing the collected validation issues */
|
||||||
public <T> Result<T> validateBean(final T bean) {
|
public <T> Result<T> validateBean(final T bean) {
|
||||||
final DirectFieldBindingResult errors = new DirectFieldBindingResult(bean, "");
|
final DirectFieldBindingResult errors = new DirectFieldBindingResult(bean, "");
|
||||||
this.validator.validate(bean, errors);
|
this.validator.validate(bean, errors);
|
||||||
|
@ -51,6 +64,10 @@ public class BeanValidationService {
|
||||||
return Result.of(bean);
|
return Result.of(bean);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Indicates whether the Entity of a given EntityKey is currently active or not.
|
||||||
|
*
|
||||||
|
* @param entityKey the EntityKey of the Entity to check
|
||||||
|
* @return true if the Entity of a given EntityKey is currently active */
|
||||||
public boolean isActive(final EntityKey entityKey) {
|
public boolean isActive(final EntityKey entityKey) {
|
||||||
final ActivatableEntityDAO<?, ?> activatableEntityDAO = this.activatableDAOs.get(entityKey.entityType);
|
final ActivatableEntityDAO<?, ?> activatableEntityDAO = this.activatableDAOs.get(entityKey.entityType);
|
||||||
if (activatableEntityDAO == null) {
|
if (activatableEntityDAO == null) {
|
||||||
|
|
|
@ -240,10 +240,10 @@ public abstract class EntityController<T extends GrantEntity, M extends GrantEnt
|
||||||
return Arrays.asList(StringUtils.split(modelIds, Constants.LIST_SEPARATOR_CHAR))
|
return Arrays.asList(StringUtils.split(modelIds, Constants.LIST_SEPARATOR_CHAR))
|
||||||
.stream()
|
.stream()
|
||||||
.map(modelId -> new EntityKey(modelId, this.entityDAO.entityType()))
|
.map(modelId -> new EntityKey(modelId, this.entityDAO.entityType()))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toSet());
|
||||||
|
|
||||||
})
|
})
|
||||||
.flatMap(this.entityDAO::loadEntities)
|
.flatMap(this.entityDAO::byEntityKeys)
|
||||||
.getOrThrow()
|
.getOrThrow()
|
||||||
.stream()
|
.stream()
|
||||||
.filter(this.authorization::hasReadonlyGrant)
|
.filter(this.authorization::hasReadonlyGrant)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
|
||||||
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
|
||||||
|
|
||||||
|
/** Spring MVC controller that defines a revoke token endpoint */
|
||||||
@Controller
|
@Controller
|
||||||
@WebServiceProfile
|
@WebServiceProfile
|
||||||
public class RevokeTokenEndpoint {
|
public class RevokeTokenEndpoint {
|
||||||
|
|
|
@ -27,6 +27,7 @@ sebserver.form.validation.fieldError.username.notunique=This Username is already
|
||||||
sebserver.form.validation.fieldError.password.wrong=Old password is wrong
|
sebserver.form.validation.fieldError.password.wrong=Old password is wrong
|
||||||
sebserver.form.validation.fieldError.password.mismatch=Re-typed password don't match new password
|
sebserver.form.validation.fieldError.password.mismatch=Re-typed password don't match new password
|
||||||
sebserver.form.validation.fieldError.invalidURL=The input does not match the URL pattern.
|
sebserver.form.validation.fieldError.invalidURL=The input does not match the URL pattern.
|
||||||
|
sebserver.form.validation.fieldError.exists=This name already exists. Please choose another.
|
||||||
sebserver.error.unexpected=Unexpected Error
|
sebserver.error.unexpected=Unexpected Error
|
||||||
sebserver.page.message=Information
|
sebserver.page.message=Information
|
||||||
sebserver.dialog.confirm.title=Confirmation
|
sebserver.dialog.confirm.title=Confirmation
|
||||||
|
|
Loading…
Reference in a new issue