You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2023/08/23 09:34:01 UTC
[james-project] branch master updated: JAMES-4840 refactor quota for cassandra: create cassandra DAOs (#1673)
This is an automated email from the ASF dual-hosted git repository.
rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 5243d007fc JAMES-4840 refactor quota for cassandra: create cassandra DAOs (#1673)
5243d007fc is described below
commit 5243d007fcfd934f92ca4c14674a300ce3f77a56
Author: hungphan227 <45...@users.noreply.github.com>
AuthorDate: Wed Aug 23 16:33:55 2023 +0700
JAMES-4840 refactor quota for cassandra: create cassandra DAOs (#1673)
---
.../apache/james/core/quota/QuotaCurrentValue.java | 12 +-
.../cassandra/modules/CassandraQuotaModule.java | 23 +++
.../quota/CassandraQuotaCurrentValueDao.java | 205 ++++++++++++++++++++
.../cassandra/quota/CassandraQuotaLimitDao.java | 209 +++++++++++++++++++++
.../table/CassandraQuotaCurrentValueTable.java | 31 +++
.../cassandra/table/CassandraQuotaLimitTable.java | 32 ++++
.../quota/CassandraQuotaCurrentValueDaoTest.java | 136 ++++++++++++++
.../quota/CassandraQuotaLimitDaoTest.java | 91 +++++++++
.../modules/mailbox/CassandraQuotaModule.java | 4 +
9 files changed, 736 insertions(+), 7 deletions(-)
diff --git a/core/src/main/java/org/apache/james/core/quota/QuotaCurrentValue.java b/core/src/main/java/org/apache/james/core/quota/QuotaCurrentValue.java
index 22f6b3f004..682f10c7bc 100644
--- a/core/src/main/java/org/apache/james/core/quota/QuotaCurrentValue.java
+++ b/core/src/main/java/org/apache/james/core/quota/QuotaCurrentValue.java
@@ -21,8 +21,6 @@ package org.apache.james.core.quota;
import java.util.Objects;
-import org.apache.james.core.Username;
-
import com.google.common.base.MoreObjects;
import com.google.common.base.Preconditions;
@@ -30,7 +28,7 @@ public class QuotaCurrentValue {
public static class Builder {
private QuotaComponent quotaComponent;
- private Username identifier;
+ private String identifier;
private QuotaType quotaType;
private long currentValue;
@@ -39,7 +37,7 @@ public class QuotaCurrentValue {
return this;
}
- public Builder identifier(Username identifier) {
+ public Builder identifier(String identifier) {
this.identifier = identifier;
return this;
}
@@ -68,11 +66,11 @@ public class QuotaCurrentValue {
}
private final QuotaComponent quotaComponent;
- private final Username identifier;
+ private final String identifier;
private final QuotaType quotaType;
private final long currentValue;
- private QuotaCurrentValue(QuotaComponent quotaComponent, Username identifier, QuotaType quotaType, long currentValue) {
+ private QuotaCurrentValue(QuotaComponent quotaComponent, String identifier, QuotaType quotaType, long currentValue) {
this.quotaComponent = quotaComponent;
this.identifier = identifier;
this.quotaType = quotaType;
@@ -83,7 +81,7 @@ public class QuotaCurrentValue {
return quotaComponent;
}
- public Username getIdentifier() {
+ public String getIdentifier() {
return identifier;
}
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraQuotaModule.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraQuotaModule.java
index ae44dad59c..9164b27a7a 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraQuotaModule.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/modules/CassandraQuotaModule.java
@@ -30,6 +30,8 @@ import org.apache.james.mailbox.cassandra.table.CassandraCurrentQuota;
import org.apache.james.mailbox.cassandra.table.CassandraDomainMaxQuota;
import org.apache.james.mailbox.cassandra.table.CassandraGlobalMaxQuota;
import org.apache.james.mailbox.cassandra.table.CassandraMaxQuota;
+import org.apache.james.mailbox.cassandra.table.CassandraQuotaCurrentValueTable;
+import org.apache.james.mailbox.cassandra.table.CassandraQuotaLimitTable;
public interface CassandraQuotaModule {
CassandraModule MODULE = CassandraModule.builder()
@@ -68,5 +70,26 @@ public interface CassandraQuotaModule {
.withPartitionKey(CassandraGlobalMaxQuota.KEY, TEXT)
.withColumn(CassandraGlobalMaxQuota.STORAGE, BIGINT)
.withColumn(CassandraGlobalMaxQuota.MESSAGE, BIGINT))
+
+ .table(CassandraQuotaLimitTable.TABLE_NAME)
+ .comment("Holds quota limits.")
+ .options(options -> options
+ .withCaching(true, rows(CassandraConstants.DEFAULT_CACHED_ROW_PER_PARTITION)))
+ .statement(statement -> types -> statement
+ .withPartitionKey(CassandraQuotaLimitTable.QUOTA_SCOPE, TEXT)
+ .withPartitionKey(CassandraQuotaLimitTable.IDENTIFIER, TEXT)
+ .withClusteringColumn(CassandraQuotaLimitTable.QUOTA_COMPONENT, TEXT)
+ .withClusteringColumn(CassandraQuotaLimitTable.QUOTA_TYPE, TEXT)
+ .withColumn(CassandraQuotaLimitTable.QUOTA_LIMIT, BIGINT))
+
+ .table(CassandraQuotaCurrentValueTable.TABLE_NAME)
+ .comment("Holds quota current values.")
+ .options(options -> options
+ .withCaching(true, rows(CassandraConstants.DEFAULT_CACHED_ROW_PER_PARTITION)))
+ .statement(statement -> types -> statement
+ .withPartitionKey(CassandraQuotaCurrentValueTable.IDENTIFIER, TEXT)
+ .withClusteringColumn(CassandraQuotaCurrentValueTable.QUOTA_COMPONENT, TEXT)
+ .withClusteringColumn(CassandraQuotaCurrentValueTable.QUOTA_TYPE, TEXT)
+ .withColumn(CassandraQuotaCurrentValueTable.CURRENT_VALUE, COUNTER))
.build();
}
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaCurrentValueDao.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaCurrentValueDao.java
new file mode 100644
index 0000000000..4a5546a8cc
--- /dev/null
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaCurrentValueDao.java
@@ -0,0 +1,205 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mailbox.cassandra.quota;
+
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.update;
+import static com.datastax.oss.driver.api.querybuilder.relation.Relation.column;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaCurrentValueTable.CURRENT_VALUE;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaCurrentValueTable.IDENTIFIER;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaCurrentValueTable.QUOTA_COMPONENT;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaCurrentValueTable.QUOTA_TYPE;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaCurrentValueTable.TABLE_NAME;
+
+import java.util.Objects;
+
+import javax.inject.Inject;
+
+import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
+import org.apache.james.core.quota.QuotaComponent;
+import org.apache.james.core.quota.QuotaCurrentValue;
+import org.apache.james.core.quota.QuotaType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.cql.PreparedStatement;
+import com.datastax.oss.driver.api.core.cql.Row;
+import com.datastax.oss.driver.api.querybuilder.delete.Delete;
+import com.datastax.oss.driver.api.querybuilder.select.Select;
+import com.datastax.oss.driver.api.querybuilder.update.Update;
+import com.google.common.base.MoreObjects;
+
+import reactor.core.publisher.Mono;
+
+public class CassandraQuotaCurrentValueDao {
+
+ public static class QuotaKey {
+
+ public static QuotaKey of(QuotaComponent component, String identifier, QuotaType quotaType) {
+ return new QuotaKey(component, identifier, quotaType);
+ }
+
+ private final QuotaComponent quotaComponent;
+ private final String identifier;
+ private final QuotaType quotaType;
+
+ public QuotaComponent getQuotaComponent() {
+ return quotaComponent;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public QuotaType getQuotaType() {
+ return quotaType;
+ }
+
+ private QuotaKey(QuotaComponent quotaComponent, String identifier, QuotaType quotaType) {
+ this.quotaComponent = quotaComponent;
+ this.identifier = identifier;
+ this.quotaType = quotaType;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(quotaComponent, identifier, quotaType);
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof QuotaKey) {
+ QuotaKey other = (QuotaKey) o;
+ return Objects.equals(quotaComponent, other.quotaComponent)
+ && Objects.equals(identifier, other.identifier)
+ && Objects.equals(quotaType, other.quotaType);
+ }
+ return false;
+ }
+
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("quotaComponent", quotaComponent)
+ .add("identifier", identifier)
+ .add("quotaType", quotaType)
+ .toString();
+ }
+ }
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CassandraQuotaCurrentValueDao.class);
+
+ private final CassandraAsyncExecutor queryExecutor;
+ private final PreparedStatement increaseStatement;
+ private final PreparedStatement decreaseStatement;
+ private final PreparedStatement getQuotaCurrentValueStatement;
+ private final PreparedStatement deleteQuotaCurrentValueStatement;
+
+ @Inject
+ public CassandraQuotaCurrentValueDao(CqlSession session) {
+ this.queryExecutor = new CassandraAsyncExecutor(session);
+ this.increaseStatement = session.prepare(increaseStatement().build());
+ this.decreaseStatement = session.prepare(decreaseStatement().build());
+ this.getQuotaCurrentValueStatement = session.prepare(getQuotaCurrentValueStatement().build());
+ this.deleteQuotaCurrentValueStatement = session.prepare(deleteQuotaCurrentValueStatement().build());
+ }
+
+ public Mono<Void> increase(QuotaKey quotaKey, long amount) {
+ return queryExecutor.executeVoid(increaseStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaKey.getQuotaComponent().getValue())
+ .setString(IDENTIFIER, quotaKey.getIdentifier())
+ .setString(QUOTA_TYPE, quotaKey.getQuotaType().getValue())
+ .setLong(CURRENT_VALUE, amount))
+ .onErrorResume(ex -> {
+ LOGGER.warn("Failure when increasing {} {} quota for {}. Quota current value is thus not updated and needs recomputation",
+ quotaKey.getQuotaComponent().getValue(), quotaKey.getQuotaType().getValue(), quotaKey.getIdentifier(), ex);
+ return Mono.empty();
+ });
+ }
+
+ public Mono<Void> decrease(QuotaKey quotaKey, long amount) {
+ return queryExecutor.executeVoid(decreaseStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaKey.getQuotaComponent().getValue())
+ .setString(IDENTIFIER, quotaKey.getIdentifier())
+ .setString(QUOTA_TYPE, quotaKey.getQuotaType().getValue())
+ .setLong(CURRENT_VALUE, amount))
+ .onErrorResume(ex -> {
+ LOGGER.warn("Failure when decreasing {} {} quota for {}. Quota current value is thus not updated and needs recomputation",
+ quotaKey.getQuotaComponent().getValue(), quotaKey.getQuotaType().getValue(), quotaKey.getIdentifier(), ex);
+ return Mono.empty();
+ });
+ }
+
+ public Mono<QuotaCurrentValue> getQuotaCurrentValue(QuotaKey quotaKey) {
+ return queryExecutor.executeSingleRow(getQuotaCurrentValueStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaKey.getQuotaComponent().getValue())
+ .setString(IDENTIFIER, quotaKey.getIdentifier())
+ .setString(QUOTA_TYPE, quotaKey.getQuotaType().getValue()))
+ .map(row -> convertRowToModel(row));
+ }
+
+ public Mono<Void> deleteQuotaCurrentValue(QuotaKey quotaKey) {
+ return queryExecutor.executeVoid(deleteQuotaCurrentValueStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaKey.getQuotaComponent().getValue())
+ .setString(IDENTIFIER, quotaKey.getIdentifier())
+ .setString(QUOTA_TYPE, quotaKey.getQuotaType().getValue()));
+ }
+
+ private Update increaseStatement() {
+ return update(TABLE_NAME)
+ .increment(CURRENT_VALUE, bindMarker(CURRENT_VALUE))
+ .where(column(IDENTIFIER).isEqualTo(bindMarker(IDENTIFIER)),
+ column(QUOTA_COMPONENT).isEqualTo(bindMarker(QUOTA_COMPONENT)),
+ column(QUOTA_TYPE).isEqualTo(bindMarker(QUOTA_TYPE)));
+ }
+
+ private Update decreaseStatement() {
+ return update(TABLE_NAME)
+ .decrement(CURRENT_VALUE, bindMarker(CURRENT_VALUE))
+ .where(column(IDENTIFIER).isEqualTo(bindMarker(IDENTIFIER)),
+ column(QUOTA_COMPONENT).isEqualTo(bindMarker(QUOTA_COMPONENT)),
+ column(QUOTA_TYPE).isEqualTo(bindMarker(QUOTA_TYPE)));
+ }
+
+ private Select getQuotaCurrentValueStatement() {
+ return selectFrom(TABLE_NAME)
+ .all()
+ .where(column(IDENTIFIER).isEqualTo(bindMarker(IDENTIFIER)),
+ column(QUOTA_COMPONENT).isEqualTo(bindMarker(QUOTA_COMPONENT)),
+ column(QUOTA_TYPE).isEqualTo(bindMarker(QUOTA_TYPE)));
+ }
+
+ private Delete deleteQuotaCurrentValueStatement() {
+ return deleteFrom(TABLE_NAME)
+ .where(column(IDENTIFIER).isEqualTo(bindMarker(IDENTIFIER)),
+ column(QUOTA_COMPONENT).isEqualTo(bindMarker(QUOTA_COMPONENT)),
+ column(QUOTA_TYPE).isEqualTo(bindMarker(QUOTA_TYPE)));
+ }
+
+ private QuotaCurrentValue convertRowToModel(Row row) {
+ return QuotaCurrentValue.builder().quotaComponent(QuotaComponent.of(row.get(QUOTA_COMPONENT, String.class)))
+ .identifier(row.get(IDENTIFIER, String.class))
+ .quotaType(QuotaType.of(row.get(QUOTA_TYPE, String.class)))
+ .currentValue(row.get(CURRENT_VALUE, Long.class)).build();
+ }
+
+}
\ No newline at end of file
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaLimitDao.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaLimitDao.java
new file mode 100644
index 0000000000..6062eebb6f
--- /dev/null
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaLimitDao.java
@@ -0,0 +1,209 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mailbox.cassandra.quota;
+
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.bindMarker;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.deleteFrom;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.insertInto;
+import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.selectFrom;
+import static com.datastax.oss.driver.api.querybuilder.relation.Relation.column;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaLimitTable.IDENTIFIER;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaLimitTable.QUOTA_COMPONENT;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaLimitTable.QUOTA_LIMIT;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaLimitTable.QUOTA_SCOPE;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaLimitTable.QUOTA_TYPE;
+import static org.apache.james.mailbox.cassandra.table.CassandraQuotaLimitTable.TABLE_NAME;
+
+import java.util.Objects;
+
+import javax.inject.Inject;
+
+import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
+import org.apache.james.core.quota.QuotaComponent;
+import org.apache.james.core.quota.QuotaLimit;
+import org.apache.james.core.quota.QuotaScope;
+import org.apache.james.core.quota.QuotaType;
+
+import com.datastax.oss.driver.api.core.CqlSession;
+import com.datastax.oss.driver.api.core.cql.PreparedStatement;
+import com.datastax.oss.driver.api.core.cql.Row;
+import com.datastax.oss.driver.api.querybuilder.delete.Delete;
+import com.datastax.oss.driver.api.querybuilder.insert.Insert;
+import com.datastax.oss.driver.api.querybuilder.select.Select;
+import com.google.common.base.MoreObjects;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class CassandraQuotaLimitDao {
+
+ public static class QuotaLimitKey {
+
+ public static QuotaLimitKey of(QuotaComponent component, QuotaScope scope, String identifier, QuotaType quotaType) {
+ return new QuotaLimitKey(component, scope, identifier, quotaType);
+ }
+
+ private final QuotaComponent quotaComponent;
+ private final QuotaScope quotaScope;
+ private final String identifier;
+ private final QuotaType quotaType;
+
+ public QuotaComponent getQuotaComponent() {
+ return quotaComponent;
+ }
+
+ public QuotaScope getQuotaScope() {
+ return quotaScope;
+ }
+
+ public String getIdentifier() {
+ return identifier;
+ }
+
+ public QuotaType getQuotaType() {
+ return quotaType;
+ }
+
+ private QuotaLimitKey(QuotaComponent quotaComponent, QuotaScope quotaScope, String identifier, QuotaType quotaType) {
+ this.quotaComponent = quotaComponent;
+ this.quotaScope = quotaScope;
+ this.identifier = identifier;
+ this.quotaType = quotaType;
+ }
+
+ @Override
+ public final int hashCode() {
+ return Objects.hash(quotaComponent, quotaScope, identifier, quotaType);
+ }
+
+ @Override
+ public final boolean equals(Object o) {
+ if (o instanceof QuotaLimitKey) {
+ QuotaLimitKey other = (QuotaLimitKey) o;
+ return Objects.equals(quotaComponent, other.quotaComponent)
+ && Objects.equals(quotaScope, other.quotaScope)
+ && Objects.equals(identifier, other.identifier)
+ && Objects.equals(quotaType, other.quotaType);
+ }
+ return false;
+ }
+
+ public String toString() {
+ return MoreObjects.toStringHelper(this)
+ .add("quotaComponent", quotaComponent)
+ .add("quotaScope", quotaScope)
+ .add("identifier", identifier)
+ .add("quotaType", quotaType)
+ .toString();
+ }
+ }
+
+ private final CassandraAsyncExecutor queryExecutor;
+ private final PreparedStatement getQuotaLimitStatement;
+ private final PreparedStatement getQuotaLimitsStatement;
+ private final PreparedStatement setQuotaLimitStatement;
+ private final PreparedStatement deleteQuotaLimitStatement;
+
+ @Inject
+ public CassandraQuotaLimitDao(CqlSession session) {
+ this.queryExecutor = new CassandraAsyncExecutor(session);
+ this.getQuotaLimitStatement = session.prepare(getQuotaLimitStatement().build());
+ this.getQuotaLimitsStatement = session.prepare(getQuotaLimitsStatement().build());
+ this.setQuotaLimitStatement = session.prepare(setQuotaLimitStatement().build());
+ this.deleteQuotaLimitStatement = session.prepare((deleteQuotaLimitStatement().build()));
+ }
+
+ public Mono<QuotaLimit> getQuotaLimit(QuotaLimitKey quotaKey) {
+ return queryExecutor.executeSingleRow(getQuotaLimitStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaKey.getQuotaComponent().getValue())
+ .setString(QUOTA_SCOPE, quotaKey.getQuotaScope().getValue())
+ .setString(IDENTIFIER, quotaKey.getIdentifier())
+ .setString(QUOTA_TYPE, quotaKey.getQuotaType().getValue()))
+ .map(row -> convertRowToModel(row));
+ }
+
+ public Flux<QuotaLimit> getQuotaLimits(QuotaComponent quotaComponent, QuotaScope quotaScope, String identifier) {
+ return queryExecutor.executeRows(getQuotaLimitsStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaComponent.getValue())
+ .setString(QUOTA_SCOPE, quotaScope.getValue())
+ .setString(IDENTIFIER, identifier))
+ .map(row -> convertRowToModel(row));
+ }
+
+ public Mono<Void> setQuotaLimit(QuotaLimit quotaLimit) {
+ return queryExecutor.executeVoid(setQuotaLimitStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaLimit.getQuotaComponent().getValue())
+ .setString(QUOTA_SCOPE, quotaLimit.getQuotaScope().getValue())
+ .setString(IDENTIFIER, quotaLimit.getIdentifier())
+ .setString(QUOTA_TYPE, quotaLimit.getQuotaType().getValue())
+ .setLong(QUOTA_LIMIT, quotaLimit.getQuotaLimit().orElse(null)));
+ }
+
+ public Mono<Void> deleteQuotaLimit(QuotaLimitKey quotaKey) {
+ return queryExecutor.executeVoid(deleteQuotaLimitStatement.bind()
+ .setString(QUOTA_COMPONENT, quotaKey.getQuotaComponent().getValue())
+ .setString(QUOTA_SCOPE, quotaKey.getQuotaScope().getValue())
+ .setString(IDENTIFIER, quotaKey.getIdentifier())
+ .setString(QUOTA_TYPE, quotaKey.getQuotaType().getValue()));
+ }
+
+ private Select getQuotaLimitStatement() {
+ return selectFrom(TABLE_NAME)
+ .all()
+ .where(column(IDENTIFIER).isEqualTo(bindMarker(IDENTIFIER)),
+ column(QUOTA_COMPONENT).isEqualTo(bindMarker(QUOTA_COMPONENT)),
+ column(QUOTA_TYPE).isEqualTo(bindMarker(QUOTA_TYPE)),
+ column(QUOTA_SCOPE).isEqualTo(bindMarker(QUOTA_SCOPE)));
+ }
+
+ private Select getQuotaLimitsStatement() {
+ return selectFrom(TABLE_NAME)
+ .all()
+ .where(column(IDENTIFIER).isEqualTo(bindMarker(IDENTIFIER)),
+ column(QUOTA_COMPONENT).isEqualTo(bindMarker(QUOTA_COMPONENT)),
+ column(QUOTA_SCOPE).isEqualTo(bindMarker(QUOTA_SCOPE)));
+ }
+
+ private Insert setQuotaLimitStatement() {
+ return insertInto(TABLE_NAME)
+ .value(IDENTIFIER, bindMarker(IDENTIFIER))
+ .value(QUOTA_COMPONENT, bindMarker(QUOTA_COMPONENT))
+ .value(QUOTA_TYPE, bindMarker(QUOTA_TYPE))
+ .value(QUOTA_SCOPE, bindMarker(QUOTA_SCOPE))
+ .value(QUOTA_LIMIT, bindMarker(QUOTA_LIMIT));
+ }
+
+ private Delete deleteQuotaLimitStatement() {
+ return deleteFrom(TABLE_NAME)
+ .where(column(IDENTIFIER).isEqualTo(bindMarker(IDENTIFIER)),
+ column(QUOTA_COMPONENT).isEqualTo(bindMarker(QUOTA_COMPONENT)),
+ column(QUOTA_TYPE).isEqualTo(bindMarker(QUOTA_TYPE)),
+ column(QUOTA_SCOPE).isEqualTo(bindMarker(QUOTA_SCOPE)));
+ }
+
+ private QuotaLimit convertRowToModel(Row row) {
+ return QuotaLimit.builder().quotaComponent(QuotaComponent.of(row.get(QUOTA_COMPONENT, String.class)))
+ .quotaScope(QuotaScope.of(row.get(QUOTA_SCOPE, String.class)))
+ .identifier(row.get(IDENTIFIER, String.class))
+ .quotaType(QuotaType.of(row.get(QUOTA_TYPE, String.class)))
+ .quotaLimit(row.get(QUOTA_LIMIT, Long.class)).build();
+ }
+
+}
\ No newline at end of file
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaCurrentValueTable.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaCurrentValueTable.java
new file mode 100644
index 0000000000..5d3c088761
--- /dev/null
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaCurrentValueTable.java
@@ -0,0 +1,31 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mailbox.cassandra.table;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+
+public interface CassandraQuotaCurrentValueTable {
+ String TABLE_NAME = "quotaCurrentValue";
+
+ CqlIdentifier IDENTIFIER = CqlIdentifier.fromCql("identifier");
+ CqlIdentifier QUOTA_COMPONENT = CqlIdentifier.fromCql("quotaComponent");
+ CqlIdentifier QUOTA_TYPE = CqlIdentifier.fromCql("quotaType");
+ CqlIdentifier CURRENT_VALUE = CqlIdentifier.fromCql("currentValue");
+}
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaLimitTable.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaLimitTable.java
new file mode 100644
index 0000000000..b9215e3d31
--- /dev/null
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaLimitTable.java
@@ -0,0 +1,32 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mailbox.cassandra.table;
+
+import com.datastax.oss.driver.api.core.CqlIdentifier;
+
+public interface CassandraQuotaLimitTable {
+ String TABLE_NAME = "quotaLimit";
+
+ CqlIdentifier IDENTIFIER = CqlIdentifier.fromCql("identifier");
+ CqlIdentifier QUOTA_COMPONENT = CqlIdentifier.fromCql("quotaComponent");
+ CqlIdentifier QUOTA_TYPE = CqlIdentifier.fromCql("quotaType");
+ CqlIdentifier QUOTA_SCOPE = CqlIdentifier.fromCql("quotaScope");
+ CqlIdentifier QUOTA_LIMIT = CqlIdentifier.fromCql("quotaLimit");
+}
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaCurrentValueDaoTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaCurrentValueDaoTest.java
new file mode 100644
index 0000000000..447cca50c0
--- /dev/null
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaCurrentValueDaoTest.java
@@ -0,0 +1,136 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mailbox.cassandra.quota;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatRuntimeException;
+
+import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.backends.cassandra.components.CassandraModule;
+import org.apache.james.core.Username;
+import org.apache.james.core.quota.QuotaComponent;
+import org.apache.james.core.quota.QuotaCurrentValue;
+import org.apache.james.core.quota.QuotaType;
+import org.apache.james.mailbox.cassandra.mail.utils.GuiceUtils;
+import org.apache.james.mailbox.cassandra.modules.CassandraQuotaModule;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class CassandraQuotaCurrentValueDaoTest {
+
+ private static final CassandraQuotaCurrentValueDao.QuotaKey QUOTA_KEY
+ = CassandraQuotaCurrentValueDao.QuotaKey.of(QuotaComponent.MAILBOX, "james@abc.com", QuotaType.SIZE);
+
+ private CassandraQuotaCurrentValueDao cassandraQuotaCurrentValueDao;
+
+ @RegisterExtension
+ static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraModule.aggregateModules(CassandraQuotaModule.MODULE));
+
+ @BeforeEach
+ private void setup() {
+ cassandraQuotaCurrentValueDao = GuiceUtils.testInjector(cassandraCluster.getCassandraCluster()).getInstance(CassandraQuotaCurrentValueDao.class);
+ resetCounterToZero();
+ }
+
+ private void resetCounterToZero() {
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 0).block();
+ QuotaCurrentValue quotaCurrentValue = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ cassandraQuotaCurrentValueDao.decrease(QUOTA_KEY, quotaCurrentValue.getCurrentValue()).block();
+ QuotaCurrentValue actual = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ assertThat(actual.getCurrentValue()).isEqualTo(0l);
+ }
+
+ @Test
+ void increaseQuotaCurrentValueShouldCreateNewRowSuccessfully() {
+ QuotaCurrentValue expected = QuotaCurrentValue.builder().quotaComponent(QUOTA_KEY.getQuotaComponent())
+ .identifier(QUOTA_KEY.getIdentifier()).quotaType(QUOTA_KEY.getQuotaType()).currentValue(100l).build();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 100l).block();
+
+ QuotaCurrentValue actual = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ void increaseQuotaCurrentValueShouldCreateNewRowSuccessfullyWhenIncreaseAmountIsZero() {
+ QuotaCurrentValue expected = QuotaCurrentValue.builder().quotaComponent(QUOTA_KEY.getQuotaComponent())
+ .identifier(QUOTA_KEY.getIdentifier()).quotaType(QUOTA_KEY.getQuotaType()).currentValue(0l).build();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 0l).block();
+
+ QuotaCurrentValue actual = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ void increaseQuotaCurrentValueShouldIncreaseValueSuccessfully() {
+ QuotaCurrentValue expected = QuotaCurrentValue.builder().quotaComponent(QUOTA_KEY.getQuotaComponent())
+ .identifier(QUOTA_KEY.getIdentifier()).quotaType(QUOTA_KEY.getQuotaType()).currentValue(200l).build();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 100l).block();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 100l).block();
+
+ QuotaCurrentValue actual = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ void increaseQuotaCurrentValueShouldDecreaseValueSuccessfullyWhenIncreaseAmountIsNegative() {
+ QuotaCurrentValue expected = QuotaCurrentValue.builder().quotaComponent(QUOTA_KEY.getQuotaComponent())
+ .identifier(QUOTA_KEY.getIdentifier()).quotaType(QUOTA_KEY.getQuotaType()).currentValue(100l).build();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 200l).block();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, -100l).block();
+
+ QuotaCurrentValue actual = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ void decreaseQuotaCurrentValueShouldDecreaseValueSuccessfully() {
+ QuotaCurrentValue expected = QuotaCurrentValue.builder().quotaComponent(QUOTA_KEY.getQuotaComponent())
+ .identifier(QUOTA_KEY.getIdentifier()).quotaType(QUOTA_KEY.getQuotaType()).currentValue(100l).build();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 200l).block();
+ cassandraQuotaCurrentValueDao.decrease(QUOTA_KEY, 100l).block();
+
+ QuotaCurrentValue actual = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ assertThat(actual).isEqualTo(expected);
+ }
+
+ @Test
+ void deleteQuotaCurrentValueShouldDeleteSuccessfully() {
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 100l).block();
+ cassandraQuotaCurrentValueDao.deleteQuotaCurrentValue(QUOTA_KEY).block();
+
+ QuotaCurrentValue actual = cassandraQuotaCurrentValueDao.getQuotaCurrentValue(QUOTA_KEY).block();
+ assertThat(actual).isNull();
+ }
+
+ @Test
+ void increaseQuotaCurrentValueShouldNotThrowExceptionWhenQueryExecutorThrowException() {
+ cassandraCluster.pause();
+ cassandraQuotaCurrentValueDao.increase(QUOTA_KEY, 100l).block();
+ cassandraCluster.unpause();
+ }
+
+ @Test
+ void decreaseQuotaCurrentValueShouldNotThrowExceptionWhenQueryExecutorThrowException() {
+ cassandraCluster.pause();
+ cassandraQuotaCurrentValueDao.decrease(QUOTA_KEY, 100l).block();
+ cassandraCluster.unpause();
+ }
+}
\ No newline at end of file
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaLimitDaoTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaLimitDaoTest.java
new file mode 100644
index 0000000000..15c9f9cdae
--- /dev/null
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaLimitDaoTest.java
@@ -0,0 +1,91 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.mailbox.cassandra.quota;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.backends.cassandra.components.CassandraModule;
+import org.apache.james.blob.cassandra.CassandraBlobModule;
+import org.apache.james.core.quota.QuotaComponent;
+import org.apache.james.core.quota.QuotaLimit;
+import org.apache.james.core.quota.QuotaScope;
+import org.apache.james.core.quota.QuotaType;
+import org.apache.james.mailbox.cassandra.mail.utils.GuiceUtils;
+import org.apache.james.mailbox.cassandra.modules.CassandraQuotaModule;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class CassandraQuotaLimitDaoTest {
+
+ private CassandraQuotaLimitDao cassandraQuotaLimitDao;
+
+ @RegisterExtension
+ static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraModule.aggregateModules(CassandraQuotaModule.MODULE));
+
+ @BeforeEach
+ private void setup() {
+ cassandraQuotaLimitDao = GuiceUtils.testInjector(cassandraCluster.getCassandraCluster()).getInstance(CassandraQuotaLimitDao.class);
+ }
+
+ @Test
+ void getQuotaLimitsShouldGetSomeQuotaLimitsSuccessfully() {
+ QuotaLimit expectedOne = QuotaLimit.builder().quotaComponent(QuotaComponent.MAILBOX).quotaScope(QuotaScope.DOMAIN).identifier("A").quotaType(QuotaType.COUNT).quotaLimit(200l).build();
+ QuotaLimit expectedTwo = QuotaLimit.builder().quotaComponent(QuotaComponent.MAILBOX).quotaScope(QuotaScope.DOMAIN).identifier("A").quotaType(QuotaType.SIZE).quotaLimit(100l).build();
+ cassandraQuotaLimitDao.setQuotaLimit(expectedOne).block();
+ cassandraQuotaLimitDao.setQuotaLimit(expectedTwo).block();
+
+ assertThat(cassandraQuotaLimitDao.getQuotaLimits(QuotaComponent.MAILBOX, QuotaScope.DOMAIN, "A").collectList().block())
+ .containsExactlyInAnyOrder(expectedOne, expectedTwo);
+ }
+
+ @Test
+ void setQuotaLimitShouldSaveObjectSuccessfully() {
+ QuotaLimit expected = QuotaLimit.builder().quotaComponent(QuotaComponent.MAILBOX).quotaScope(QuotaScope.DOMAIN).identifier("A").quotaType(QuotaType.COUNT).quotaLimit(100l).build();
+ cassandraQuotaLimitDao.setQuotaLimit(expected).block();
+
+ assertThat(cassandraQuotaLimitDao.getQuotaLimit(CassandraQuotaLimitDao.QuotaLimitKey.of(QuotaComponent.MAILBOX, QuotaScope.DOMAIN, "A", QuotaType.COUNT)).block())
+ .isEqualTo(expected);
+ }
+
+ @Test
+ void setQuotaLimitShouldSaveObjectSuccessfullyWhenLimitIsMinusOne() {
+ QuotaLimit expected = QuotaLimit.builder().quotaComponent(QuotaComponent.MAILBOX).quotaScope(QuotaScope.DOMAIN).identifier("A").quotaType(QuotaType.COUNT).quotaLimit(-1l).build();
+ cassandraQuotaLimitDao.setQuotaLimit(expected).block();
+
+ assertThat(cassandraQuotaLimitDao.getQuotaLimit(CassandraQuotaLimitDao.QuotaLimitKey.of(QuotaComponent.MAILBOX, QuotaScope.DOMAIN, "A", QuotaType.COUNT)).block())
+ .isEqualTo(expected);
+ }
+
+ @Test
+ void deleteQuotaLimitShouldDeleteObjectSuccessfully() {
+ QuotaLimit quotaLimit = QuotaLimit.builder().quotaComponent(QuotaComponent.MAILBOX).quotaScope(QuotaScope.DOMAIN).identifier("A").quotaType(QuotaType.COUNT).quotaLimit(100l).build();
+ cassandraQuotaLimitDao.setQuotaLimit(quotaLimit).block();
+ cassandraQuotaLimitDao.deleteQuotaLimit(CassandraQuotaLimitDao.QuotaLimitKey.of(QuotaComponent.MAILBOX, QuotaScope.DOMAIN, "A", QuotaType.COUNT)).block();
+
+ assertThat(cassandraQuotaLimitDao.getQuotaLimit(CassandraQuotaLimitDao.QuotaLimitKey.of(QuotaComponent.MAILBOX, QuotaScope.DOMAIN, "A", QuotaType.COUNT)).block())
+ .isNull();
+ }
+
+}
\ No newline at end of file
diff --git a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraQuotaModule.java b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraQuotaModule.java
index a8eb5351ff..f79fc9d1d6 100644
--- a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraQuotaModule.java
+++ b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/mailbox/CassandraQuotaModule.java
@@ -26,6 +26,8 @@ import org.apache.james.mailbox.cassandra.quota.CassandraGlobalMaxQuotaDao;
import org.apache.james.mailbox.cassandra.quota.CassandraPerDomainMaxQuotaDao;
import org.apache.james.mailbox.cassandra.quota.CassandraPerUserMaxQuotaDao;
import org.apache.james.mailbox.cassandra.quota.CassandraPerUserMaxQuotaManager;
+import org.apache.james.mailbox.cassandra.quota.CassandraQuotaCurrentValueDao;
+import org.apache.james.mailbox.cassandra.quota.CassandraQuotaLimitDao;
import org.apache.james.mailbox.quota.CurrentQuotaManager;
import org.apache.james.mailbox.quota.MaxQuotaManager;
import org.apache.james.mailbox.quota.QuotaManager;
@@ -46,6 +48,8 @@ public class CassandraQuotaModule extends AbstractModule {
@Override
protected void configure() {
bind(CassandraCurrentQuotaManager.class).in(Scopes.SINGLETON);
+ bind(CassandraQuotaLimitDao.class).in(Scopes.SINGLETON);
+ bind(CassandraQuotaCurrentValueDao.class).in(Scopes.SINGLETON);
bind(CassandraGlobalMaxQuotaDao.class).in(Scopes.SINGLETON);
bind(CassandraPerDomainMaxQuotaDao.class).in(Scopes.SINGLETON);
bind(CassandraPerUserMaxQuotaDao.class).in(Scopes.SINGLETON);
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org