LUCENE-2039: Added extensible query parser which enables arbitrary parser extensions based on field naming scheme

git-svn-id: https://svn.apache.org/repos/asf/lucene/java/trunk@887533 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Simon Willnauer 2009-12-05 12:29:59 +00:00
parent ab447b6af0
commit 5556599fad
9 changed files with 750 additions and 0 deletions

View File

@ -16,6 +16,11 @@ API Changes
reader. (Eirik Bjørsnøs via Mike McCandless) reader. (Eirik Bjørsnøs via Mike McCandless)
New features New features
* LUCENE-2039: Add a extensible query parser to contrib/misc.
ExtendableQueryParser enables arbitrary parser extensions based on a
customizable field naming scheme.
(Simon Willnauer)
* LUCENE-2108: Spellchecker now safely supports concurrent modifications to * LUCENE-2108: Spellchecker now safely supports concurrent modifications to
the spell-index. Threads can safely obtain term suggestions while the spell- the spell-index. Threads can safely obtain term suggestions while the spell-

View File

@ -0,0 +1,142 @@
package org.apache.lucene.queryParser.ext;
/**
* 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.
*/
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.ext.Extensions.Pair;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.Version;
/**
* The {@link ExtendableQueryParser} enables arbitrary query parser extension
* based on a customizable field naming scheme. The lucene query syntax allows
* implicit and explicit field definitions as query prefix followed by a colon
* (':') character. The {@link ExtendableQueryParser} allows to encode extension
* keys into the field symbol associated with a registered instance of
* {@link ParserExtension}. A customizable separation character separates the
* extension key from the actual field symbol. The {@link ExtendableQueryParser}
* splits (@see {@link Extensions#splitExtensionField(String, String)}) the
* extension key from the field symbol and tries to resolve the associated
* {@link ParserExtension}. If the parser can't resolve the key or the field
* token does not contain a separation character, {@link ExtendableQueryParser}
* yields the same behavior as its super class {@link QueryParser}. Otherwise,
* if the key is associated with a {@link ParserExtension} instance, the parser
* builds an instance of {@link ExtensionQuery} to be processed by
* {@link ParserExtension#parse(ExtensionQuery)}.If a extension field does not
* contain a field part the default field for the query will be used.
* <p>
* To guarantee that an extension field is processed with its associated
* extension, the extension query part must escape any special characters like
* '*' or '['. If the extension query contains any whitespace characters, the
* extension query part must be enclosed in quotes.
* Example ('_' used as separation character):
* <pre>
* title_customExt:"Apache Lucene\?" OR content_customExt:prefix\*
* </pre>
*
* Search on the default field:
* <pre>
* _customExt:"Apache Lucene\?" OR _customExt:prefix\*
* </pre>
* </p>
* <p>
* The {@link ExtendableQueryParser} itself does not implement the logic how
* field and extension key are separated or ordered. All logic regarding the
* extension key and field symbol parsing is located in {@link Extensions}.
* Customized extension schemes should be implemented by sub-classing
* {@link Extensions}.
* </p>
* <p>
* For details about the default encoding scheme see {@link Extensions}.
* </p>
*
* @see Extensions
* @see ParserExtension
* @see ExtensionQuery
*/
public class ExtendableQueryParser extends QueryParser {
private final String defaultField;
private final Extensions extensions;
/**
* Default empty extensions instance
*/
private static final Extensions DEFAULT_EXTENSION = new Extensions();
/**
* Creates a new {@link ExtendableQueryParser} instance
*
* @param matchVersion
* the lucene version to use.
* @param f
* the default query field
* @param a
* the analyzer used to find terms in a query string
*/
public ExtendableQueryParser(final Version matchVersion, final String f,
final Analyzer a) {
this(matchVersion, f, a, DEFAULT_EXTENSION);
}
/**
* Creates a new {@link ExtendableQueryParser} instance
*
* @param matchVersion
* the lucene version to use.
* @param f
* the default query field
* @param a
* the analyzer used to find terms in a query string
* @param ext
* the query parser extensions
*/
public ExtendableQueryParser(final Version matchVersion, final String f,
final Analyzer a, final Extensions ext) {
super(matchVersion, f, a);
this.defaultField = f;
this.extensions = ext;
}
/**
* Returns the extension field delimiter character.
*
* @return the extension field delimiter character.
*/
public char getExtensionFieldDelimiter() {
return extensions.getExtensionFieldDelimiter();
}
@Override
protected Query getFieldQuery(final String field, final String queryText)
throws ParseException {
final Pair<String,String> splitExtensionField = this.extensions
.splitExtensionField(defaultField, field);
final ParserExtension extension = this.extensions
.getExtension(splitExtensionField.cud);
if (extension != null) {
return extension.parse(new ExtensionQuery(splitExtensionField.cur,
queryText));
}
return super.getFieldQuery(field, queryText);
}
}

View File

@ -0,0 +1,63 @@
package org.apache.lucene.queryParser.ext;
/**
* 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.
*/
/**
* {@link ExtensionQuery} holds all query components extracted from the original
* query string like the query field and the extension query string.
*
* @see Extensions
* @see ExtendableQueryParser
* @see ParserExtension
*/
public class ExtensionQuery {
private final String field;
private final String rawQueryString;
/**
* Creates a new {@link ExtensionQuery}
*
* @param field
* the query field
* @param rawQueryString
* the raw extension query string
*/
public ExtensionQuery(String field, String rawQueryString) {
this.field = field;
this.rawQueryString = rawQueryString;
}
/**
* Returns the query field
*
* @return the query field
*/
public String getField() {
return field;
}
/**
* Returns the raw extension query string
*
* @return the raw extension query string
*/
public String getRawQueryString() {
return rawQueryString;
}
}

View File

@ -0,0 +1,217 @@
package org.apache.lucene.queryParser.ext;
/**
* 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.
*/
import java.util.HashMap;
import java.util.Map;
import org.apache.lucene.queryParser.QueryParser;
/**
* The {@link Extensions} class represents an extension mapping to associate
* {@link ParserExtension} instances with extension keys. An extension key is a
* string encoded into a Lucene standard query parser field symbol recognized by
* {@link ExtendableQueryParser}. The query parser passes each extension field
* token to {@link #splitExtensionField(String, String)} to separate the
* extension key from the field identifier.
* <p>
* In addition to the key to extension mapping this class also defines the field
* name overloading scheme. {@link ExtendableQueryParser} uses the given
* extension to split the actual field name and extension key by calling
* {@link #splitExtensionField(String, String)}. To change the order or the key
* / field name encoding scheme users can subclass {@link Extensions} to
* implement their own.
*
* @see ExtendableQueryParser
* @see ParserExtension
*/
public class Extensions {
private final Map<String,ParserExtension> extensions = new HashMap<String,ParserExtension>();
private final char extensionFieldDelimiter;
/**
* The default extension field delimiter character. This constant is set to
* ':'
*/
public static final char DEFAULT_EXTENSION_FIELD_DELIMITER = ':';
/**
* Creates a new {@link Extensions} instance with the
* {@link #DEFAULT_EXTENSION_FIELD_DELIMITER} as a delimiter character.
*/
public Extensions() {
this(DEFAULT_EXTENSION_FIELD_DELIMITER);
}
/**
* Creates a new {@link Extensions} instance
*
* @param extensionFieldDelimiter
* the extensions field delimiter character
*/
public Extensions(char extensionFieldDelimiter) {
this.extensionFieldDelimiter = extensionFieldDelimiter;
}
/**
* Adds a new {@link ParserExtension} instance associated with the given key.
*
* @param key
* the parser extension key
* @param extension
* the parser extension
*/
public void add(String key, ParserExtension extension) {
this.extensions.put(key, extension);
}
/**
* Returns the {@link ParserExtension} instance for the given key or
* <code>null</code> if no extension can be found for the key.
*
* @param key
* the extension key
* @return the {@link ParserExtension} instance for the given key or
* <code>null</code> if no extension can be found for the key.
*/
public final ParserExtension getExtension(String key) {
return this.extensions.get(key);
}
/**
* Returns the extension field delimiter
*
* @return the extension field delimiter
*/
public char getExtensionFieldDelimiter() {
return extensionFieldDelimiter;
}
/**
* Splits a extension field and returns the field / extension part as a
* {@link Pair}. This method tries to split on the first occurrence of the
* extension field delimiter, if the delimiter is not present in the string
* the result will contain a <code>null</code> value for the extension key and
* the given field string as the field value. If the given extension field
* string contains no field identifier the result pair will carry the given
* default field as the field value.
*
* @param defaultField
* the default query field
* @param field
* the extension field string
* @return a {@link Pair} with the field name as the {@link Pair#cur} and the
* extension key as the {@link Pair#cud}
*/
public Pair<String,String> splitExtensionField(String defaultField,
String field) {
int indexOf = field.indexOf(this.extensionFieldDelimiter);
if (indexOf < 0)
return new Pair<String,String>(field, null);
final String indexField = indexOf == 0 ? defaultField : field.substring(0,
indexOf);
final String extensionKey = field.substring(indexOf + 1);
return new Pair<String,String>(indexField, extensionKey);
}
/**
* Escapes an extension field. The default implementation is equivalent to
* {@link QueryParser#escape(String)}.
*
* @param extfield
* the extension field identifier
* @return the extension field identifier with all special chars escaped with
* a backslash character.
*/
public String escapeExtensionField(String extfield) {
return QueryParser.escape(extfield);
}
/**
* Builds an extension field string from a given extension key and the default
* query field. The default field and the key are delimited with the extension
* field delimiter character. This method makes no assumption about the order
* of the extension key and the field. By default the extension key is
* appended to the end of the returned string while the field is added to the
* beginning. Special Query characters are escaped in the result.
* <p>
* Note: {@link Extensions} subclasses must maintain the contract between
* {@link #buildExtensionField(String)} and
* {@link #splitExtensionField(String, String)} where the latter inverts the
* former.
* </p>
*/
public String buildExtensionField(String extensionKey) {
return buildExtensionField(extensionKey, "");
}
/**
* Builds an extension field string from a given extension key and the
* extensions field. The field and the key are delimited with the extension
* field delimiter character. This method makes no assumption about the order
* of the extension key and the field. By default the extension key is
* appended to the end of the returned string while the field is added to the
* beginning. Special Query characters are escaped in the result.
* <p>
* Note: {@link Extensions} subclasses must maintain the contract between
* {@link #buildExtensionField(String, String)} and
* {@link #splitExtensionField(String, String)} where the latter inverts the
* former.
* </p>
*
* @param extensionKey
* the extension key
* @param field
* the field to apply the extension on.
* @return escaped extension field identifier
* @see #buildExtensionField(String) to use the default query field
*/
public String buildExtensionField(String extensionKey, String field) {
StringBuilder builder = new StringBuilder(field);
builder.append(this.extensionFieldDelimiter);
builder.append(extensionKey);
return escapeExtensionField(builder.toString());
}
/**
* This class represents a generic pair.
*
* @param <Cur>
* the pairs first element
* @param <Cud>
* the pairs last element of the pair.
*/
public static class Pair<Cur,Cud> {
public final Cur cur;
public final Cud cud;
/**
* Creates a new Pair
*
* @param cur
* the pairs first element
* @param cud
* the pairs last element
*/
public Pair(Cur cur, Cud cud) {
this.cur = cur;
this.cud = cud;
}
}
}

View File

@ -0,0 +1,53 @@
package org.apache.lucene.queryParser.ext;
/**
* 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.
*/
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.search.Query;
/**
* This class represents an extension base class to the Lucene standard
* {@link QueryParser}. The {@link QueryParser} is generated by the JavaCC
* parser generator. Changing or adding functionality or syntax in the standard
* query parser requires changes to the JavaCC source file. To enable extending
* the standard query parser without changing the JavaCC sources and re-generate
* the parser the {@link ParserExtension} can be customized and plugged into an
* instance of {@link ExtendableQueryParser}, a direct subclass of
* {@link QueryParser}.
*
* @see Extensions
* @see ExtendableQueryParser
*/
public abstract class ParserExtension {
/**
* Processes the given {@link ExtensionQuery} and returns a corresponding
* {@link Query} instance. Subclasses must either return a {@link Query}
* instance or raise a {@link ParseException}. This method must not return
* <code>null</code>.
*
* @param query
* the extension query
* @return a new query instance
* @throws ParseException
* if the query can not be parsed.
*/
public abstract Query parse(final ExtensionQuery query) throws ParseException;
}

View File

@ -0,0 +1,22 @@
<!doctype html public "-//w3c//dtd html 4.0 transitional//en">
<!--
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.
-->
<html><head></head>
<body>
Extendable QueryParser provides a simple and flexible extension mechanism by overloading query field names.
</body>
</html>

View File

@ -0,0 +1,33 @@
package org.apache.lucene.queryParser.ext;
import org.apache.lucene.index.Term;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
/**
* 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.
*/
class ExtensionStub extends ParserExtension {
@Override
public Query parse(ExtensionQuery components) throws ParseException {
return new TermQuery(new Term(components.getField(), components
.getRawQueryString()));
}
}

View File

@ -0,0 +1,137 @@
package org.apache.lucene.queryParser.ext;
/**
* 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.
*/
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.SimpleAnalyzer;
import org.apache.lucene.queryParser.ParseException;
import org.apache.lucene.queryParser.QueryParser;
import org.apache.lucene.queryParser.TestQueryParser;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.util.Version;
/**
* Testcase for the class {@link ExtendableQueryParser}
*/
public class TestExtendableQueryParser extends TestQueryParser {
private static char[] DELIMITERS = new char[] {
Extensions.DEFAULT_EXTENSION_FIELD_DELIMITER, '-', '|' };
public TestExtendableQueryParser(String name) {
super(name);
}
@Override
public QueryParser getParser(Analyzer a) throws Exception {
return getParser(a, null);
}
public QueryParser getParser(Analyzer a, Extensions extensions)
throws Exception {
if (a == null)
a = new SimpleAnalyzer();
QueryParser qp = extensions == null ? new ExtendableQueryParser(
Version.LUCENE_CURRENT, "field", a) : new ExtendableQueryParser(
Version.LUCENE_CURRENT, "field", a, extensions);
qp.setDefaultOperator(QueryParser.OR_OPERATOR);
return qp;
}
public void testUnescapedExtDelimiter() throws Exception {
Extensions ext = newExtensions(':');
ext.add("testExt", new ExtensionStub());
ExtendableQueryParser parser = (ExtendableQueryParser) getParser(null, ext);
try {
parser.parse("aField:testExt:\"foo \\& bar\"");
fail("extension field delimiter is not escaped");
} catch (ParseException e) {
}
}
public void testExtFieldUnqoted() throws Exception {
for (int i = 0; i < DELIMITERS.length; i++) {
Extensions ext = newExtensions(DELIMITERS[i]);
ext.add("testExt", new ExtensionStub());
ExtendableQueryParser parser = (ExtendableQueryParser) getParser(null,
ext);
String field = ext.buildExtensionField("testExt", "aField");
Query query = parser.parse(String.format("%s:foo bar", field));
assertTrue("expected instance of BooleanQuery but was "
+ query.getClass(), query instanceof BooleanQuery);
BooleanQuery bquery = (BooleanQuery) query;
BooleanClause[] clauses = bquery.getClauses();
assertEquals(2, clauses.length);
BooleanClause booleanClause = clauses[0];
query = booleanClause.getQuery();
assertTrue("expected instance of TermQuery but was " + query.getClass(),
query instanceof TermQuery);
TermQuery tquery = (TermQuery) query;
assertEquals("aField", tquery.getTerm()
.field());
assertEquals("foo", tquery.getTerm().text());
booleanClause = clauses[1];
query = booleanClause.getQuery();
assertTrue("expected instance of TermQuery but was " + query.getClass(),
query instanceof TermQuery);
tquery = (TermQuery) query;
assertEquals("field", tquery.getTerm().field());
assertEquals("bar", tquery.getTerm().text());
}
}
public void testExtDefaultField() throws Exception {
for (int i = 0; i < DELIMITERS.length; i++) {
Extensions ext = newExtensions(DELIMITERS[i]);
ext.add("testExt", new ExtensionStub());
ExtendableQueryParser parser = (ExtendableQueryParser) getParser(null,
ext);
String field = ext.buildExtensionField("testExt");
Query parse = parser.parse(String.format("%s:\"foo \\& bar\"", field));
assertTrue("expected instance of TermQuery but was " + parse.getClass(),
parse instanceof TermQuery);
TermQuery tquery = (TermQuery) parse;
assertEquals("field", tquery.getTerm().field());
assertEquals("foo & bar", tquery.getTerm().text());
}
}
public Extensions newExtensions(char delimiter) {
return new Extensions(delimiter);
}
public void testExtField() throws Exception {
for (int i = 0; i < DELIMITERS.length; i++) {
Extensions ext = newExtensions(DELIMITERS[i]);
ext.add("testExt", new ExtensionStub());
ExtendableQueryParser parser = (ExtendableQueryParser) getParser(null,
ext);
String field = ext.buildExtensionField("testExt", "afield");
Query parse = parser.parse(String.format("%s:\"foo \\& bar\"", field));
assertTrue("expected instance of TermQuery but was " + parse.getClass(),
parse instanceof TermQuery);
TermQuery tquery = (TermQuery) parse;
assertEquals("afield", tquery.getTerm().field());
assertEquals("foo & bar", tquery.getTerm().text());
}
}
}

View File

@ -0,0 +1,78 @@
package org.apache.lucene.queryParser.ext;
/**
* 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.
*/
import org.apache.lucene.util.LuceneTestCase;
/**
* Testcase for the {@link Extensions} class
*/
public class TestExtensions extends LuceneTestCase {
private Extensions ext;
protected void setUp() throws Exception {
super.setUp();
this.ext = new Extensions();
}
public void testBuildExtensionField() {
assertEquals("field\\:key", ext.buildExtensionField("key", "field"));
assertEquals("\\:key", ext.buildExtensionField("key"));
ext = new Extensions('.');
assertEquals("field.key", ext.buildExtensionField("key", "field"));
assertEquals(".key", ext.buildExtensionField("key"));
}
public void testSplitExtensionField() {
assertEquals("field\\:key", ext.buildExtensionField("key", "field"));
assertEquals("\\:key", ext.buildExtensionField("key"));
ext = new Extensions('.');
assertEquals("field.key", ext.buildExtensionField("key", "field"));
assertEquals(".key", ext.buildExtensionField("key"));
}
public void testAddGetExtension() {
ParserExtension extension = new ExtensionStub();
assertNull(ext.getExtension("foo"));
ext.add("foo", extension);
assertSame(extension, ext.getExtension("foo"));
ext.add("foo", null);
assertNull(ext.getExtension("foo"));
}
public void testGetExtDelimiter() {
assertEquals(Extensions.DEFAULT_EXTENSION_FIELD_DELIMITER, this.ext
.getExtensionFieldDelimiter());
ext = new Extensions('?');
assertEquals('?', this.ext.getExtensionFieldDelimiter());
}
public void testEscapeExtension() {
assertEquals("abc\\:\\?\\{\\}\\[\\]\\\\\\(\\)\\+\\-\\!\\~", ext
.escapeExtensionField("abc:?{}[]\\()+-!~"));
try {
ext.escapeExtensionField(null);
fail("should throw NPE - escape string is null");
} catch (NullPointerException e) {
//
}
}
}