/** * 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.metamodel.query.parser; import org.apache.metamodel.DataContext; import org.apache.metamodel.query.Query; /** * A parser class of for full SQL-like queries. */ public class QueryParser { private final DataContext _dataContext; private final String _queryString; private final String _queryStringUpperCase; public QueryParser(DataContext dataContext, String queryString) { if (dataContext == null) { throw new IllegalArgumentException("DataContext cannot be null"); } if (queryString == null) { throw new IllegalArgumentException("Query string cannot be null"); } _dataContext = dataContext; _queryString = prepareQuery(queryString); _queryStringUpperCase = _queryString.toUpperCase(); } /** * Performs any preparations (not changing any semantics) to the query * string * * @param queryString * @return */ private String prepareQuery(String queryString) { queryString = queryString.replaceAll("[\n\r\t]", " "); queryString = queryString.replaceAll(" ", " "); queryString = queryString.trim(); return queryString; } public Query parse() throws QueryParserException { final Query query = new Query(); // collect focal point query clauses int[] selectIndices = indexesOf("SELECT ", null); int[] fromIndices = indexesOf(" FROM ", selectIndices); int[] whereIndices = indexesOf(" WHERE ", fromIndices); int[] groupByIndices = indexesOf(" GROUP BY ", whereIndices); int[] havingIndices = indexesOf(" HAVING ", groupByIndices); int[] orderByIndices = indexesOf(" ORDER BY", havingIndices); int[] limitIndices = indexesOf(" LIMIT ", orderByIndices); int[] offsetIndices = indexesOf(" OFFSET ", limitIndices); // a few validations, minimum requirements if (selectIndices == null) { throw new QueryParserException("SELECT not found in query: " + _queryString); } if (fromIndices == null) { throw new QueryParserException("FROM not found in query: " + _queryString); } // parse FROM { final String fromClause = getSubstring( getLastEndIndex(fromIndices), getNextStartIndex(whereIndices, groupByIndices, havingIndices, orderByIndices, limitIndices, offsetIndices)); parseFromClause(query, fromClause); } { String selectClause = getSubstring(getLastEndIndex(selectIndices), fromIndices[0]); if (selectClause.toUpperCase().startsWith("DISTINCT ")) { query.selectDistinct(); selectClause = selectClause.substring("DISTINCT ".length()); } parseSelectClause(query, selectClause); } if (whereIndices != null) { final String whereClause = getSubstring(getLastEndIndex(whereIndices), getNextStartIndex(groupByIndices, havingIndices, orderByIndices, limitIndices, offsetIndices)); if (whereClause != null) { parseWhereClause(query, whereClause); } } if (groupByIndices != null) { final String groupByClause = getSubstring(getLastEndIndex(groupByIndices, whereIndices), getNextStartIndex(havingIndices, orderByIndices, limitIndices, offsetIndices)); if (groupByClause != null) { parseGroupByClause(query, groupByClause); } } if (havingIndices != null) { final String havingClause = getSubstring( getLastEndIndex(havingIndices, groupByIndices, whereIndices, fromIndices, selectIndices), getNextStartIndex(orderByIndices, limitIndices, offsetIndices)); if (havingClause != null) { parseHavingClause(query, havingClause); } } if (orderByIndices != null) { final String orderByClause = getSubstring( getLastEndIndex(orderByIndices, havingIndices, groupByIndices, whereIndices, fromIndices, selectIndices), getNextStartIndex(limitIndices, offsetIndices)); if (orderByClause != null) { parseOrderByClause(query, orderByClause); } } if (limitIndices != null) { final String limitClause = getSubstring( getLastEndIndex(limitIndices, orderByIndices, havingIndices, groupByIndices, whereIndices, fromIndices, selectIndices), getNextStartIndex(offsetIndices)); if (limitClause != null) { parseLimitClause(query, limitClause); } } if (offsetIndices != null) { final String offsetClause = getSubstring( getLastEndIndex(offsetIndices, limitIndices, orderByIndices, havingIndices, groupByIndices, whereIndices, fromIndices, selectIndices), getNextStartIndex()); if (offsetClause != null) { parseOffsetClause(query, offsetClause); } } return query; } private void parseFromClause(Query query, String fromClause) { QueryPartParser clauseParser = new QueryPartParser(new FromItemParser(_dataContext, query), fromClause, ","); clauseParser.parse(); } private void parseSelectClause(Query query, String selectClause) { QueryPartParser clauseParser = new QueryPartParser(new SelectItemParser(query, false), selectClause, ","); clauseParser.parse(); } private void parseWhereClause(Query query, String whereClause) { // only parse "AND" delimitors, since "OR" will be taken care of as // compound filter items at 2nd level parsing QueryPartParser clauseParser = new QueryPartParser(new WhereItemParser(query), whereClause, " AND "); clauseParser.parse(); } private void parseGroupByClause(Query query, String groupByClause) { QueryPartParser clauseParser = new QueryPartParser(new GroupByItemParser(query), groupByClause, ","); clauseParser.parse(); } private void parseHavingClause(Query query, String havingClause) { // only parse "AND" delimitors, since "OR" will be taken care of as // compound filter items at 2nd level parsing QueryPartParser clauseParser = new QueryPartParser(new HavingItemParser(query), havingClause, " AND "); clauseParser.parse(); } private void parseOrderByClause(Query query, String orderByClause) { QueryPartParser clauseParser = new QueryPartParser(new OrderByItemParser(query), orderByClause, ","); clauseParser.parse(); } private void parseLimitClause(Query query, String limitClause) { limitClause = limitClause.trim(); if (!limitClause.isEmpty()) { try { int limit = Integer.parseInt(limitClause); query.setMaxRows(limit); } catch (NumberFormatException e) { throw new QueryParserException("Could not parse LIMIT value: " + limitClause); } } } private void parseOffsetClause(Query query, String offsetClause) { offsetClause = offsetClause.trim(); if (!offsetClause.isEmpty()) { try { final int offset = Integer.parseInt(offsetClause); // ofset is 0-based, but first-row is 1-based final int firstRow = offset + 1; query.setFirstRow(firstRow); } catch (NumberFormatException e) { throw new QueryParserException("Could not parse OFFSET value: " + offsetClause); } } } private String getSubstring(Integer from, int to) { if (from == null) { return null; } if (from.intValue() == to) { return null; } return _queryString.substring(from, to); } private int getNextStartIndex(int[]... indicesArray) { for (int[] indices : indicesArray) { if (indices != null) { return indices[0]; } } return _queryString.length(); } private Integer getLastEndIndex(int[]... indicesArray) { for (int[] indices : indicesArray) { if (indices != null) { return indices[1]; } } return null; } /** * Finds the start and end indexes of a string in the query. The string * parameter of this method is expected to be in upper case, while the query * itself is tolerant of case differences. * * @param string * @param previousIndices * @return */ protected int[] indexesOf(String string, int[] previousIndices) { final int startIndex; if (previousIndices == null) { startIndex = _queryStringUpperCase.indexOf(string); } else { startIndex = _queryStringUpperCase.indexOf(string, previousIndices[1]); } if (startIndex == -1) { return null; } int endIndex = startIndex + string.length(); return new int[] { startIndex, endIndex }; } }