jstastny-cz commented on code in PR #2328:
URL:
https://github.com/apache/incubator-kie-kogito-apps/pull/2328#discussion_r3497381235
##########
data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java:
##########
@@ -156,17 +185,145 @@ protected final Predicate
buildPredicateFunction(AttributeFilter filter, Root<E>
return builder
.lessThanOrEqualTo(getAttributePath(root,
filter.getAttribute()), (Comparable) filter.getValue());
case OR:
- return builder.or(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, false, false);
case AND:
- return builder.and(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, true, false);
case NOT:
- return builder.not(filterPredicateFunction(root,
builder).apply((AttributeFilter<?>) filter.getValue()));
+ AttributeFilter<?> innerFilter = (AttributeFilter<?>)
filter.getValue();
+
+ // Handle NOT with AND/OR: Apply De Morgan's Law by negating
and flipping the operator
+ if (innerFilter.getCondition() == FilterCondition.AND ||
innerFilter.getCondition() == FilterCondition.OR) {
+ // NOT (A AND B) = NOT A OR NOT B, NOT (A OR B) = NOT A
AND NOT B
+ boolean flipToAnd = innerFilter.getCondition() ==
FilterCondition.OR;
+ return buildGroupedPredicate(innerFilter, root, builder,
criteriaQuery, flipToAnd, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute()) &&
+ (innerFilter.getCondition() ==
FilterCondition.CONTAINS ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ALL ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ANY ||
+ innerFilter.getCondition() ==
FilterCondition.EQUAL)) {
+ return buildCollectionPredicate(innerFilter, root,
builder, criteriaQuery, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute())) {
+ return buildNegatedCollectionPredicate(innerFilter, root,
builder, criteriaQuery);
+ }
+
+ return builder.not(filterPredicateFunction(root, builder,
criteriaQuery).apply(innerFilter));
default:
return null;
}
}
+ /**
+ * Builds predicates for AND/OR, grouping CONTAINS filters on same
collection into single EXISTS.
+ *
+ * @param isNegated if true, negates each predicate (for NOT operations
with De Morgan's Law)
+ */
+ private Predicate buildGroupedPredicate(AttributeFilter<?> filter, Root<E>
root,
+ CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean
isAnd, boolean isNegated) {
+
+ List<AttributeFilter<?>> nestedFilters = (List<AttributeFilter<?>>)
filter.getValue();
+
+ java.util.Map<String, List<AttributeFilter<?>>> groups = new
java.util.HashMap<>();
+ List<Predicate> otherPredicates = new ArrayList<>();
+
+ for (AttributeFilter<?> f : nestedFilters) {
+ if (f.getAttribute() != null &&
isCollectionAttribute(f.getAttribute()) &&
+ (f.getCondition() == FilterCondition.CONTAINS ||
Review Comment:
the same if condition is in the method above too, so I'd say it's a
categorization that we want to have explicitly defined in a single place.
##########
data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java:
##########
@@ -156,17 +185,145 @@ protected final Predicate
buildPredicateFunction(AttributeFilter filter, Root<E>
return builder
.lessThanOrEqualTo(getAttributePath(root,
filter.getAttribute()), (Comparable) filter.getValue());
case OR:
- return builder.or(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, false, false);
case AND:
- return builder.and(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, true, false);
case NOT:
- return builder.not(filterPredicateFunction(root,
builder).apply((AttributeFilter<?>) filter.getValue()));
+ AttributeFilter<?> innerFilter = (AttributeFilter<?>)
filter.getValue();
+
+ // Handle NOT with AND/OR: Apply De Morgan's Law by negating
and flipping the operator
+ if (innerFilter.getCondition() == FilterCondition.AND ||
innerFilter.getCondition() == FilterCondition.OR) {
+ // NOT (A AND B) = NOT A OR NOT B, NOT (A OR B) = NOT A
AND NOT B
+ boolean flipToAnd = innerFilter.getCondition() ==
FilterCondition.OR;
+ return buildGroupedPredicate(innerFilter, root, builder,
criteriaQuery, flipToAnd, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute()) &&
+ (innerFilter.getCondition() ==
FilterCondition.CONTAINS ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ALL ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ANY ||
+ innerFilter.getCondition() ==
FilterCondition.EQUAL)) {
+ return buildCollectionPredicate(innerFilter, root,
builder, criteriaQuery, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute())) {
+ return buildNegatedCollectionPredicate(innerFilter, root,
builder, criteriaQuery);
+ }
+
+ return builder.not(filterPredicateFunction(root, builder,
criteriaQuery).apply(innerFilter));
default:
return null;
}
}
+ /**
+ * Builds predicates for AND/OR, grouping CONTAINS filters on same
collection into single EXISTS.
+ *
+ * @param isNegated if true, negates each predicate (for NOT operations
with De Morgan's Law)
+ */
+ private Predicate buildGroupedPredicate(AttributeFilter<?> filter, Root<E>
root,
+ CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean
isAnd, boolean isNegated) {
+
+ List<AttributeFilter<?>> nestedFilters = (List<AttributeFilter<?>>)
filter.getValue();
+
+ java.util.Map<String, List<AttributeFilter<?>>> groups = new
java.util.HashMap<>();
+ List<Predicate> otherPredicates = new ArrayList<>();
+
+ for (AttributeFilter<?> f : nestedFilters) {
+ if (f.getAttribute() != null &&
isCollectionAttribute(f.getAttribute()) &&
+ (f.getCondition() == FilterCondition.CONTAINS ||
+ f.getCondition() == FilterCondition.CONTAINS_ALL ||
+ f.getCondition() == FilterCondition.CONTAINS_ANY ||
+ f.getCondition() == FilterCondition.EQUAL)) {
+ String collection = f.getAttribute().split("\\.")[0];
+ groups.computeIfAbsent(collection, k -> new
ArrayList<>()).add(f);
+ } else {
+ Predicate pred = filterPredicateFunction(root, builder,
criteriaQuery).apply(f);
+ otherPredicates.add(isNegated ? builder.not(pred) : pred);
+ }
+ }
+
+ List<Predicate> allPredicates = new ArrayList<>(otherPredicates);
+ for (List<AttributeFilter<?>> groupFilters : groups.values()) {
+ if (groupFilters.size() == 1) {
+
allPredicates.add(buildCollectionPredicate(groupFilters.get(0), root, builder,
criteriaQuery, isNegated));
+ } else {
+
allPredicates.add(buildMultiFilterCollectionPredicate(groupFilters, root,
builder, criteriaQuery, isAnd, isNegated));
+ }
+ }
+
+ return isAnd ? builder.and(allPredicates.toArray(new Predicate[0]))
+ : builder.or(allPredicates.toArray(new Predicate[0]));
+ }
+
+ /**
+ * Builds single EXISTS with HAVING for multiple CONTAINS filters on same
collection.
+ * Example: [{ comments.id: "A" }, { comments.status: "ACTIVE" }]
+ * → EXISTS(... HAVING SUM(id='A')>0 AND SUM(status='ACTIVE')>0)
+ *
+ * @param isNegated if true, uses = 0 instead of > 0 and applies De
Morgan's Law
+ */
+ private Predicate
buildMultiFilterCollectionPredicate(List<AttributeFilter<?>> filters, Root<E>
root,
+ CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean
combineWithAnd, boolean isNegated) {
+
+ String collectionName = filters.get(0).getAttribute().split("\\.")[0];
+
+ Subquery<Integer> subquery = criteriaQuery.subquery(Integer.class);
+ Root<E> subRoot = subquery.from(entityClass);
+ Join<?, ?> collectionJoin = subRoot.join(collectionName);
+
+ List<Predicate> havingConditions = new ArrayList<>();
+
+ for (AttributeFilter<?> filter : filters) {
+ String property = filter.getAttribute().split("\\.")[1];
+ List<Object> values = (filter.getCondition() ==
FilterCondition.CONTAINS || filter.getCondition() == FilterCondition.EQUAL)
Review Comment:
Perhaps even each multipleValues condition could carry the information of
what is the "joining" operation, if AND or OR ... but that's just a tiny detail.
##########
data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java:
##########
@@ -156,17 +185,145 @@ protected final Predicate
buildPredicateFunction(AttributeFilter filter, Root<E>
return builder
.lessThanOrEqualTo(getAttributePath(root,
filter.getAttribute()), (Comparable) filter.getValue());
case OR:
- return builder.or(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, false, false);
case AND:
- return builder.and(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, true, false);
case NOT:
- return builder.not(filterPredicateFunction(root,
builder).apply((AttributeFilter<?>) filter.getValue()));
+ AttributeFilter<?> innerFilter = (AttributeFilter<?>)
filter.getValue();
+
+ // Handle NOT with AND/OR: Apply De Morgan's Law by negating
and flipping the operator
+ if (innerFilter.getCondition() == FilterCondition.AND ||
innerFilter.getCondition() == FilterCondition.OR) {
+ // NOT (A AND B) = NOT A OR NOT B, NOT (A OR B) = NOT A
AND NOT B
+ boolean flipToAnd = innerFilter.getCondition() ==
FilterCondition.OR;
+ return buildGroupedPredicate(innerFilter, root, builder,
criteriaQuery, flipToAnd, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute()) &&
+ (innerFilter.getCondition() ==
FilterCondition.CONTAINS ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ALL ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ANY ||
+ innerFilter.getCondition() ==
FilterCondition.EQUAL)) {
+ return buildCollectionPredicate(innerFilter, root,
builder, criteriaQuery, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute())) {
+ return buildNegatedCollectionPredicate(innerFilter, root,
builder, criteriaQuery);
+ }
+
+ return builder.not(filterPredicateFunction(root, builder,
criteriaQuery).apply(innerFilter));
default:
return null;
}
}
+ /**
+ * Builds predicates for AND/OR, grouping CONTAINS filters on same
collection into single EXISTS.
+ *
+ * @param isNegated if true, negates each predicate (for NOT operations
with De Morgan's Law)
+ */
+ private Predicate buildGroupedPredicate(AttributeFilter<?> filter, Root<E>
root,
+ CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean
isAnd, boolean isNegated) {
+
+ List<AttributeFilter<?>> nestedFilters = (List<AttributeFilter<?>>)
filter.getValue();
+
+ java.util.Map<String, List<AttributeFilter<?>>> groups = new
java.util.HashMap<>();
+ List<Predicate> otherPredicates = new ArrayList<>();
+
+ for (AttributeFilter<?> f : nestedFilters) {
+ if (f.getAttribute() != null &&
isCollectionAttribute(f.getAttribute()) &&
+ (f.getCondition() == FilterCondition.CONTAINS ||
+ f.getCondition() == FilterCondition.CONTAINS_ALL ||
+ f.getCondition() == FilterCondition.CONTAINS_ANY ||
+ f.getCondition() == FilterCondition.EQUAL)) {
+ String collection = f.getAttribute().split("\\.")[0];
+ groups.computeIfAbsent(collection, k -> new
ArrayList<>()).add(f);
+ } else {
+ Predicate pred = filterPredicateFunction(root, builder,
criteriaQuery).apply(f);
+ otherPredicates.add(isNegated ? builder.not(pred) : pred);
+ }
+ }
+
+ List<Predicate> allPredicates = new ArrayList<>(otherPredicates);
+ for (List<AttributeFilter<?>> groupFilters : groups.values()) {
+ if (groupFilters.size() == 1) {
+
allPredicates.add(buildCollectionPredicate(groupFilters.get(0), root, builder,
criteriaQuery, isNegated));
+ } else {
+
allPredicates.add(buildMultiFilterCollectionPredicate(groupFilters, root,
builder, criteriaQuery, isAnd, isNegated));
+ }
Review Comment:
what would happen if we call buildMultiFilterCollectionPredicate on
groupFilters size=1 ? I mean, don't we have the "single-group" method while it
could be done using the other?
##########
data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java:
##########
@@ -156,17 +185,145 @@ protected final Predicate
buildPredicateFunction(AttributeFilter filter, Root<E>
return builder
.lessThanOrEqualTo(getAttributePath(root,
filter.getAttribute()), (Comparable) filter.getValue());
case OR:
- return builder.or(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, false, false);
case AND:
- return builder.and(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, true, false);
case NOT:
- return builder.not(filterPredicateFunction(root,
builder).apply((AttributeFilter<?>) filter.getValue()));
+ AttributeFilter<?> innerFilter = (AttributeFilter<?>)
filter.getValue();
+
+ // Handle NOT with AND/OR: Apply De Morgan's Law by negating
and flipping the operator
+ if (innerFilter.getCondition() == FilterCondition.AND ||
innerFilter.getCondition() == FilterCondition.OR) {
+ // NOT (A AND B) = NOT A OR NOT B, NOT (A OR B) = NOT A
AND NOT B
+ boolean flipToAnd = innerFilter.getCondition() ==
FilterCondition.OR;
+ return buildGroupedPredicate(innerFilter, root, builder,
criteriaQuery, flipToAnd, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute()) &&
+ (innerFilter.getCondition() ==
FilterCondition.CONTAINS ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ALL ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ANY ||
+ innerFilter.getCondition() ==
FilterCondition.EQUAL)) {
+ return buildCollectionPredicate(innerFilter, root,
builder, criteriaQuery, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute())) {
+ return buildNegatedCollectionPredicate(innerFilter, root,
builder, criteriaQuery);
+ }
+
+ return builder.not(filterPredicateFunction(root, builder,
criteriaQuery).apply(innerFilter));
default:
return null;
}
}
+ /**
+ * Builds predicates for AND/OR, grouping CONTAINS filters on same
collection into single EXISTS.
+ *
+ * @param isNegated if true, negates each predicate (for NOT operations
with De Morgan's Law)
+ */
+ private Predicate buildGroupedPredicate(AttributeFilter<?> filter, Root<E>
root,
+ CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean
isAnd, boolean isNegated) {
+
+ List<AttributeFilter<?>> nestedFilters = (List<AttributeFilter<?>>)
filter.getValue();
+
+ java.util.Map<String, List<AttributeFilter<?>>> groups = new
java.util.HashMap<>();
+ List<Predicate> otherPredicates = new ArrayList<>();
+
+ for (AttributeFilter<?> f : nestedFilters) {
+ if (f.getAttribute() != null &&
isCollectionAttribute(f.getAttribute()) &&
+ (f.getCondition() == FilterCondition.CONTAINS ||
+ f.getCondition() == FilterCondition.CONTAINS_ALL ||
+ f.getCondition() == FilterCondition.CONTAINS_ANY ||
+ f.getCondition() == FilterCondition.EQUAL)) {
+ String collection = f.getAttribute().split("\\.")[0];
+ groups.computeIfAbsent(collection, k -> new
ArrayList<>()).add(f);
+ } else {
+ Predicate pred = filterPredicateFunction(root, builder,
criteriaQuery).apply(f);
+ otherPredicates.add(isNegated ? builder.not(pred) : pred);
+ }
+ }
+
+ List<Predicate> allPredicates = new ArrayList<>(otherPredicates);
+ for (List<AttributeFilter<?>> groupFilters : groups.values()) {
+ if (groupFilters.size() == 1) {
+
allPredicates.add(buildCollectionPredicate(groupFilters.get(0), root, builder,
criteriaQuery, isNegated));
+ } else {
+
allPredicates.add(buildMultiFilterCollectionPredicate(groupFilters, root,
builder, criteriaQuery, isAnd, isNegated));
+ }
+ }
+
+ return isAnd ? builder.and(allPredicates.toArray(new Predicate[0]))
+ : builder.or(allPredicates.toArray(new Predicate[0]));
+ }
+
+ /**
+ * Builds single EXISTS with HAVING for multiple CONTAINS filters on same
collection.
+ * Example: [{ comments.id: "A" }, { comments.status: "ACTIVE" }]
+ * → EXISTS(... HAVING SUM(id='A')>0 AND SUM(status='ACTIVE')>0)
+ *
+ * @param isNegated if true, uses = 0 instead of > 0 and applies De
Morgan's Law
+ */
+ private Predicate
buildMultiFilterCollectionPredicate(List<AttributeFilter<?>> filters, Root<E>
root,
+ CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean
combineWithAnd, boolean isNegated) {
+
+ String collectionName = filters.get(0).getAttribute().split("\\.")[0];
+
+ Subquery<Integer> subquery = criteriaQuery.subquery(Integer.class);
+ Root<E> subRoot = subquery.from(entityClass);
+ Join<?, ?> collectionJoin = subRoot.join(collectionName);
+
+ List<Predicate> havingConditions = new ArrayList<>();
+
+ for (AttributeFilter<?> filter : filters) {
+ String property = filter.getAttribute().split("\\.")[1];
+ List<Object> values = (filter.getCondition() ==
FilterCondition.CONTAINS || filter.getCondition() == FilterCondition.EQUAL)
Review Comment:
Could we perhaps alter AttributeFilter class by adding method that tells us
if the value it holds is collection or single value?
So that then we'd be able to call `filter.isMultipleValues()` or similar? I
noticed we have this conditional in 2 places at least. Or perhaps at least add
static method to AttributeFilter, that accepts FilterCondition and tells us if
expects multiple values or not? With that we could also FilterCondition enum to
hold for each operation the boolean and add a method, e.g. IN would expect
multiple values, CONTAINS would expect single value, etc. The AttributeFilter's
static method then can only invoke FilterCondition#expectsMultipleValues() (or
similar) .
##########
data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java:
##########
@@ -156,17 +185,145 @@ protected final Predicate
buildPredicateFunction(AttributeFilter filter, Root<E>
return builder
.lessThanOrEqualTo(getAttributePath(root,
filter.getAttribute()), (Comparable) filter.getValue());
case OR:
- return builder.or(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, false, false);
case AND:
- return builder.and(getRecursivePredicate(filter, root,
builder).toArray(new Predicate[] {}));
+ return buildGroupedPredicate(filter, root, builder,
criteriaQuery, true, false);
case NOT:
- return builder.not(filterPredicateFunction(root,
builder).apply((AttributeFilter<?>) filter.getValue()));
+ AttributeFilter<?> innerFilter = (AttributeFilter<?>)
filter.getValue();
+
+ // Handle NOT with AND/OR: Apply De Morgan's Law by negating
and flipping the operator
+ if (innerFilter.getCondition() == FilterCondition.AND ||
innerFilter.getCondition() == FilterCondition.OR) {
+ // NOT (A AND B) = NOT A OR NOT B, NOT (A OR B) = NOT A
AND NOT B
+ boolean flipToAnd = innerFilter.getCondition() ==
FilterCondition.OR;
+ return buildGroupedPredicate(innerFilter, root, builder,
criteriaQuery, flipToAnd, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute()) &&
+ (innerFilter.getCondition() ==
FilterCondition.CONTAINS ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ALL ||
+ innerFilter.getCondition() ==
FilterCondition.CONTAINS_ANY ||
+ innerFilter.getCondition() ==
FilterCondition.EQUAL)) {
+ return buildCollectionPredicate(innerFilter, root,
builder, criteriaQuery, true);
+ }
+
+ if (innerFilter.getAttribute() != null &&
isCollectionAttribute(innerFilter.getAttribute())) {
+ return buildNegatedCollectionPredicate(innerFilter, root,
builder, criteriaQuery);
+ }
+
+ return builder.not(filterPredicateFunction(root, builder,
criteriaQuery).apply(innerFilter));
default:
return null;
}
}
+ /**
+ * Builds predicates for AND/OR, grouping CONTAINS filters on same
collection into single EXISTS.
+ *
+ * @param isNegated if true, negates each predicate (for NOT operations
with De Morgan's Law)
+ */
+ private Predicate buildGroupedPredicate(AttributeFilter<?> filter, Root<E>
root,
+ CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean
isAnd, boolean isNegated) {
+
+ List<AttributeFilter<?>> nestedFilters = (List<AttributeFilter<?>>)
filter.getValue();
+
+ java.util.Map<String, List<AttributeFilter<?>>> groups = new
java.util.HashMap<>();
+ List<Predicate> otherPredicates = new ArrayList<>();
+
+ for (AttributeFilter<?> f : nestedFilters) {
+ if (f.getAttribute() != null &&
isCollectionAttribute(f.getAttribute()) &&
+ (f.getCondition() == FilterCondition.CONTAINS ||
Review Comment:
If we're able to name a method nicely, I'd consider extending
FilterCondition enum by a field and method that would return boolean and
simplify this if to `f.getCondition().isXY()` .... is it `isCollection()` what
we're checking here, i.e. filtering conditions based on fact if they are to be
used on collections? Then I'd say it's an obvious candidate for such enum
extension, cause it directly describes the value.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]