diff --git a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java index bcb01ecb5..064a4bad7 100644 --- a/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java +++ b/backend/core/src/main/java/org/opencdmp/audit/AuditableAction.java @@ -177,6 +177,11 @@ public class AuditableAction { public static final EventId TenantConfiguration_LookupByType = new EventId(270004, "TenantConfiguration_LookupByType"); public static final EventId Annotation_Created_Notify = new EventId(280000, "Annotation_Created_Notify"); + + public static final EventId UsageLimit_Query = new EventId(290000, "UsageLimit_Query"); + public static final EventId UsageLimit_Lookup = new EventId(290001, "UsageLimit_Lookup"); + public static final EventId UsageLimit_Persist = new EventId(290002, "UsageLimit_Persist"); + public static final EventId UsageLimit_Delete = new EventId(290003, "UsageLimit_Delete"); diff --git a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java index 0d1fa6add..07014bda6 100644 --- a/backend/core/src/main/java/org/opencdmp/authorization/Permission.java +++ b/backend/core/src/main/java/org/opencdmp/authorization/Permission.java @@ -200,6 +200,11 @@ public final class Permission { public static String EditPrefillingSource= "EditPrefillingSource"; public static String DeletePrefillingSource = "DeletePrefillingSource"; + //UsageLimit + public static String BrowseUsageLimit = "BrowseUsageLimit"; + public static String EditUsageLimit = "EditUsageLimit"; + public static String DeleteUsageLimit = "DeleteUsageLimit"; + //NotificationTemplate public static String BrowseStatus = "BrowseStatus"; public static String EditStatus = "EditStatus"; @@ -220,6 +225,7 @@ public final class Permission { public static String ViewReferenceTypePage = "ViewReferenceTypePage"; public static String ViewReferencePaPlge = "ViewReferencePage"; public static String ViewEntityLockPage = "ViewEntityLockPage"; + public static String ViewUsageLimitPage = "ViewUsageLimitPage"; public static String ViewDescriptionTemplatePage = "ViewDescriptionTemplatePage"; public static String ViewPlanBlueprintPage = "ViewPlanBlueprintPage"; public static String ViewPublicDescriptionPage = "ViewPublicDescriptionPage"; diff --git a/backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitMetricValue.java b/backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitMetricValue.java new file mode 100644 index 000000000..8d7e7a171 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/commons/enums/UsageLimitMetricValue.java @@ -0,0 +1,44 @@ +package org.opencdmp.commons.enums; + +import com.fasterxml.jackson.annotation.JsonValue; +import org.opencdmp.data.converters.enums.DatabaseEnum; + +import java.util.Map; + +public enum UsageLimitMetricValue implements DatabaseEnum { + USER_COUNT(MetricValues.UserCount), + PLAN_COUNT(MetricValues.PlanCount), + BLUEPRINT_COUNT(MetricValues.BlueprintCount), + DESCRIPTION_COUNT(MetricValues.DescriptionCount), + DESCRIPTION_TEMPLATE_COUNT(MetricValues.DescriptionTemplateCount), + DESCRIPTION_TEMPLATE_TYPE_COUNT(MetricValues.DescriptionTemplateTypeCount), + PREFILLING_SOURCES_COUNT(MetricValues.PrefillingSourcesCount), + REFERENCE_TYPE_COUNT(MetricValues.ReferenceTypeCount); + private final String value; + + public static class MetricValues { + public static final String UserCount = "user_count"; + public static final String PlanCount = "plan_count"; + public static final String BlueprintCount = "blueprint_count"; + public static final String DescriptionCount = "description_count"; + public static final String DescriptionTemplateCount = "description_template_count"; + public static final String DescriptionTemplateTypeCount = "description_template_type_count"; + public static final String PrefillingSourcesCount = "prefilling_sources_count"; + public static final String ReferenceTypeCount = "reference_type_count"; + } + + UsageLimitMetricValue(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return this.value; + } + + private static final Map map = EnumUtils.getEnumValueMap(UsageLimitMetricValue.class); + + public static UsageLimitMetricValue of(String i) { + return map.get(i); + } +} diff --git a/backend/core/src/main/java/org/opencdmp/data/UsageLimitEntity.java b/backend/core/src/main/java/org/opencdmp/data/UsageLimitEntity.java new file mode 100644 index 000000000..3598ad8bd --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/data/UsageLimitEntity.java @@ -0,0 +1,111 @@ +package org.opencdmp.data; + +import jakarta.persistence.*; +import org.opencdmp.commons.enums.IsActive; +import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.data.converters.enums.IsActiveConverter; +import org.opencdmp.data.tenant.TenantScopedBaseEntity; + +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "\"UsageLimit\"") +public class UsageLimitEntity extends TenantScopedBaseEntity { + + @Id + @Column(name = "id", columnDefinition = "uuid", updatable = false, nullable = false) + private UUID id; + + public static final String _id = "id"; + + @Column(name = "label", length = _labelLength, nullable = false) + private String label; + + public static final String _label = "label"; + + public static final int _labelLength = 250; + + + @Column(name = "metric_value", nullable = false) + private UsageLimitMetricValue metricValue; + + public static final String _metricValue = "metricValue"; + + @Column(name = "value", nullable = false) + private Long value; + + public static final String _value = "value"; + + @Column(name = "is_active", nullable = false) + @Convert(converter = IsActiveConverter.class) + private IsActive isActive; + + public static final String _isActive = "isActive"; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + public static final String _createdAt = "createdAt"; + + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + + public static final String _updatedAt = "updatedAt"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public UsageLimitMetricValue getMetricValue() { + return metricValue; + } + + public void setMetricValue(UsageLimitMetricValue metricValue) { + this.metricValue = metricValue; + } + + public Long getValue() { + return value; + } + + public void setValue(Long value) { + this.value = value; + } + + public IsActive getIsActive() { + return isActive; + } + + public void setIsActive(IsActive isActive) { + this.isActive = isActive; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/UsageLimit.java b/backend/core/src/main/java/org/opencdmp/model/UsageLimit.java new file mode 100644 index 000000000..5ddfff509 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/UsageLimit.java @@ -0,0 +1,109 @@ +package org.opencdmp.model; + +import org.opencdmp.commons.enums.IsActive; +import org.opencdmp.commons.enums.UsageLimitMetricValue; + +import java.time.Instant; +import java.util.UUID; + +public class UsageLimit { + + private UUID id; + public static final String _id = "id"; + + private String label; + public static final String _label = "label"; + + private UsageLimitMetricValue metricValue; + public static final String _metricValue = "metricValue"; + + private Long value; + public static final String _value = "value"; + + private IsActive isActive; + public static final String _isActive = "isActive"; + + private Instant createdAt; + public static final String _createdAt = "createdAt"; + + private Instant updatedAt; + public static final String _updatedAt = "updatedAt"; + + private String hash; + public static final String _hash = "hash"; + + private Boolean belongsToCurrentTenant; + public static final String _belongsToCurrentTenant = "belongsToCurrentTenant"; + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public UsageLimitMetricValue getMetricValue() { + return metricValue; + } + + public void setMetricValue(UsageLimitMetricValue metricValue) { + this.metricValue = metricValue; + } + + public Long getValue() { + return value; + } + + public void setValue(Long value) { + this.value = value; + } + + public IsActive getIsActive() { + return isActive; + } + + public void setIsActive(IsActive isActive) { + this.isActive = isActive; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + public Boolean getBelongsToCurrentTenant() { + return belongsToCurrentTenant; + } + + public void setBelongsToCurrentTenant(Boolean belongsToCurrentTenant) { + this.belongsToCurrentTenant = belongsToCurrentTenant; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/builder/UsageLimitBuilder.java b/backend/core/src/main/java/org/opencdmp/model/builder/UsageLimitBuilder.java new file mode 100644 index 000000000..12d4e54da --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/builder/UsageLimitBuilder.java @@ -0,0 +1,74 @@ +package org.opencdmp.model.builder; + +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.opencdmp.authorization.AuthorizationFlags; +import org.opencdmp.commons.scope.tenant.TenantScope; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.data.UsageLimitEntity; +import org.opencdmp.model.UsageLimit; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.util.*; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class UsageLimitBuilder extends BaseBuilder { + + private final TenantScope tenantScope; + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + @Autowired + public UsageLimitBuilder( + ConventionService conventionService, + TenantScope tenantScope) { + super(conventionService, new LoggerService(LoggerFactory.getLogger(UsageLimitBuilder.class))); + this.tenantScope = tenantScope; + } + + public UsageLimitBuilder authorize(EnumSet values) { + this.authorize = values; + return this; + } + + @Override + public List build(FieldSet fields, List data) throws MyApplicationException { + this.logger.debug("building for {} items requesting {} fields", Optional.ofNullable(data).map(List::size).orElse(0), Optional.ofNullable(fields).map(FieldSet::getFields).map(Set::size).orElse(0)); + this.logger.trace(new DataLogEntry("requested fields", fields)); + if (fields == null || data == null || fields.isEmpty()) + return new ArrayList<>(); + + List models = new ArrayList<>(); + for (UsageLimitEntity d : data) { + UsageLimit m = new UsageLimit(); + if (fields.hasField(this.asIndexer(UsageLimit._id))) + m.setId(d.getId()); + if (fields.hasField(this.asIndexer(UsageLimit._label))) + m.setLabel(d.getLabel()); + if (fields.hasField(this.asIndexer(UsageLimit._metricValue))) + m.setMetricValue(d.getMetricValue()); + if (fields.hasField(this.asIndexer(UsageLimit._value))) + m.setValue(d.getValue()); + if (fields.hasField(this.asIndexer(UsageLimit._createdAt))) + m.setCreatedAt(d.getCreatedAt()); + if (fields.hasField(this.asIndexer(UsageLimit._updatedAt))) + m.setUpdatedAt(d.getUpdatedAt()); + if (fields.hasField(this.asIndexer(UsageLimit._isActive))) + m.setIsActive(d.getIsActive()); + if (fields.hasField(this.asIndexer(UsageLimit._hash))) + m.setHash(this.hashValue(d.getUpdatedAt())); + if (fields.hasField(this.asIndexer(UsageLimit._belongsToCurrentTenant))) m.setBelongsToCurrentTenant(this.getBelongsToCurrentTenant(d, this.tenantScope)); + + models.add(m); + } + this.logger.debug("build {} items", Optional.of(models).map(List::size).orElse(0)); + return models; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/model/censorship/UsageLimitsCensor.java b/backend/core/src/main/java/org/opencdmp/model/censorship/UsageLimitsCensor.java new file mode 100644 index 000000000..b26d158f6 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/censorship/UsageLimitsCensor.java @@ -0,0 +1,40 @@ +package org.opencdmp.model.censorship; + +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.censor.CensorFactory; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.DataLogEntry; +import gr.cite.tools.logging.LoggerService; +import org.opencdmp.authorization.Permission; +import org.opencdmp.convention.ConventionService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class UsageLimitsCensor extends BaseCensor { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UsageLimitsCensor.class)); + + protected final AuthorizationService authService; + protected final CensorFactory censorFactory; + + public UsageLimitsCensor(ConventionService conventionService, + AuthorizationService authService, + CensorFactory censorFactory) { + super(conventionService); + this.authService = authService; + this.censorFactory = censorFactory; + } + + public void censor(FieldSet fields) { + logger.debug(new DataLogEntry("censoring fields", fields)); + if (fields == null || fields.isEmpty()) + return; + + this.authService.authorizeForce(Permission.BrowseUsageLimit); + } + +} diff --git a/backend/core/src/main/java/org/opencdmp/model/deleter/UsageLimitDeleter.java b/backend/core/src/main/java/org/opencdmp/model/deleter/UsageLimitDeleter.java new file mode 100644 index 000000000..12706f5b4 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/deleter/UsageLimitDeleter.java @@ -0,0 +1,79 @@ +package org.opencdmp.model.deleter; + +import gr.cite.tools.data.deleter.Deleter; +import gr.cite.tools.data.deleter.DeleterFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import org.opencdmp.commons.enums.IsActive; +import org.opencdmp.data.TenantEntityManager; +import org.opencdmp.data.UsageLimitEntity; +import org.opencdmp.query.UsageLimitQuery; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import javax.management.InvalidApplicationException; +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class UsageLimitDeleter implements Deleter { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UsageLimitDeleter.class)); + + private final TenantEntityManager entityManager; + + protected final QueryFactory queryFactory; + + protected final DeleterFactory deleterFactory; + + @Autowired + public UsageLimitDeleter( + TenantEntityManager entityManager, + QueryFactory queryFactory, + DeleterFactory deleterFactory + ) { + this.entityManager = entityManager; + this.queryFactory = queryFactory; + this.deleterFactory = deleterFactory; + } + + public void deleteAndSaveByIds(List ids) throws InvalidApplicationException { + logger.debug(new MapLogEntry("collecting to delete").And("count", Optional.ofNullable(ids).map(List::size).orElse(0)).And("ids", ids)); + List data = this.queryFactory.query(UsageLimitQuery.class).ids(ids).collect(); + logger.trace("retrieved {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + this.deleteAndSave(data); + } + + public void deleteAndSave(List data) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + this.delete(data); + logger.trace("saving changes"); + this.entityManager.flush(); + logger.trace("changes saved"); + } + + public void delete(List data) throws InvalidApplicationException { + logger.debug("will delete {} items", Optional.ofNullable(data).map(List::size).orElse(0)); + if (data == null || data.isEmpty()) + return; + + Instant now = Instant.now(); + + for (UsageLimitEntity item : data) { + logger.trace("deleting item {}", item.getId()); + item.setIsActive(IsActive.Inactive); + item.setUpdatedAt(now); + logger.trace("updating item"); + this.entityManager.merge(item); + logger.trace("updated item"); + } + } + +} diff --git a/backend/core/src/main/java/org/opencdmp/model/persist/UsageLimitPersist.java b/backend/core/src/main/java/org/opencdmp/model/persist/UsageLimitPersist.java new file mode 100644 index 000000000..7b0bccb64 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/model/persist/UsageLimitPersist.java @@ -0,0 +1,122 @@ +package org.opencdmp.model.persist; + +import gr.cite.tools.validation.specification.Specification; +import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.validation.BaseValidator; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.data.UsageLimitEntity; +import org.opencdmp.errorcode.ErrorThesaurusProperties; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.annotation.Scope; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +public class UsageLimitPersist { + + private UUID id; + public static final String _id = "id"; + + private String label; + public static final String _label = "label"; + + private UsageLimitMetricValue metricValue;; + public static final String _metricValue = "metricValue"; + + private Long value; + public static final String _value = "value"; + + private String hash; + public static final String _hash = "hash"; + + public UUID getId() { + return this.id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getLabel() { + return this.label; + } + + public void setLabel(String label) { + this.label = label; + } + + public UsageLimitMetricValue getMetricValue() { + return metricValue; + } + + public void setMetricValue(UsageLimitMetricValue metricValue) { + this.metricValue = metricValue; + } + + public Long getValue() { + return value; + } + + public void setValue(Long value) { + this.value = value; + } + + public String getHash() { + return this.hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + @Component(UsageLimitPersistValidator.ValidatorName) + @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) + public static class UsageLimitPersistValidator extends BaseValidator { + + public static final String ValidatorName = "UsageLimitPersistValidator"; + + private final MessageSource messageSource; + + protected UsageLimitPersistValidator(ConventionService conventionService, ErrorThesaurusProperties errors, MessageSource messageSource) { + super(conventionService, errors); + this.messageSource = messageSource; + } + + @Override + protected Class modelClass() { + return UsageLimitPersist.class; + } + + @Override + protected List specifications(UsageLimitPersist item) { + return Arrays.asList( + this.spec() + .iff(() -> this.isValidGuid(item.getId())) + .must(() -> this.isValidHash(item.getHash())) + .failOn(UsageLimitPersist._hash).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._hash}, LocaleContextHolder.getLocale())), + this.spec() + .iff(() -> !this.isValidGuid(item.getId())) + .must(() -> !this.isValidHash(item.getHash())) + .failOn(UsageLimitPersist._hash).failWith(this.messageSource.getMessage("Validation_OverPosting", new Object[]{}, LocaleContextHolder.getLocale())), + this.spec() + .must(() -> !this.isEmpty(item.getLabel())) + .failOn(UsageLimitPersist._label).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._label}, LocaleContextHolder.getLocale())), + this.spec() + .iff(() -> !this.isEmpty(item.getLabel())) + .must(() -> this.lessEqualLength(item.getLabel(), UsageLimitEntity._labelLength)) + .failOn(UsageLimitPersist._label).failWith(this.messageSource.getMessage("Validation_MaxLength", new Object[]{UsageLimitPersist._label}, LocaleContextHolder.getLocale())), + this.spec() + .must(() -> !this.isNull(item.getMetricValue())) + .failOn(UsageLimitPersist._metricValue).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._metricValue}, LocaleContextHolder.getLocale())), + this.spec() + .must(() -> !this.isNull(item.getValue())) + .failOn(UsageLimitPersist._value).failWith(this.messageSource.getMessage("Validation_Required", new Object[]{UsageLimitPersist._value}, LocaleContextHolder.getLocale())) + ); + } + } + +} diff --git a/backend/core/src/main/java/org/opencdmp/query/UsageLimitQuery.java b/backend/core/src/main/java/org/opencdmp/query/UsageLimitQuery.java new file mode 100644 index 000000000..74092c48a --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/query/UsageLimitQuery.java @@ -0,0 +1,227 @@ +package org.opencdmp.query; + +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.query.FieldResolver; +import gr.cite.tools.data.query.QueryBase; +import gr.cite.tools.data.query.QueryContext; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Tuple; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Predicate; +import org.opencdmp.authorization.AuthorizationFlags; +import org.opencdmp.commons.enums.IsActive; +import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.scope.user.UserScope; +import org.opencdmp.data.*; +import org.opencdmp.model.UsageLimit; +import org.opencdmp.query.utils.QueryUtilsService; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.util.*; + +@Component +@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) +public class UsageLimitQuery extends QueryBase { + + private String like; + + private Collection ids; + + private Collection isActives; + + private Collection usageLimitMetricValues; + + private Collection excludedIds; + + private EnumSet authorize = EnumSet.of(AuthorizationFlags.None); + + public UsageLimitQuery like(String value) { + this.like = value; + return this; + } + + public UsageLimitQuery ids(UUID value) { + this.ids = List.of(value); + return this; + } + + public UsageLimitQuery ids(UUID... value) { + this.ids = Arrays.asList(value); + return this; + } + + public UsageLimitQuery ids(Collection values) { + this.ids = values; + return this; + } + + public UsageLimitQuery isActive(IsActive value) { + this.isActives = List.of(value); + return this; + } + + public UsageLimitQuery isActive(IsActive... value) { + this.isActives = Arrays.asList(value); + return this; + } + + public UsageLimitQuery isActive(Collection values) { + this.isActives = values; + return this; + } + + public UsageLimitQuery excludedIds(Collection values) { + this.excludedIds = values; + return this; + } + + public UsageLimitQuery excludedIds(UUID value) { + this.excludedIds = List.of(value); + return this; + } + + public UsageLimitQuery excludedIds(UUID... value) { + this.excludedIds = Arrays.asList(value); + return this; + } + + public UsageLimitQuery usageLimitMetricValues(UsageLimitMetricValue value) { + this.usageLimitMetricValues = List.of(value); + return this; + } + + public UsageLimitQuery usageLimitMetricValues(UsageLimitMetricValue... value) { + this.usageLimitMetricValues = Arrays.asList(value); + return this; + } + + public UsageLimitQuery usageLimitMetricValues(Collection values) { + this.usageLimitMetricValues = values; + return this; + } + + public UsageLimitQuery authorize(EnumSet values) { + this.authorize = values; + return this; + } + + public UsageLimitQuery enableTracking() { + this.noTracking = false; + return this; + } + + public UsageLimitQuery disableTracking() { + this.noTracking = true; + return this; + } + + private final UserScope userScope; + + private final AuthorizationService authService; + + private final QueryUtilsService queryUtilsService; + private final TenantEntityManager tenantEntityManager; + + public UsageLimitQuery( + UserScope userScope, AuthorizationService authService, QueryUtilsService queryUtilsService, TenantEntityManager tenantEntityManager) { + this.userScope = userScope; + this.authService = authService; + this.queryUtilsService = queryUtilsService; + this.tenantEntityManager = tenantEntityManager; + } + + @Override + protected EntityManager entityManager(){ + return this.tenantEntityManager.getEntityManager(); + } + + @Override + protected Class entityClass() { + return UsageLimitEntity.class; + } + + @Override + protected Boolean isFalseQuery() { + return this.isEmpty(this.ids) || this.isEmpty(this.isActives) || this.isEmpty(this.excludedIds) || this.isEmpty(this.usageLimitMetricValues); + } + + @Override + protected Predicate applyFilters(QueryContext queryContext) { + List predicates = new ArrayList<>(); + if (this.ids != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UsageLimitEntity._id)); + for (UUID item : this.ids) + inClause.value(item); + predicates.add(inClause); + } + if (this.like != null && !this.like.isBlank()) { + predicates.add(this.queryUtilsService.ilike(queryContext.CriteriaBuilder, queryContext.Root.get(UsageLimitEntity._label), this.like)); + } + if (this.isActives != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UsageLimitEntity._isActive)); + for (IsActive item : this.isActives) + inClause.value(item); + predicates.add(inClause); + } + if (this.usageLimitMetricValues != null) { + CriteriaBuilder.In inClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(UsageLimitEntity._metricValue)); + for (UsageLimitMetricValue item : this.usageLimitMetricValues) + inClause.value(item); + predicates.add(inClause); + } + if (this.excludedIds != null) { + CriteriaBuilder.In notInClause = queryContext.CriteriaBuilder.in(queryContext.Root.get(ReferenceEntity._id)); + for (UUID item : this.excludedIds) + notInClause.value(item); + predicates.add(notInClause.not()); + } + if (!predicates.isEmpty()) { + Predicate[] predicatesArray = predicates.toArray(new Predicate[0]); + return queryContext.CriteriaBuilder.and(predicatesArray); + } else { + return null; + } + } + + @Override + protected UsageLimitEntity convert(Tuple tuple, Set columns) { + UsageLimitEntity item = new UsageLimitEntity(); + item.setId(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._id, UUID.class)); + item.setTenantId(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._tenantId, UUID.class)); + item.setLabel(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._label, String.class)); + item.setMetricValue(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._metricValue, UsageLimitMetricValue.class)); + item.setValue(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._value, Long.class)); + item.setCreatedAt(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._createdAt, Instant.class)); + item.setUpdatedAt(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._updatedAt, Instant.class)); + item.setIsActive(QueryBase.convertSafe(tuple, columns, UsageLimitEntity._isActive, IsActive.class)); + return item; + } + + @Override + protected String fieldNameOf(FieldResolver item) { + if (item.match(UsageLimit._id)) + return UsageLimitEntity._id; + else if (item.match(UsageLimit._label)) + return UsageLimitEntity._label; + else if (item.match(UsageLimit._metricValue)) + return UsageLimitEntity._metricValue; + else if (item.match(UsageLimit._value)) + return UsageLimitEntity._value; + else if (item.match(UsageLimit._createdAt)) + return UsageLimitEntity._createdAt; + else if (item.match(UsageLimit._updatedAt)) + return UsageLimitEntity._updatedAt; + else if (item.match(UsageLimit._hash)) + return UsageLimitEntity._updatedAt; + else if (item.match(UsageLimit._isActive)) + return UsageLimitEntity._isActive; + else if (item.prefix(UsageLimit._belongsToCurrentTenant)) + return UsageLimitEntity._tenantId; + else + return null; + } + +} diff --git a/backend/core/src/main/java/org/opencdmp/query/lookup/UsageLimitLookup.java b/backend/core/src/main/java/org/opencdmp/query/lookup/UsageLimitLookup.java new file mode 100644 index 000000000..cb32432d8 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/query/lookup/UsageLimitLookup.java @@ -0,0 +1,70 @@ +package org.opencdmp.query.lookup; + +import gr.cite.tools.data.query.Lookup; +import gr.cite.tools.data.query.QueryFactory; +import org.opencdmp.commons.enums.IsActive; +import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.query.UsageLimitQuery; + +import java.util.List; +import java.util.UUID; + +public class UsageLimitLookup extends Lookup { + + private String like; + private List isActive; + private List ids; + private List usageLimitMetricValues; + private List excludedIds; + + public String getLike() { + return like; + } + + public void setLike(String like) { + this.like = like; + } + + public List getIsActive() { + return isActive; + } + + public void setIsActive(List isActive) { + this.isActive = isActive; + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { this.ids = ids; } + + public List getUsageLimitsMetricValues() { + return usageLimitMetricValues; + } + + public void setUsageLimitsMetricValues(List usageLimitMetricValues) { + this.usageLimitMetricValues = usageLimitMetricValues; + } + + public List getExcludedIds() { + return excludedIds; + } + + public void setExcludedIds(List excludeIds) { + this.excludedIds = excludeIds; + } + + public UsageLimitQuery enrich(QueryFactory queryFactory) { + UsageLimitQuery query = queryFactory.query(UsageLimitQuery.class); + if (this.like != null) query.like(this.like); + if (this.isActive != null) query.isActive(this.isActive); + if (this.ids != null) query.ids(this.ids); + if (this.usageLimitMetricValues != null) query.usageLimitMetricValues(this.usageLimitMetricValues); + if (this.excludedIds != null) query.excludedIds(this.excludedIds); + + this.enrichCommon(query); + + return query; + } +} diff --git a/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java index e25c49c7a..07c74d2ee 100644 --- a/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java +++ b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitService.java @@ -1,11 +1,23 @@ package org.opencdmp.service.usagelimit; -public class UsageLimitService { +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.exception.MyValidationException; +import gr.cite.tools.fieldset.FieldSet; +import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.model.UsageLimit; +import org.opencdmp.model.persist.UsageLimitPersist; - private void checkIncrease(String metric) { - //Get/Calculate current metric value from accountingService - //Find metric value from db - // compare these two and throw UsageLimitException when current > metric value - } +import javax.management.InvalidApplicationException; +import java.util.UUID; + +public interface UsageLimitService { + + UsageLimit persist(UsageLimitPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException; + + void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException; + + void checkIncrease(UsageLimitMetricValue metric); } diff --git a/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitServiceImpl.java b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitServiceImpl.java new file mode 100644 index 000000000..d14b54ec9 --- /dev/null +++ b/backend/core/src/main/java/org/opencdmp/service/usagelimit/UsageLimitServiceImpl.java @@ -0,0 +1,126 @@ +package org.opencdmp.service.usagelimit; + +import gr.cite.commons.web.authz.service.AuthorizationService; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.deleter.DeleterFactory; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.exception.MyValidationException; +import gr.cite.tools.fieldset.BaseFieldSet; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import org.opencdmp.authorization.AuthorizationFlags; +import org.opencdmp.authorization.Permission; +import org.opencdmp.commons.enums.IsActive; +import org.opencdmp.commons.enums.UsageLimitMetricValue; +import org.opencdmp.commons.scope.user.UserScope; +import org.opencdmp.convention.ConventionService; +import org.opencdmp.data.TenantEntityManager; +import org.opencdmp.data.UsageLimitEntity; +import org.opencdmp.errorcode.ErrorThesaurusProperties; +import org.opencdmp.model.Tag; +import org.opencdmp.model.UsageLimit; +import org.opencdmp.model.builder.UsageLimitBuilder; +import org.opencdmp.model.deleter.UsageLimitDeleter; +import org.opencdmp.model.persist.UsageLimitPersist; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.stereotype.Service; + +import javax.management.InvalidApplicationException; +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Service +public class UsageLimitServiceImpl implements UsageLimitService { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UsageLimitServiceImpl.class)); + + private final TenantEntityManager entityManager; + + private final AuthorizationService authorizationService; + + private final DeleterFactory deleterFactory; + + private final BuilderFactory builderFactory; + + private final ConventionService conventionService; + + private final ErrorThesaurusProperties errors; + + private final MessageSource messageSource; + + private final UserScope userScope; + + + @Autowired + public UsageLimitServiceImpl( + TenantEntityManager entityManager, + AuthorizationService authorizationService, + DeleterFactory deleterFactory, + BuilderFactory builderFactory, + ConventionService conventionService, + ErrorThesaurusProperties errors, + MessageSource messageSource, + UserScope userScope) { + this.entityManager = entityManager; + this.authorizationService = authorizationService; + this.deleterFactory = deleterFactory; + this.builderFactory = builderFactory; + this.conventionService = conventionService; + this.errors = errors; + this.messageSource = messageSource; + this.userScope = userScope; + } + + public UsageLimit persist(UsageLimitPersist model, FieldSet fields) throws MyForbiddenException, MyValidationException, MyApplicationException, MyNotFoundException, InvalidApplicationException { + logger.debug(new MapLogEntry("persisting data tag").And("model", model).And("fields", fields)); + + this.authorizationService.authorizeForce(Permission.EditUsageLimit); + + Boolean isUpdate = this.conventionService.isValidGuid(model.getId()); + + UsageLimitEntity data; + if (isUpdate) { + data = this.entityManager.find(UsageLimitEntity.class, model.getId()); + if (data == null) throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{model.getId(), Tag.class.getSimpleName()}, LocaleContextHolder.getLocale())); + if (!this.conventionService.hashValue(data.getUpdatedAt()).equals(model.getHash())) throw new MyValidationException(this.errors.getHashConflict().getCode(), this.errors.getHashConflict().getMessage()); + } else { + data = new UsageLimitEntity(); + data.setId(UUID.randomUUID()); + data.setIsActive(IsActive.Active); + data.setCreatedAt(Instant.now()); + } + + data.setLabel(model.getLabel()); + data.setMetricValue(model.getMetricValue()); + data.setValue(model.getValue()); + data.setUpdatedAt(Instant.now()); + if (isUpdate) + this.entityManager.merge(data); + else + this.entityManager.persist(data); + + this.entityManager.flush(); + return this.builderFactory.builder(UsageLimitBuilder.class).authorize(AuthorizationFlags.AllExceptPublic).build(BaseFieldSet.build(fields, UsageLimit._id), data); + } + + public void deleteAndSave(UUID id) throws MyForbiddenException, InvalidApplicationException { + logger.debug("deleting UsageLimit: {}", id); + + this.authorizationService.authorizeForce(Permission.DeleteUsageLimit); + + this.deleterFactory.deleter(UsageLimitDeleter.class).deleteAndSaveByIds(List.of(id)); + } + + public void checkIncrease(UsageLimitMetricValue metric) { + //TODO + } + +} + diff --git a/backend/web/src/main/java/org/opencdmp/controllers/UsageFilterController.java b/backend/web/src/main/java/org/opencdmp/controllers/UsageFilterController.java new file mode 100644 index 000000000..b6c6d1592 --- /dev/null +++ b/backend/web/src/main/java/org/opencdmp/controllers/UsageFilterController.java @@ -0,0 +1,142 @@ +package org.opencdmp.controllers; + +import com.fasterxml.jackson.core.JsonProcessingException; +import gr.cite.tools.auditing.AuditService; +import gr.cite.tools.data.builder.BuilderFactory; +import gr.cite.tools.data.censor.CensorFactory; +import gr.cite.tools.data.query.QueryFactory; +import gr.cite.tools.exception.MyApplicationException; +import gr.cite.tools.exception.MyForbiddenException; +import gr.cite.tools.exception.MyNotFoundException; +import gr.cite.tools.fieldset.FieldSet; +import gr.cite.tools.logging.LoggerService; +import gr.cite.tools.logging.MapLogEntry; +import gr.cite.tools.validation.ValidationFilterAnnotation; +import jakarta.transaction.Transactional; +import jakarta.xml.bind.JAXBException; +import org.opencdmp.audit.AuditableAction; +import org.opencdmp.authorization.AuthorizationFlags; +import org.opencdmp.data.UsageLimitEntity; +import org.opencdmp.model.UsageLimit; +import org.opencdmp.model.builder.UsageLimitBuilder; +import org.opencdmp.model.censorship.UsageLimitsCensor; +import org.opencdmp.model.persist.UsageLimitPersist; +import org.opencdmp.model.result.QueryResult; +import org.opencdmp.query.UsageLimitQuery; +import org.opencdmp.query.lookup.UsageLimitLookup; +import org.opencdmp.service.usagelimit.UsageLimitService; +import org.slf4j.LoggerFactory; +import org.springframework.context.MessageSource; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.web.bind.annotation.*; + +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.management.InvalidApplicationException; +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.transform.TransformerException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.AbstractMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping(path = "api/usage-limit") +public class UsageFilterController { + + private static final LoggerService logger = new LoggerService(LoggerFactory.getLogger(UsageFilterController.class)); + + private final BuilderFactory builderFactory; + + private final AuditService auditService; + + private final UsageLimitService usageLimitService; + + private final CensorFactory censorFactory; + + private final QueryFactory queryFactory; + + private final MessageSource messageSource; + + public UsageFilterController( + BuilderFactory builderFactory, + AuditService auditService, + UsageLimitService usageLimitService, + CensorFactory censorFactory, + QueryFactory queryFactory, + MessageSource messageSource) { + this.builderFactory = builderFactory; + this.auditService = auditService; + this.usageLimitService = usageLimitService; + this.censorFactory = censorFactory; + this.queryFactory = queryFactory; + this.messageSource = messageSource; + } + + @PostMapping("query") + public QueryResult query(@RequestBody UsageLimitLookup lookup) throws MyApplicationException, MyForbiddenException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException, InvalidApplicationException { + logger.debug("querying {}", UsageLimit.class.getSimpleName()); + + this.censorFactory.censor(UsageLimitsCensor.class).censor(lookup.getProject()); + UsageLimitQuery query = lookup.enrich(this.queryFactory).authorize(AuthorizationFlags.AllExceptPublic); + + List data = query.collectAs(lookup.getProject()); + List models = this.builderFactory.builder(UsageLimitBuilder.class).authorize(AuthorizationFlags.AllExceptPublic).build(lookup.getProject(), data); + long count = (lookup.getMetadata() != null && lookup.getMetadata().getCountAll()) ? query.count() : models.size(); + + this.auditService.track(AuditableAction.UsageLimit_Query, "lookup", lookup); + + return new QueryResult<>(models, count); + } + + @GetMapping("{id}") + public UsageLimit get(@PathVariable("id") UUID id, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidKeyException, InvalidApplicationException { + logger.debug(new MapLogEntry("retrieving" + UsageLimit.class.getSimpleName()).And("id", id).And("fields", fieldSet)); + + this.censorFactory.censor(UsageLimitsCensor.class).censor(fieldSet); + + UsageLimitQuery query = this.queryFactory.query(UsageLimitQuery.class).disableTracking().authorize(AuthorizationFlags.AllExceptPublic).ids(id); + UsageLimit model = this.builderFactory.builder(UsageLimitBuilder.class).authorize(AuthorizationFlags.AllExceptPublic).build(fieldSet, query.firstAs(fieldSet)); + if (model == null) + throw new MyNotFoundException(this.messageSource.getMessage("General_ItemNotFound", new Object[]{id, UsageLimit.class.getSimpleName()}, LocaleContextHolder.getLocale())); + + this.auditService.track(AuditableAction.UsageLimit_Lookup, Map.ofEntries( + new AbstractMap.SimpleEntry("id", id), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + + return model; + } + + @PostMapping("persist") + @Transactional + @ValidationFilterAnnotation(validator = UsageLimitPersist.UsageLimitPersistValidator.ValidatorName, argumentName = "model") + public UsageLimit persist(@RequestBody UsageLimitPersist model, FieldSet fieldSet) throws MyApplicationException, MyForbiddenException, MyNotFoundException, InvalidApplicationException, JAXBException, ParserConfigurationException, JsonProcessingException, TransformerException, InvalidAlgorithmParameterException, NoSuchPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException { + logger.debug(new MapLogEntry("persisting" + UsageLimit.class.getSimpleName()).And("model", model).And("fieldSet", fieldSet)); + this.censorFactory.censor(UsageLimitsCensor.class).censor(fieldSet); + + UsageLimit persisted = this.usageLimitService.persist(model, fieldSet); + + this.auditService.track(AuditableAction.UsageLimit_Persist, Map.ofEntries( + new AbstractMap.SimpleEntry("model", model), + new AbstractMap.SimpleEntry("fields", fieldSet) + )); + + return persisted; + } + + @DeleteMapping("{id}") + @Transactional + public void delete(@PathVariable("id") UUID id) throws MyForbiddenException, InvalidApplicationException { + logger.debug(new MapLogEntry("retrieving" + UsageLimit.class.getSimpleName()).And("id", id)); + + this.usageLimitService.deleteAndSave(id); + + this.auditService.track(AuditableAction.UsageLimit_Delete, "id", id); + } + +} diff --git a/backend/web/src/main/resources/config/permissions.yml b/backend/web/src/main/resources/config/permissions.yml index 8ee5609c2..19af56300 100644 --- a/backend/web/src/main/resources/config/permissions.yml +++ b/backend/web/src/main/resources/config/permissions.yml @@ -1063,6 +1063,28 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + + # Tenant Permissions + BrowseUsageLimit: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + EditUsageLimit: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + DeleteUsageLimit: + roles: + - Admin + claims: [ ] + clients: [ ] + allowAnonymous: false + allowAuthenticated: false + # Status BrowseStatus: roles: @@ -1179,6 +1201,12 @@ permissions: clients: [ ] allowAnonymous: false allowAuthenticated: false + ViewUsageLimitPage: + roles: + - Admin + clients: [ ] + allowAnonymous: false + allowAuthenticated: false ViewDescriptionTemplatePage: roles: - Admin diff --git a/dmp-db-scema/updates/00.01.067_Add_UsageLimits.sql b/dmp-db-scema/updates/00.01.067_Add_UsageLimits.sql index 9295c435e..eaaf80488 100644 --- a/dmp-db-scema/updates/00.01.067_Add_UsageLimits.sql +++ b/dmp-db-scema/updates/00.01.067_Add_UsageLimits.sql @@ -4,7 +4,7 @@ BEGIN PERFORM * FROM "DBVersion" WHERE version = this_version; IF FOUND THEN RETURN; END IF; - CREATE TABLE public."UsageLimits" + CREATE TABLE public."UsageLimit" ( id uuid NOT NULL, label character varying NOT NULL, @@ -15,13 +15,13 @@ BEGIN is_active smallint NOT NULL, tenant uuid, PRIMARY KEY (id), - CONSTRAINT "UsageLimits_tenant_fkey" FOREIGN KEY (tenant) + CONSTRAINT "UsageLimit_tenant_fkey" FOREIGN KEY (tenant) REFERENCES public."Tenant" (id) MATCH SIMPLE ON UPDATE NO ACTION ON DELETE NO ACTION NOT VALID ); - INSERT INTO public."DBVersion" VALUES ('DMPDB', '00.01.067', '2024-11-07 12:00:00.000000+02', now(), 'Add UsageLimits table.'); + INSERT INTO public."DBVersion" VALUES ('DMPDB', '00.01.067', '2024-11-07 12:00:00.000000+02', now(), 'Add UsageLimit table.'); END$$; \ No newline at end of file