org.teiid.query.parser.QueryParser Maven / Gradle / Ivy
/*
* Copyright Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags and
* the COPYRIGHT.txt file distributed with this work.
*
* Licensed 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.teiid.query.parser;
import static org.teiid.query.parser.SQLParserConstants.*;
import static org.teiid.query.parser.TeiidSQLParserTokenManager.*;
import java.io.Reader;
import java.io.StringReader;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.teiid.api.exception.query.QueryParserException;
import org.teiid.language.SQLConstants;
import org.teiid.metadata.DataWrapper;
import org.teiid.metadata.Database;
import org.teiid.metadata.Datatype;
import org.teiid.metadata.MetadataException;
import org.teiid.metadata.MetadataFactory;
import org.teiid.metadata.Parser;
import org.teiid.metadata.Server;
import org.teiid.query.QueryPlugin;
import org.teiid.query.metadata.CompositeMetadataStore;
import org.teiid.query.metadata.DatabaseStore;
import org.teiid.query.metadata.DatabaseStore.Mode;
import org.teiid.query.metadata.DatabaseUtil;
import org.teiid.query.metadata.TransformationMetadata;
import org.teiid.query.sql.lang.CacheHint;
import org.teiid.query.sql.lang.Command;
import org.teiid.query.sql.lang.Criteria;
import org.teiid.query.sql.symbol.Expression;
import org.teiid.query.util.CommandContext;
/**
* Converts a SQL-string to an object version of a query. This
* QueryParser can be reused but is NOT thread-safe as the parser uses an
* input stream. Putting multiple queries into the same stream will result
* in unpredictable and most likely incorrect behavior.
*/
public class QueryParser implements Parser {
private static final class SingleSchemaDatabaseStore extends DatabaseStore {
private final MetadataFactory factory;
private TransformationMetadata transformationMetadata;
private SingleSchemaDatabaseStore(MetadataFactory factory) {
this.factory = factory;
}
@Override
public Map getRuntimeTypes() {
return factory.getDataTypes();
}
@Override
protected TransformationMetadata getTransformationMetadata() {
return transformationMetadata;
}
public void setTransformationMetadata(
TransformationMetadata transformationMetadata) {
this.transformationMetadata = transformationMetadata;
}
}
private static ThreadLocal QUERY_PARSER = new ThreadLocal() {
/**
* @see java.lang.ThreadLocal#initialValue()
*/
@Override
protected QueryParser initialValue() {
return new QueryParser();
}
};
private static final String XQUERY_DECLARE = "declare"; //$NON-NLS-1$
private static final String XML_OPEN_BRACKET = "<"; //$NON-NLS-1$
private static final String NONE = "none"; //$NON-NLS-1$
private SQLParser parser;
private TeiidSQLParserTokenManager tm;
/**
* Construct a QueryParser - this may be reused.
*/
public QueryParser() {}
public static QueryParser getQueryParser() {
return QUERY_PARSER.get();
}
/**
* Helper method to get a SQLParser instance for given sql string.
*/
private SQLParser getSqlParser(String sql) {
return getSqlParser(new StringReader(sql));
}
private SQLParser getSqlParser(Reader sql) {
if(parser == null) {
JavaCharStream jcs = new JavaCharStream(sql);
tm = new TeiidSQLParserTokenManager(new JavaCharStream(sql));
parser = new SQLParser(tm);
parser.jj_input_stream = jcs;
} else {
parser.ReInit(sql);
tm.reinit();
}
return parser;
}
/**
* Takes a SQL string representing a Command and returns the object
* representation.
* @param sql SQL string
* instead of string litral
* @return SQL object representation
* @throws QueryParserException if parsing fails
* @throws IllegalArgumentException if sql is null
*/
public Command parseCommand(String sql) throws QueryParserException {
return parseCommand(sql, new ParseInfo());
}
public Command parseProcedure(String sql, boolean update) throws QueryParserException {
try{
if (update) {
return getSqlParser(sql).forEachRowTriggerAction(new ParseInfo());
}
Command result = getSqlParser(sql).procedureBodyCommand(new ParseInfo());
result.setCacheHint(SQLParserUtil.getQueryCacheOption(sql));
return result;
} catch(ParseException pe) {
throw convertParserException(pe);
} finally {
tm.reinit();
}
}
/**
* Takes a SQL string representing a Command and returns the object
* representation.
* @param sql SQL string
* @param parseInfo - instructions to parse
* @return SQL object representation
* @throws QueryParserException if parsing fails
* @throws IllegalArgumentException if sql is null
*/
public Command parseCommand(String sql, ParseInfo parseInfo) throws QueryParserException {
return parseCommand(sql, parseInfo, false);
}
public Command parseDesignerCommand(String sql) throws QueryParserException {
return parseCommand(sql, new ParseInfo(), true);
}
public Command parseCommand(String sql, ParseInfo parseInfo, boolean designerCommands) throws QueryParserException {
return parseCommand(sql, parseInfo, designerCommands, null, null, null, null);
}
public Command parseCommand(final String sql, ParseInfo parseInfo, boolean designerCommands, String vdbName,
String vdbVersion, String schemaName, CommandContext commandContext) throws QueryParserException {
if(sql == null || sql.length() == 0) {
throw new QueryParserException(QueryPlugin.Event.TEIID30377, QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30377));
}
Command result = null;
try{
if (designerCommands) {
result = getSqlParser(sql).designerCommand(parseInfo);
} else {
result = getSqlParser(sql).command(parseInfo);
}
result.setCacheHint(SQLParserUtil.getQueryCacheOption(sql));
} catch(ParseException pe) {
if(sql.startsWith(XML_OPEN_BRACKET) || sql.startsWith(XQUERY_DECLARE)) {
throw new QueryParserException(QueryPlugin.Event.TEIID30378, convertParserException(pe), QueryPlugin.Util.gs(QueryPlugin.Event.TEIID30378, sql));
}
throw convertParserException(pe);
} finally {
tm.reinit();
}
return result;
}
public CacheHint parseCacheHint(String sql) {
if(sql == null || sql.length() == 0) {
return null;
}
return SQLParserUtil.getQueryCacheOption(sql);
}
/**
* Takes a SQL string representing an SQL criteria (i.e. just the WHERE
* clause) and returns the object representation.
* @param sql SQL criteria (WHERE clause) string
* @return Criteria SQL object representation
* @throws QueryParserException if parsing fails
* @throws IllegalArgumentException if sql is null
*/
public Criteria parseCriteria(String sql) throws QueryParserException {
if(sql == null) {
throw new IllegalArgumentException(QueryPlugin.Util.getString("QueryParser.nullSqlCrit")); //$NON-NLS-1$
}
ParseInfo dummyInfo = new ParseInfo();
Criteria result = null;
try{
result = getSqlParser(sql).criteria(dummyInfo);
} catch(ParseException pe) {
throw convertParserException(pe);
} finally {
tm.reinit();
}
return result;
}
private QueryParserException convertParserException(ParseException pe) {
if (pe.currentToken == null) {
List preceeding = findPreceeding(parser.token, 1);
if (!preceeding.isEmpty()) {
pe.currentToken = preceeding.get(0);
} else {
pe.currentToken = parser.token;
}
}
QueryParserException qpe = new QueryParserException(QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31100, getMessage(pe, 10)));
qpe.setParseException(pe);
return qpe;
}
/**
* The default JavaCC message is not very good. This method produces a much more readable result.
* @param pe
* @param maxExpansions
* @return
*/
public String getMessage(ParseException pe, int maxExpansions) {
if (pe.expectedTokenSequences == null) {
if (pe.currentToken == null) {
return pe.getMessage();
}
StringBuilder sb = encountered(pe, pe.currentToken.next!=null?1:0);
if (pe.currentToken.kind == INVALID_TOKEN) {
sb.append(QueryPlugin.Util.getString("QueryParser.lexicalError", pe.currentToken.image)); //$NON-NLS-1$
} else if (pe.currentToken.next != null && pe.currentToken.next.kind == -1) {
sb.append(QueryPlugin.Util.getString("QueryParser.lexicalError", pe.currentToken.next.image)); //$NON-NLS-1$
}
if (pe.getMessage() != null) {
sb.append(pe.getMessage());
}
return sb.toString();
}
Token currentToken = pe.currentToken;
//if the next token is invalid, we wan to use a lexical message, not the sequences
if (currentToken.next.kind == INVALID_TOKEN) {
StringBuilder retval = encountered(pe, 1);
retval.append(QueryPlugin.Util.getString("QueryParser.lexicalError", currentToken.next.image)); //$NON-NLS-1$
return retval.toString();
}
//find the longest match chain an all possible end tokens
int[][] expectedTokenSequences = pe.expectedTokenSequences;
int[] ex = null;
Set last = new TreeSet();
outer : for (int i = 0; i < expectedTokenSequences.length; i++) {
if (ex == null || expectedTokenSequences[i].length > ex.length) {
ex = expectedTokenSequences[i];
last.clear();
} else if (expectedTokenSequences[i].length < ex.length) {
continue;
} else {
for (int j = 0; j < ex.length -1; j++) {
if (ex[j] != expectedTokenSequences[i][j]) {
continue outer; //TODO : not sure how to handle this case
}
}
}
last.add(expectedTokenSequences[i][expectedTokenSequences[i].length-1]);
}
if (ex == null) {
return pe.getMessage(); //shouldn't happen
}
StringBuilder retval = encountered(pe, ex.length);
//output the expected tokens condensing the id/non-reserved
retval.append("Was expecting: "); //$NON-NLS-1$
boolean id = last.contains(SQLParserConstants.ID);
int count = 0;
for (Integer t : last) {
String img = tokenImage[t];
if (id && img.startsWith("\"") //$NON-NLS-1$
&& Character.isLetter(img.charAt(1))
&& (!SQLConstants.isReservedWord(img.substring(1, img.length()-1)) || img.equals("\"default\""))) { //$NON-NLS-1$
continue;
}
if (count > 0) {
retval.append(" | "); //$NON-NLS-1$
}
count++;
if (t == SQLParserConstants.ID) {
retval.append("id"); //$NON-NLS-1$
} else {
retval.append(img);
}
if (count == maxExpansions) {
retval.append(" ..."); //$NON-NLS-1$
break;
}
}
return retval.toString();
}
private StringBuilder encountered(ParseException pe, int offset) {
StringBuilder retval = new StringBuilder("Encountered \""); //$NON-NLS-1$
Token currentToken = pe.currentToken;
for (int i = 1; i < offset; i++) {
//TODO: for large offsets we don't have to call findPreceeding
currentToken = currentToken.next;
}
List preceeding = findPreceeding(currentToken, 2);
if (offset > 0 && !preceeding.isEmpty()) {
addTokenSequence(preceeding.size() + 1, retval, null, preceeding.get(0), false);
} else {
addTokenSequence(1, retval, null, currentToken, offset==0);
}
if (currentToken.next != null && offset>0) {
addTokenSequence(3, retval, currentToken, currentToken.next, true);
currentToken = currentToken.next; //move to the error token
}
retval.append("\" at line ").append(currentToken.beginLine).append(", column ").append(currentToken.beginColumn); //$NON-NLS-1$ //$NON-NLS-2$
retval.append(".\n"); //$NON-NLS-1$
return retval;
}
private List findPreceeding(Token currentToken, int count) {
LinkedList preceeding = new LinkedList();
Token tok = this.tm.head;
boolean found = false;
while (tok != null) {
if (tok == currentToken) {
found = true;
break;
}
preceeding.add(tok);
if (preceeding.size() > count) {
preceeding.removeFirst();
}
tok = tok.next;
}
if (!found) {
preceeding.clear();
}
return preceeding;
}
private Token addTokenSequence(int maxSize, StringBuilder retval,
Token last, Token tok, boolean highlight) {
for (int i = 0; i < maxSize && tok != null; i++) {
if (last != null && last.endColumn + 1 != tok.beginColumn && tok.kind != SQLParserConstants.EOF) {
retval.append(" "); //$NON-NLS-1$
}
last = tok;
if (i == 0 && highlight) {
retval.append("[*]"); //$NON-NLS-1$
}
if (tok.image != null && !tok.image.isEmpty()) {
add_escapes(tok.image, retval);
if (i == 0 && highlight) {
retval.append("[*]"); //$NON-NLS-1$
}
}
while (tok.next == null) {
if (this.parser.getNextToken() == null) {
break;
}
}
tok = tok.next;
}
return last;
}
/**
* Used to convert raw characters to their escaped version
* when these raw version cannot be used as part of an ASCII
* string literal. Also escapes double quotes.
*/
protected void add_escapes(String str, StringBuilder retval) {
for (int i = 0; i < str.length(); i++) {
char ch = str.charAt(i);
switch (ch)
{
case 0 :
continue;
case '\b':
retval.append("\\b"); //$NON-NLS-1$
continue;
case '\t':
retval.append("\\t"); //$NON-NLS-1$
continue;
case '\n':
retval.append("\\n"); //$NON-NLS-1$
continue;
case '\f':
retval.append("\\f"); //$NON-NLS-1$
continue;
case '\r':
retval.append("\\r"); //$NON-NLS-1$
continue;
case '\"':
retval.append("\\\""); //$NON-NLS-1$
continue;
case '\\':
retval.append("\\\\"); //$NON-NLS-1$
continue;
default:
if (ch < 0x20 || ch > 0x7e) {
String s = "0000" + Integer.toString(ch, 16); //$NON-NLS-1$
retval.append("\\u" + s.substring(s.length() - 4, s.length())); //$NON-NLS-1$
} else {
retval.append(ch);
}
continue;
}
}
}
/**
* Takes a SQL string representing an SQL expression
* and returns the object representation.
* @param sql SQL expression string
* @return SQL expression object
* @throws QueryParserException if parsing fails
* @throws IllegalArgumentException if sql is null
*/
public Expression parseExpression(String sql) throws QueryParserException {
if(sql == null) {
throw new IllegalArgumentException(QueryPlugin.Util.getString("QueryParser.nullSqlExpr")); //$NON-NLS-1$
}
ParseInfo dummyInfo = new ParseInfo();
Expression result = null;
try{
result = getSqlParser(sql).expression(dummyInfo);
} catch(ParseException pe) {
throw convertParserException(pe);
} finally {
tm.reinit();
}
return result;
}
public Expression parseSelectExpression(String sql) throws QueryParserException {
if(sql == null) {
throw new IllegalArgumentException(QueryPlugin.Util.getString("QueryParser.nullSqlExpr")); //$NON-NLS-1$
}
ParseInfo dummyInfo = new ParseInfo();
Expression result = null;
try{
result = getSqlParser(sql).selectExpression(dummyInfo);
} catch(ParseException pe) {
throw convertParserException(pe);
} finally {
tm.reinit();
}
return result;
}
public void parseDDL(MetadataFactory factory, String ddl) {
parseDDL(factory, new StringReader(ddl));
}
public void parseDDL(final MetadataFactory factory, Reader ddl) {
SingleSchemaDatabaseStore store = new SingleSchemaDatabaseStore(factory);
store.startEditing(true);
Database db = new Database(factory.getVdbName(), factory.getVdbVersion());
store.databaseCreated(db);
store.databaseSwitched(factory.getVdbName(), factory.getVdbVersion());
store.dataWrapperCreated(new DataWrapper(NONE));
Server server = new Server(NONE);
server.setDataWrapper(NONE);
store.serverCreated(server);
if (factory.getSchema().isPhysical()) {
Server s = new Server(factory.getSchema().getName());
s.setDataWrapper(NONE);
store.serverCreated(s);
}
List servers = Collections.emptyList();
store.schemaCreated(factory.getSchema(), servers);
//with the schema created, create the TransformationMetadata
CompositeMetadataStore cms = new CompositeMetadataStore(db.getMetadataStore());
TransformationMetadata qmi = new TransformationMetadata(DatabaseUtil.convert(db), cms, null, null, null);
store.setTransformationMetadata(qmi.getDesignTimeMetadata());
store.schemaSwitched(factory.getSchema().getName());
store.setMode(Mode.SCHEMA);
store.setStrict(true);
try {
parseDDL(store, ddl);
Map colNs = store.getNameSpaces();
for (String key:colNs.keySet()) {
factory.addNamespace(key, colNs.get(key));
}
} finally {
store.stopEditing();
}
}
public void parseDDL(DatabaseStore repository, Reader ddl)
throws MetadataException {
SQLParser sqlParser = getSqlParser(ddl);
try {
sqlParser.parseMetadata(repository);
} catch (org.teiid.metadata.ParseException e) {
throw e;
} catch (MetadataException e) {
Token t = sqlParser.token;
throw new org.teiid.metadata.ParseException(QueryPlugin.Event.TEIID31259, e,
QueryPlugin.Util.gs(QueryPlugin.Event.TEIID31259, t.image, t.beginLine, t.beginColumn, e.getMessage()));
} catch (ParseException e) {
throw new org.teiid.metadata.ParseException(QueryPlugin.Event.TEIID30386, convertParserException(e));
} finally {
tm.reinit();
}
}
}