This is an automated email from the ASF dual-hosted git repository.
jsweeney pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 3a69654b9eb SOLR-17022: Support for glob patterns for fields in Export
handler, Stream handler and with SelectStream streaming expression (#1996)
3a69654b9eb is described below
commit 3a69654b9eb2382c550d90b62c253588cb95f029
Author: Justin Sweeney <[email protected]>
AuthorDate: Fri Dec 22 05:29:38 2023 -0700
SOLR-17022: Support for glob patterns for fields in Export handler, Stream
handler and with SelectStream streaming expression (#1996)
* Adding support for Glob patterns in Export handler and Select stream
handler using the same logic to match glob patterns to fields as is used in
select requests
---
.../apache/solr/handler/export/ExportWriter.java | 77 ++++++++++++----------
.../org/apache/solr/search/SolrReturnFields.java | 5 +-
.../solr/handler/export/TestExportWriter.java | 37 +++++++++++
.../query-guide/pages/exporting-result-sets.adoc | 5 +-
.../pages/stream-decorator-reference.adoc | 2 +-
.../solr/client/solrj/io/stream/SelectStream.java | 30 ++++++++-
.../io/stream/StreamExpressionToExpessionTest.java | 3 +-
.../apache/solr/common/util/GlobPatternUtil.java | 37 +++++++++++
.../solr/common/util/TestGlobPatternUtil.java | 33 ++++++++++
9 files changed, 187 insertions(+), 42 deletions(-)
diff --git
a/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java
b/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java
index 51ba5551b69..e5e40e6d07e 100644
--- a/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java
+++ b/solr/core/src/java/org/apache/solr/handler/export/ExportWriter.java
@@ -27,6 +27,7 @@ import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
@@ -76,6 +77,7 @@ import org.apache.solr.schema.SortableTextField;
import org.apache.solr.schema.StrField;
import org.apache.solr.search.DocValuesIteratorCache;
import org.apache.solr.search.SolrIndexSearcher;
+import org.apache.solr.search.SolrReturnFields;
import org.apache.solr.search.SortSpec;
import org.apache.solr.search.SyntaxError;
import org.slf4j.Logger;
@@ -121,7 +123,7 @@ public class ExportWriter implements SolrCore.RawWriter,
Closeable {
private int priorityQueueSize;
StreamExpression streamExpression;
StreamContext streamContext;
- FieldWriter[] fieldWriters;
+ List<FieldWriter> fieldWriters;
int totalHits = 0;
FixedBitSet[] sets = null;
PushWriter writer;
@@ -293,7 +295,7 @@ public class ExportWriter implements SolrCore.RawWriter,
Closeable {
}
try {
- fieldWriters = getFieldWriters(fields, req.getSearcher());
+ fieldWriters = getFieldWriters(fields, req);
} catch (Exception e) {
writeException(e, writer, true);
return;
@@ -473,7 +475,7 @@ public class ExportWriter implements SolrCore.RawWriter,
Closeable {
}
void writeDoc(
- SortDoc sortDoc, List<LeafReaderContext> leaves, EntryWriter ew,
FieldWriter[] writers)
+ SortDoc sortDoc, List<LeafReaderContext> leaves, EntryWriter ew,
List<FieldWriter> writers)
throws IOException {
int ord = sortDoc.ord;
LeafReaderContext context = leaves.get(ord);
@@ -485,82 +487,89 @@ public class ExportWriter implements SolrCore.RawWriter,
Closeable {
}
}
- public FieldWriter[] getFieldWriters(String[] fields, SolrIndexSearcher
searcher)
+ public List<FieldWriter> getFieldWriters(String[] fields, SolrQueryRequest
req)
throws IOException {
- IndexSchema schema = searcher.getSchema();
- FieldWriter[] writers = new FieldWriter[fields.length];
- DocValuesIteratorCache dvIterCache = new DocValuesIteratorCache(searcher,
false);
- for (int i = 0; i < fields.length; i++) {
- String field = fields[i];
- SchemaField schemaField = null;
+ DocValuesIteratorCache dvIterCache = new
DocValuesIteratorCache(req.getSearcher(), false);
- try {
- schemaField = schema.getField(field);
- } catch (Exception e) {
- throw new IOException(e);
- }
+ SolrReturnFields solrReturnFields = new SolrReturnFields(fields, req);
+ List<FieldWriter> writers = new ArrayList<>();
+ for (String field : req.getSearcher().getFieldNames()) {
+ if (!solrReturnFields.wantsField(field)) {
+ continue;
+ }
+ SchemaField schemaField = req.getSchema().getField(field);
if (!schemaField.hasDocValues()) {
throw new IOException(schemaField + " must have DocValues to use this
feature.");
}
boolean multiValued = schemaField.multiValued();
FieldType fieldType = schemaField.getType();
-
- if (fieldType instanceof SortableTextField &&
schemaField.useDocValuesAsStored() == false) {
- throw new IOException(
- schemaField + " Must have useDocValuesAsStored='true' to be used
with export writer");
+ FieldWriter writer;
+
+ if (fieldType instanceof SortableTextField &&
!schemaField.useDocValuesAsStored()) {
+ if (solrReturnFields.getRequestedFieldNames() != null
+ && solrReturnFields.getRequestedFieldNames().contains(field)) {
+ // Explicitly requested field cannot be used due to not having
useDocValuesAsStored=true,
+ // throw exception
+ throw new IOException(
+ schemaField + " Must have useDocValuesAsStored='true' to be used
with export writer");
+ } else {
+ // Glob pattern matched field cannot be used due to not having
useDocValuesAsStored=true
+ continue;
+ }
}
DocValuesIteratorCache.FieldDocValuesSupplier docValuesCache =
dvIterCache.getSupplier(field);
if (docValuesCache == null) {
- writers[i] = EMPTY_FIELD_WRITER;
+ writer = EMPTY_FIELD_WRITER;
} else if (fieldType instanceof IntValueFieldType) {
if (multiValued) {
- writers[i] = new MultiFieldWriter(field, fieldType, schemaField,
true, docValuesCache);
+ writer = new MultiFieldWriter(field, fieldType, schemaField, true,
docValuesCache);
} else {
- writers[i] = new IntFieldWriter(field, docValuesCache);
+ writer = new IntFieldWriter(field, docValuesCache);
}
} else if (fieldType instanceof LongValueFieldType) {
if (multiValued) {
- writers[i] = new MultiFieldWriter(field, fieldType, schemaField,
true, docValuesCache);
+ writer = new MultiFieldWriter(field, fieldType, schemaField, true,
docValuesCache);
} else {
- writers[i] = new LongFieldWriter(field, docValuesCache);
+ writer = new LongFieldWriter(field, docValuesCache);
}
} else if (fieldType instanceof FloatValueFieldType) {
if (multiValued) {
- writers[i] = new MultiFieldWriter(field, fieldType, schemaField,
true, docValuesCache);
+ writer = new MultiFieldWriter(field, fieldType, schemaField, true,
docValuesCache);
} else {
- writers[i] = new FloatFieldWriter(field, docValuesCache);
+ writer = new FloatFieldWriter(field, docValuesCache);
}
} else if (fieldType instanceof DoubleValueFieldType) {
if (multiValued) {
- writers[i] = new MultiFieldWriter(field, fieldType, schemaField,
true, docValuesCache);
+ writer = new MultiFieldWriter(field, fieldType, schemaField, true,
docValuesCache);
} else {
- writers[i] = new DoubleFieldWriter(field, docValuesCache);
+ writer = new DoubleFieldWriter(field, docValuesCache);
}
} else if (fieldType instanceof StrField || fieldType instanceof
SortableTextField) {
if (multiValued) {
- writers[i] = new MultiFieldWriter(field, fieldType, schemaField,
false, docValuesCache);
+ writer = new MultiFieldWriter(field, fieldType, schemaField, false,
docValuesCache);
} else {
- writers[i] = new StringFieldWriter(field, fieldType, docValuesCache);
+ writer = new StringFieldWriter(field, fieldType, docValuesCache);
}
} else if (fieldType instanceof DateValueFieldType) {
if (multiValued) {
- writers[i] = new MultiFieldWriter(field, fieldType, schemaField,
false, docValuesCache);
+ writer = new MultiFieldWriter(field, fieldType, schemaField, false,
docValuesCache);
} else {
- writers[i] = new DateFieldWriter(field, docValuesCache);
+ writer = new DateFieldWriter(field, docValuesCache);
}
} else if (fieldType instanceof BoolField) {
if (multiValued) {
- writers[i] = new MultiFieldWriter(field, fieldType, schemaField,
true, docValuesCache);
+ writer = new MultiFieldWriter(field, fieldType, schemaField, true,
docValuesCache);
} else {
- writers[i] = new BoolFieldWriter(field, fieldType, docValuesCache);
+ writer = new BoolFieldWriter(field, fieldType, docValuesCache);
}
} else {
throw new IOException(
"Export fields must be one of the following types:
int,float,long,double,string,date,boolean,SortableText");
}
+ writers.add(writer);
}
return writers;
}
diff --git a/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
b/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
index 7d0583ce63a..af35245af15 100644
--- a/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
+++ b/solr/core/src/java/org/apache/solr/search/SolrReturnFields.java
@@ -28,7 +28,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
-import org.apache.commons.io.FilenameUtils;
import org.apache.lucene.queries.function.FunctionQuery;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.QueryValueSource;
@@ -37,6 +36,7 @@ import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.GlobPatternUtil;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.transform.DocTransformer;
import org.apache.solr.response.transform.DocTransformers;
@@ -577,8 +577,7 @@ public class SolrReturnFields extends ReturnFields {
return true;
}
for (String s : globs) {
- // TODO something better?
- if (FilenameUtils.wildcardMatch(name, s)) {
+ if (GlobPatternUtil.matches(s, name)) {
okFieldNames.add(name); // Don't calculate it again
return true;
}
diff --git
a/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java
b/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java
index 8337609faf9..e37f26efc94 100644
--- a/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java
+++ b/solr/core/src/test/org/apache/solr/handler/export/TestExportWriter.java
@@ -1298,6 +1298,43 @@ public class TestExportWriter extends SolrTestCaseJ4 {
.contains("Must have useDocValuesAsStored='true'"));
}
+ @Test
+ public void testGlobFields() throws Exception {
+ assertU(delQ("*:*"));
+ assertU(commit());
+ createLargeIndex();
+ SolrQueryRequest req =
+ req("q", "*:*", "qt", "/export", "fl", "id,*_udvas,*_i_p", "sort", "id
asc");
+ assertJQ(
+ req,
+ "response/numFound==100000",
+ "response/docs/[0]/id=='0'",
+ "response/docs/[1]/id=='1'",
+ "response/docs/[0]/sortabledv_udvas=='0'",
+ "response/docs/[1]/sortabledv_udvas=='1'",
+ "response/docs/[0]/small_i_p==0",
+ "response/docs/[1]/small_i_p==1");
+
+ assertU(delQ("*:*"));
+ assertU(commit());
+ createLargeIndex();
+ req = req("q", "*:*", "qt", "/export", "fl", "*", "sort", "id asc");
+ assertJQ(
+ req,
+ "response/numFound==100000",
+ "response/docs/[0]/id=='0'",
+ "response/docs/[1]/id=='1'",
+ "response/docs/[0]/sortabledv_udvas=='0'",
+ "response/docs/[1]/sortabledv_udvas=='1'",
+ "response/docs/[0]/small_i_p==0",
+ "response/docs/[1]/small_i_p==1");
+
+ String jq = JQ(req);
+ assertFalse(
+ "Fields without docvalues and useDocValuesAsStored should not be
returned",
+ jq.contains("\"sortabledv\""));
+ }
+
@SuppressWarnings("rawtypes")
private void validateSort(int numDocs) throws Exception {
// 10 fields
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/exporting-result-sets.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/exporting-result-sets.adoc
index 28c395daa46..bbd31c7b358 100644
--- a/solr/solr-ref-guide/modules/query-guide/pages/exporting-result-sets.adoc
+++ b/solr/solr-ref-guide/modules/query-guide/pages/exporting-result-sets.adoc
@@ -70,7 +70,10 @@ It can get worse otherwise.
The `fl` property defines the fields that will be exported with the result set.
Any of the field types that can be sorted (i.e., int, long, float, double,
string, date, boolean) can be used in the field list.
The fields can be single or multi-valued.
-However, returning scores and wildcards are not supported at this time.
+
+Wildcard patterns can be used for the field list (e.g. `fl=*_i`) and will be
expanded to the list of fields that match the pattern and are able to be
exported, see <<Field Requirements>>.
+
+Returning scores is not supported at this time.
=== Specifying the Local Streaming Expression
diff --git
a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
index d5e25ba98fa..28a570ffae4 100644
---
a/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
+++
b/solr/solr-ref-guide/modules/query-guide/pages/stream-decorator-reference.adoc
@@ -1376,7 +1376,7 @@ One can provide a list of operations and evaluators to
perform on any fields, su
=== select Parameters
* `StreamExpression`
-* `fieldName`: name of field to include in the output tuple (can include
multiple of these), such as `outputTuple[fieldName] = inputTuple[fieldName]`
+* `fieldName`: name of field to include in the output tuple (can include
multiple of these), such as `outputTuple[fieldName] = inputTuple[fieldName]`.
The `fieldName` can be a wildcard pattern, e.g. `a_*` to select all fields that
start with `a_`.
* `fieldName as aliasFieldName`: aliased field name to include in the output
tuple (can include multiple of these), such as `outputTuple[aliasFieldName] =
incomingTuple[fieldName]`
* `replace(fieldName, value, withValue=replacementValue)`: if
`incomingTuple[fieldName] == value` then `outgoingTuple[fieldName]` will be set
to `replacementValue`.
`value` can be the string "null" to replace a null value with some other value.
diff --git
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/SelectStream.java
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/SelectStream.java
index 80219e797bb..647a1c59d4c 100644
---
a/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/SelectStream.java
+++
b/solr/solrj-streaming/src/java/org/apache/solr/client/solrj/io/stream/SelectStream.java
@@ -38,6 +38,7 @@ import
org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParameter;
import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionParser;
import org.apache.solr.client.solrj.io.stream.expr.StreamExpressionValue;
import org.apache.solr.client.solrj.io.stream.expr.StreamFactory;
+import org.apache.solr.common.util.GlobPatternUtil;
/**
* Selects fields from the incoming stream and applies optional field
renaming. Does not reorder the
@@ -52,14 +53,21 @@ public class SelectStream extends TupleStream implements
Expressible {
private TupleStream stream;
private StreamContext streamContext;
private Map<String, String> selectedFields;
+ private List<String> selectedFieldGlobPatterns;
private Map<StreamEvaluator, String> selectedEvaluators;
private List<StreamOperation> operations;
public SelectStream(TupleStream stream, List<String> selectedFields) throws
IOException {
this.stream = stream;
this.selectedFields = new HashMap<>();
+ this.selectedFieldGlobPatterns = new ArrayList<>();
for (String selectedField : selectedFields) {
- this.selectedFields.put(selectedField, selectedField);
+ if (selectedField.contains("*")) {
+ // selected field is a glob pattern
+ this.selectedFieldGlobPatterns.add(selectedField);
+ } else {
+ this.selectedFields.put(selectedField, selectedField);
+ }
}
operations = new ArrayList<>();
selectedEvaluators = new LinkedHashMap<>();
@@ -68,6 +76,7 @@ public class SelectStream extends TupleStream implements
Expressible {
public SelectStream(TupleStream stream, Map<String, String> selectedFields)
throws IOException {
this.stream = stream;
this.selectedFields = selectedFields;
+ selectedFieldGlobPatterns = new ArrayList<>();
operations = new ArrayList<>();
selectedEvaluators = new LinkedHashMap<>();
}
@@ -123,6 +132,7 @@ public class SelectStream extends TupleStream implements
Expressible {
stream = factory.constructStream(streamExpressions.get(0));
selectedFields = new HashMap<>();
+ selectedFieldGlobPatterns = new ArrayList<>();
selectedEvaluators = new LinkedHashMap<>();
for (StreamExpressionParameter parameter : selectAsFieldsExpressions) {
StreamExpressionValue selectField = (StreamExpressionValue) parameter;
@@ -175,7 +185,11 @@ public class SelectStream extends TupleStream implements
Expressible {
selectedFields.put(asValue, asName);
}
} else {
- selectedFields.put(value, value);
+ if (value.contains("*")) {
+ selectedFieldGlobPatterns.add(value);
+ } else {
+ selectedFields.put(value, value);
+ }
}
}
@@ -217,6 +231,11 @@ public class SelectStream extends TupleStream implements
Expressible {
}
}
+ // selected glob patterns
+ for (String selectFieldGlobPattern : selectedFieldGlobPatterns) {
+ expression.addParameter(selectFieldGlobPattern);
+ }
+
// selected evaluators
for (Map.Entry<StreamEvaluator, String> selectedEvaluator :
selectedEvaluators.entrySet()) {
expression.addParameter(
@@ -308,6 +327,13 @@ public class SelectStream extends TupleStream implements
Expressible {
workingForEvaluators.put(fieldName, original.get(fieldName));
if (selectedFields.containsKey(fieldName)) {
workingToReturn.put(selectedFields.get(fieldName),
original.get(fieldName));
+ } else {
+ for (String globPattern : selectedFieldGlobPatterns) {
+ if (GlobPatternUtil.matches(globPattern, fieldName)) {
+ workingToReturn.put(fieldName, original.get(fieldName));
+ break;
+ }
+ }
}
}
diff --git
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionToExpessionTest.java
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionToExpessionTest.java
index 4069b671b32..2c941f142d7 100644
---
a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionToExpessionTest.java
+++
b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/StreamExpressionToExpessionTest.java
@@ -105,7 +105,7 @@ public class StreamExpressionToExpessionTest extends
SolrTestCase {
try (SelectStream stream =
new SelectStream(
StreamExpressionParser.parse(
- "select(\"a_s as fieldA\", search(collection1, q=*:*,
fl=\"id,a_s,a_i,a_f\", sort=\"a_f asc, a_i asc\"))"),
+ "select(\"a_s as fieldA\", a_*, search(collection1, q=*:*,
fl=\"id,a_s,a_i,a_f\", sort=\"a_f asc, a_i asc\"))"),
factory)) {
expressionString = stream.toExpression(factory).toString();
assertTrue(expressionString.contains("select(search(collection1,"));
@@ -113,6 +113,7 @@ public class StreamExpressionToExpessionTest extends
SolrTestCase {
assertTrue(expressionString.contains("fl=\"id,a_s,a_i,a_f\""));
assertTrue(expressionString.contains("sort=\"a_f asc, a_i asc\""));
assertTrue(expressionString.contains("a_s as fieldA"));
+ assertTrue(expressionString.contains("a_*"));
}
}
diff --git
a/solr/solrj/src/java/org/apache/solr/common/util/GlobPatternUtil.java
b/solr/solrj/src/java/org/apache/solr/common/util/GlobPatternUtil.java
new file mode 100644
index 00000000000..8b26ab5a355
--- /dev/null
+++ b/solr/solrj/src/java/org/apache/solr/common/util/GlobPatternUtil.java
@@ -0,0 +1,37 @@
+/*
+ * 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.solr.common.util;
+
+import java.nio.file.FileSystems;
+import java.nio.file.Paths;
+
+/** Provides methods for matching glob patterns against input strings. */
+public class GlobPatternUtil {
+
+ /**
+ * Matches an input string against a provided glob patterns. This uses Java
NIO FileSystems
+ * PathMatcher to match glob patterns in the same way to how glob patterns
are matches for file
+ * paths, rather than implementing our own glob pattern matching.
+ *
+ * @param pattern the glob pattern to match against
+ * @param input the input string to match against a glob pattern
+ * @return true if the input string matches the glob pattern, false otherwise
+ */
+ public static boolean matches(String pattern, String input) {
+ return FileSystems.getDefault().getPathMatcher("glob:" +
pattern).matches(Paths.get(input));
+ }
+}
diff --git
a/solr/solrj/src/test/org/apache/solr/common/util/TestGlobPatternUtil.java
b/solr/solrj/src/test/org/apache/solr/common/util/TestGlobPatternUtil.java
new file mode 100644
index 00000000000..a5bdcad92fa
--- /dev/null
+++ b/solr/solrj/src/test/org/apache/solr/common/util/TestGlobPatternUtil.java
@@ -0,0 +1,33 @@
+/*
+ * 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.solr.common.util;
+
+import org.apache.solr.SolrTestCase;
+
+public class TestGlobPatternUtil extends SolrTestCase {
+
+ public void testMatches() {
+ assertTrue(GlobPatternUtil.matches("*_str", "user_str"));
+ assertFalse(GlobPatternUtil.matches("*_str", "str_user"));
+ assertTrue(GlobPatternUtil.matches("str_*", "str_user"));
+ assertFalse(GlobPatternUtil.matches("str_*", "user_str"));
+ assertTrue(GlobPatternUtil.matches("str?", "str1"));
+ assertFalse(GlobPatternUtil.matches("str?", "str_user"));
+ assertTrue(GlobPatternUtil.matches("user_*_str", "user_type_str"));
+ assertFalse(GlobPatternUtil.matches("user_*_str", "user_str"));
+ }
+}