This is an automated email from the ASF dual-hosted git repository.
sumitagrawal pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ozone.git
The following commit(s) were added to refs/heads/master by this push:
new 88dd4369e2 HDDS-11423. Implement equals operation for --filter option
to ozone ldb scan (#7167)
88dd4369e2 is described below
commit 88dd4369e2336b9a8a5b07ecf53afbf9af3ec820
Author: Tejaskriya <[email protected]>
AuthorDate: Tue Sep 17 15:40:19 2024 +0530
HDDS-11423. Implement equals operation for --filter option to ozone ldb
scan (#7167)
---
.../src/main/smoketest/debug/ozone-debug-ldb.robot | 93 ++++++++++++
.../org/apache/hadoop/ozone/debug/TestLDBCli.java | 12 ++
.../org/apache/hadoop/ozone/debug/DBScanner.java | 169 +++++++++++++++++++--
.../java/org/apache/hadoop/ozone/utils/Filter.java | 107 +++++++++++++
4 files changed, 370 insertions(+), 11 deletions(-)
diff --git a/hadoop-ozone/dist/src/main/smoketest/debug/ozone-debug-ldb.robot
b/hadoop-ozone/dist/src/main/smoketest/debug/ozone-debug-ldb.robot
new file mode 100644
index 0000000000..e006e154af
--- /dev/null
+++ b/hadoop-ozone/dist/src/main/smoketest/debug/ozone-debug-ldb.robot
@@ -0,0 +1,93 @@
+# 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.
+
+*** Settings ***
+Documentation Test ozone debug ldb CLI
+Library OperatingSystem
+Resource ../lib/os.robot
+Test Timeout 5 minute
+Suite Setup Write keys
+
+*** Variables ***
+${PREFIX} ${EMPTY}
+${VOLUME} cli-debug-volume${PREFIX}
+${BUCKET} cli-debug-bucket
+${DEBUGKEY} debugKey
+${TESTFILE} testfile
+
+*** Keywords ***
+Write keys
+ Run Keyword if '${SECURITY_ENABLED}' == 'true' Kinit test user
testuser testuser.keytab
+ Execute ozone sh volume create ${VOLUME}
+ Execute ozone sh bucket create ${VOLUME}/${BUCKET} -l
OBJECT_STORE
+ Execute dd if=/dev/urandom of=${TEMP_DIR}/${TESTFILE}
bs=100000 count=15
+ Execute ozone sh key put ${VOLUME}/${BUCKET}/${TESTFILE}1
${TEMP_DIR}/${TESTFILE}
+ Execute ozone sh key put ${VOLUME}/${BUCKET}/${TESTFILE}2
${TEMP_DIR}/${TESTFILE}
+ Execute ozone sh key put ${VOLUME}/${BUCKET}/${TESTFILE}3
${TEMP_DIR}/${TESTFILE}
+ Execute ozone sh key addacl -a user:systest:a
${VOLUME}/${BUCKET}/${TESTFILE}3
+
+*** Test Cases ***
+Test ozone debug ldb ls
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db ls
+ Should contain ${output} keyTable
+
+Test ozone debug ldb scan
+ # test count option
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable --count
+ Should Not Be Equal ${output} 0
+ # test valid json for scan command
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable | jq -r '.'
+ Should contain ${output} keyName
+ Should contain ${output} testfile1
+ Should contain ${output} testfile2
+ Should contain ${output} testfile3
+ # test startkey option
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable
--startkey="/cli-debug-volume/cli-debug-bucket/testfile2"
+ Should not contain ${output} testfile1
+ Should contain ${output} testfile2
+ Should contain ${output} testfile3
+ # test endkey option
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable
--endkey="/cli-debug-volume/cli-debug-bucket/testfile2"
+ Should contain ${output} testfile1
+ Should contain ${output} testfile2
+ Should not contain ${output} testfile3
+ # test fields option
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable
--fields="volumeName,bucketName,keyName"
+ Should contain ${output} volumeName
+ Should contain ${output} bucketName
+ Should contain ${output} keyName
+ Should not contain ${output} objectID
+ Should not contain ${output} dataSize
+ Should not contain ${output}
keyLocationVersions
+ # test filter option with one filter
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable --filter="keyName:equals:testfile2"
+ Should not contain ${output} testfile1
+ Should contain ${output} testfile2
+ Should not contain ${output} testfile3
+ # test filter option with one multi-level filter
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable --filter="acls.name:equals:systest"
+ Should not contain ${output} testfile1
+ Should not contain ${output} testfile2
+ Should contain ${output} testfile3
+ # test filter option with multiple filter
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable
--filter="keyName:equals:testfile3,acls.name:equals:systest"
+ Should not contain ${output} testfile1
+ Should not contain ${output} testfile2
+ Should contain ${output} testfile3
+ # test filter option with no records match both filters
+ ${output} = Execute ozone debug ldb
--db=/data/metadata/om.db scan --cf=keyTable
--filter="acls.name:equals:systest,keyName:equals:testfile2"
+ Should not contain ${output} testfile1
+ Should not contain ${output} testfile2
+ Should not contain ${output} testfile3
diff --git
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/debug/TestLDBCli.java
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/debug/TestLDBCli.java
index 7af0b5f9aa..a4327a49bf 100644
---
a/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/debug/TestLDBCli.java
+++
b/hadoop-ozone/integration-test/src/test/java/org/apache/hadoop/ozone/debug/TestLDBCli.java
@@ -170,6 +170,18 @@ public class TestLDBCli {
Named.of("Invalid EndKey key9", Arrays.asList("--endkey", "key9")),
Named.of("Expect key1-key5", Pair.of("key1", "key6"))
),
+ Arguments.of(
+ Named.of(KEY_TABLE, Pair.of(KEY_TABLE, false)),
+ Named.of("Default", Pair.of(0, "")),
+ Named.of("Filter key3", Arrays.asList("--filter",
"keyName:equals:key3")),
+ Named.of("Expect key3", Pair.of("key3", "key4"))
+ ),
+ Arguments.of(
+ Named.of(KEY_TABLE, Pair.of(KEY_TABLE, false)),
+ Named.of("Default", Pair.of(0, "")),
+ Named.of("Filter invalid key", Arrays.asList("--filter",
"keyName:equals:key9")),
+ Named.of("Expect key1-key3", null)
+ ),
Arguments.of(
Named.of(BLOCK_DATA + " V3", Pair.of(BLOCK_DATA, true)),
Named.of("Default", Pair.of(0, "")),
diff --git
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/DBScanner.java
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/DBScanner.java
index 4653aa3eeb..5e1207519a 100644
---
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/DBScanner.java
+++
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/debug/DBScanner.java
@@ -44,6 +44,7 @@ import org.apache.hadoop.hdds.utils.db.managed.ManagedSlice;
import org.apache.hadoop.ozone.OzoneConsts;
import
org.apache.hadoop.ozone.container.common.statemachine.DatanodeConfiguration;
import
org.apache.hadoop.ozone.container.metadata.DatanodeSchemaThreeDBDefinition;
+import org.apache.hadoop.ozone.utils.Filter;
import org.kohsuke.MetaInfServices;
import org.rocksdb.ColumnFamilyDescriptor;
import org.rocksdb.ColumnFamilyHandle;
@@ -128,6 +129,14 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
"eg.) \"name,acls.type\" for showing name and type under acls.")
private String fieldsFilter;
+ @CommandLine.Option(names = {"--filter"},
+ description = "Comma-separated list of \"<field>:<operator>:<value>\"
where " +
+ "<field> is any valid field of the record, " +
+ "<operator> is (EQUALS,MAX or MIN) and " +
+ "<value> is the value of the field. " +
+ "eg.) \"dataSize:equals:1000\" for showing records having the value
1000 for dataSize")
+ private String filter;
+
@CommandLine.Option(names = {"--dnSchema", "--dn-schema", "-d"},
description = "Datanode DB Schema Version: V1/V2/V3",
defaultValue = "V3")
@@ -298,7 +307,7 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
}
Future<Void> future = threadPool.submit(
new Task(dbColumnFamilyDef, batch, logWriter, sequenceId,
- withKey, schemaV3, fieldsFilter));
+ withKey, schemaV3, fieldsFilter, filter));
futures.add(future);
batch = new ArrayList<>(batchSize);
sequenceId++;
@@ -306,7 +315,7 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
}
if (!batch.isEmpty()) {
Future<Void> future = threadPool.submit(new Task(dbColumnFamilyDef,
- batch, logWriter, sequenceId, withKey, schemaV3, fieldsFilter));
+ batch, logWriter, sequenceId, withKey, schemaV3, fieldsFilter,
filter));
futures.add(future);
}
@@ -473,10 +482,12 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
private final boolean withKey;
private final boolean schemaV3;
private String valueFields;
+ private String valueFilter;
+ @SuppressWarnings("checkstyle:parameternumber")
Task(DBColumnFamilyDefinition dbColumnFamilyDefinition,
ArrayList<ByteArrayKeyValue> batch, LogWriter logWriter,
- long sequenceId, boolean withKey, boolean schemaV3, String
valueFields) {
+ long sequenceId, boolean withKey, boolean schemaV3, String
valueFields, String filter) {
this.dbColumnFamilyDefinition = dbColumnFamilyDefinition;
this.batch = batch;
this.logWriter = logWriter;
@@ -484,6 +495,7 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
this.withKey = withKey;
this.schemaV3 = schemaV3;
this.valueFields = valueFields;
+ this.valueFilter = filter;
}
Map<String, Object> getFieldSplit(List<String> fields, Map<String, Object>
fieldMap) {
@@ -504,6 +516,31 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
return fieldMap;
}
+ void getFilterSplit(List<String> fields, Map<String, Filter> fieldMap,
Filter leafValue) throws IOException {
+ int len = fields.size();
+ if (len == 1) {
+ Filter currentValue = fieldMap.get(fields.get(0));
+ if (currentValue != null) {
+ err().println("Cannot pass multiple values for the same field and " +
+ "cannot have filter for both parent and child");
+ throw new IOException("Invalid filter passed");
+ }
+ fieldMap.put(fields.get(0), leafValue);
+ } else {
+ Filter fieldMapGet = fieldMap.computeIfAbsent(fields.get(0), k -> new
Filter());
+ if (fieldMapGet.getValue() != null) {
+ err().println("Cannot pass multiple values for the same field and " +
+ "cannot have filter for both parent and child");
+ throw new IOException("Invalid filter passed");
+ }
+ Map<String, Filter> nextLevel = fieldMapGet.getNextLevel();
+ if (nextLevel == null) {
+ fieldMapGet.setNextLevel(new HashMap<>());
+ }
+ getFilterSplit(fields.subList(1, len), fieldMapGet.getNextLevel(),
leafValue);
+ }
+ }
+
@Override
public Void call() {
try {
@@ -517,6 +554,26 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
}
}
+ Map<String, Filter> fieldsFilterSplitMap = new HashMap<>();
+ if (valueFilter != null) {
+ for (String field : valueFilter.split(",")) {
+ String[] fieldValue = field.split(":");
+ if (fieldValue.length != 3) {
+ err().println("Error: Invalid format for filter \"" + field
+ + "\". Usage: <field>:<operator>:<value>. Ignoring filter
passed");
+ } else {
+ Filter filter = new Filter(fieldValue[1], fieldValue[2]);
+ if (filter.getOperator() == null) {
+ err().println("Error: Invalid format for filter \"" + filter
+ + "\". <operator> can be one of [EQUALS,MIN,MAX]. Ignoring
filter passed");
+ } else {
+ String[] subfields = fieldValue[0].split("\\.");
+ getFilterSplit(Arrays.asList(subfields), fieldsFilterSplitMap,
filter);
+ }
+ }
+ }
+ }
+
for (ByteArrayKeyValue byteArrayKeyValue : batch) {
StringBuilder sb = new StringBuilder();
if (!(sequenceId == FIRST_SEQUENCE_ID && results.isEmpty())) {
@@ -552,9 +609,14 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
Object o = dbColumnFamilyDefinition.getValueCodec()
.fromPersistedFormat(byteArrayKeyValue.getValue());
+ if (valueFilter != null &&
+ !checkFilteredObject(o, dbColumnFamilyDefinition.getValueType(),
fieldsFilterSplitMap)) {
+ // the record doesn't pass the filter
+ continue;
+ }
if (valueFields != null) {
Map<String, Object> filteredValue = new HashMap<>();
- filteredValue.putAll(getFilteredObject(o,
dbColumnFamilyDefinition.getValueType(), fieldsSplitMap));
+ filteredValue.putAll(getFieldsFilteredObject(o,
dbColumnFamilyDefinition.getValueType(), fieldsSplitMap));
sb.append(WRITER.writeValueAsString(filteredValue));
} else {
sb.append(WRITER.writeValueAsString(o));
@@ -570,7 +632,92 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
return null;
}
- Map<String, Object> getFilteredObject(Object obj, Class<?> clazz,
Map<String, Object> fieldsSplitMap) {
+ boolean checkFilteredObject(Object obj, Class<?> clazz, Map<String,
Filter> fieldsSplitMap)
+ throws IOException {
+ for (Map.Entry<String, Filter> field : fieldsSplitMap.entrySet()) {
+ try {
+ Field valueClassField = getRequiredFieldFromAllFields(clazz,
field.getKey());
+ Object valueObject = valueClassField.get(obj);
+ Filter fieldValue = field.getValue();
+
+ if (valueObject == null) {
+ // there is no such field in the record. This filter will be
ignored for the current record.
+ continue;
+ }
+ if (fieldValue == null) {
+ err().println("Malformed filter. Check input");
+ throw new IOException("Invalid filter passed");
+ } else if (fieldValue.getNextLevel() == null) {
+ // reached the end of fields hierarchy, check if they match the
filter
+ // Currently, only equals operation is supported
+ if (Filter.FilterOperator.EQUALS.equals(fieldValue.getOperator())
&&
+ !String.valueOf(valueObject).equals(fieldValue.getValue())) {
+ return false;
+ } else if
(!Filter.FilterOperator.EQUALS.equals(fieldValue.getOperator())) {
+ err().println("Only EQUALS operator is supported currently.");
+ throw new IOException("Invalid filter passed");
+ }
+ } else {
+ Map<String, Filter> subfields = fieldValue.getNextLevel();
+ if (Collection.class.isAssignableFrom(valueObject.getClass())) {
+ if (!checkFilteredObjectCollection((Collection) valueObject,
subfields)) {
+ return false;
+ }
+ } else if (Map.class.isAssignableFrom(valueObject.getClass())) {
+ Map<?, ?> valueObjectMap = (Map<?, ?>) valueObject;
+ boolean flag = false;
+ for (Map.Entry<?, ?> ob : valueObjectMap.entrySet()) {
+ boolean subflag;
+ if
(Collection.class.isAssignableFrom(ob.getValue().getClass())) {
+ subflag =
checkFilteredObjectCollection((Collection)ob.getValue(), subfields);
+ } else {
+ subflag = checkFilteredObject(ob.getValue(),
ob.getValue().getClass(), subfields);
+ }
+ if (subflag) {
+ // atleast one item in the map/list of the record has
matched the filter,
+ // so record passes the filter.
+ flag = true;
+ break;
+ }
+ }
+ if (!flag) {
+ // none of the items in the map/list passed the filter =>
record doesn't pass the filter
+ return false;
+ }
+ } else {
+ if (!checkFilteredObject(valueObject, valueClassField.getType(),
subfields)) {
+ return false;
+ }
+ }
+ }
+ } catch (NoSuchFieldException ex) {
+ err().println("ERROR: no such field: " + field);
+ exception = true;
+ return false;
+ } catch (IllegalAccessException e) {
+ err().println("ERROR: Cannot get field from object: " + field);
+ exception = true;
+ return false;
+ } catch (Exception ex) {
+ err().println("ERROR: field: " + field + ", ex: " + ex);
+ exception = true;
+ return false;
+ }
+ }
+ return true;
+ }
+
+ boolean checkFilteredObjectCollection(Collection<?> valueObject,
Map<String, Filter> fields)
+ throws NoSuchFieldException, IllegalAccessException, IOException {
+ for (Object ob : valueObject) {
+ if (checkFilteredObject(ob, ob.getClass(), fields)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ Map<String, Object> getFieldsFilteredObject(Object obj, Class<?> clazz,
Map<String, Object> fieldsSplitMap) {
Map<String, Object> valueMap = new HashMap<>();
for (Map.Entry<String, Object> field : fieldsSplitMap.entrySet()) {
try {
@@ -583,7 +730,7 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
} else {
if (Collection.class.isAssignableFrom(valueObject.getClass())) {
List<Object> subfieldObjectsList =
- getFilteredObjectCollection((Collection) valueObject,
subfields);
+ getFieldsFilteredObjectCollection((Collection) valueObject,
subfields);
valueMap.put(field.getKey(), subfieldObjectsList);
} else if (Map.class.isAssignableFrom(valueObject.getClass())) {
Map<Object, Object> subfieldObjectsMap = new HashMap<>();
@@ -591,16 +738,16 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
for (Map.Entry<?, ?> ob : valueObjectMap.entrySet()) {
Object subfieldValue;
if
(Collection.class.isAssignableFrom(ob.getValue().getClass())) {
- subfieldValue =
getFilteredObjectCollection((Collection)ob.getValue(), subfields);
+ subfieldValue =
getFieldsFilteredObjectCollection((Collection)ob.getValue(), subfields);
} else {
- subfieldValue = getFilteredObject(ob.getValue(),
ob.getValue().getClass(), subfields);
+ subfieldValue = getFieldsFilteredObject(ob.getValue(),
ob.getValue().getClass(), subfields);
}
subfieldObjectsMap.put(ob.getKey(), subfieldValue);
}
valueMap.put(field.getKey(), subfieldObjectsMap);
} else {
valueMap.put(field.getKey(),
- getFilteredObject(valueObject, valueClassField.getType(),
subfields));
+ getFieldsFilteredObject(valueObject,
valueClassField.getType(), subfields));
}
}
} catch (NoSuchFieldException ex) {
@@ -612,11 +759,11 @@ public class DBScanner implements Callable<Void>,
SubcommandWithParent {
return valueMap;
}
- List<Object> getFilteredObjectCollection(Collection<?> valueObject,
Map<String, Object> fields)
+ List<Object> getFieldsFilteredObjectCollection(Collection<?> valueObject,
Map<String, Object> fields)
throws NoSuchFieldException, IllegalAccessException {
List<Object> subfieldObjectsList = new ArrayList<>();
for (Object ob : valueObject) {
- Object subfieldValue = getFilteredObject(ob, ob.getClass(), fields);
+ Object subfieldValue = getFieldsFilteredObject(ob, ob.getClass(),
fields);
subfieldObjectsList.add(subfieldValue);
}
return subfieldObjectsList;
diff --git
a/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/utils/Filter.java
b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/utils/Filter.java
new file mode 100644
index 0000000000..129e1a6158
--- /dev/null
+++ b/hadoop-ozone/tools/src/main/java/org/apache/hadoop/ozone/utils/Filter.java
@@ -0,0 +1,107 @@
+/**
+ * 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.hadoop.ozone.utils;
+
+import java.util.Map;
+
+/**
+ * Represent class which has info of what operation and value a set of records
should be filtered with.
+ */
+public class Filter {
+ private FilterOperator operator;
+ private Object value;
+ private Map<String, Filter> nextLevel = null;
+
+ public Filter() {
+ this.operator = null;
+ this.value = null;
+ }
+
+ public Filter(FilterOperator operator, Object value) {
+ this.operator = operator;
+ this.value = value;
+ }
+
+ public Filter(String op, Object value) {
+ this.operator = getFilterOperator(op);
+ this.value = value;
+ }
+
+ public Filter(FilterOperator operator, Object value, Map<String, Filter>
next) {
+ this.operator = operator;
+ this.value = value;
+ this.nextLevel = next;
+ }
+
+ public Filter(String op, Object value, Map<String, Filter> next) {
+ this.operator = getFilterOperator(op);
+ this.value = value;
+ this.nextLevel = next;
+ }
+
+ public FilterOperator getOperator() {
+ return operator;
+ }
+
+ public void setOperator(FilterOperator operator) {
+ this.operator = operator;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public void setValue(Object value) {
+ this.value = value;
+ }
+
+ public Map<String, Filter> getNextLevel() {
+ return nextLevel;
+ }
+
+ public void setNextLevel(Map<String, Filter> nextLevel) {
+ this.nextLevel = nextLevel;
+ }
+
+ public FilterOperator getFilterOperator(String op) {
+ if (op.equalsIgnoreCase("equals")) {
+ return FilterOperator.EQUALS;
+ } else if (op.equalsIgnoreCase("max")) {
+ return FilterOperator.MAX;
+ } else if (op.equalsIgnoreCase("min")) {
+ return FilterOperator.MIN;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "(" + operator + "," + value + "," + nextLevel + ")";
+ }
+
+ /**
+ * Operation of the filter.
+ */
+ public enum FilterOperator {
+ EQUALS,
+ MAX,
+ MIN;
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]