blob: 61a770216e1ba185746e6bae53e8891611a1553b [file] [log] [blame]
/*
* 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.openjpa.jdbc.kernel;
import java.io.IOException;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.openjpa.jdbc.meta.ClassMapping;
import org.apache.openjpa.jdbc.meta.MappingRepository;
import org.apache.openjpa.jdbc.meta.QueryResultMapping;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.ResultSetResult;
import org.apache.openjpa.jdbc.sql.SQLBuffer;
import org.apache.openjpa.jdbc.sql.SQLExceptions;
import org.apache.openjpa.kernel.AbstractStoreQuery;
import org.apache.openjpa.kernel.QueryContext;
import org.apache.openjpa.kernel.StoreQuery;
import org.apache.openjpa.lib.rop.RangeResultObjectProvider;
import org.apache.openjpa.lib.rop.ResultObjectProvider;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.ClassMetaData;
import org.apache.openjpa.util.UserException;
/**
* A SQL query.
*
* @author Abe White
*/
public class SQLStoreQuery
extends AbstractStoreQuery {
private static final long serialVersionUID = 1L;
private static final Localizer _loc = Localizer.forPackage
(SQLStoreQuery.class);
private transient final JDBCStore _store;
/**
* Construct a query managed by the given context.
*/
public SQLStoreQuery(JDBCStore store) {
_store = store;
}
public JDBCStore getStore() {
return _store;
}
@Override
public boolean supportsParameterDeclarations() {
return false;
}
@Override
public boolean supportsDataStoreExecution() {
return true;
}
@Override
public Executor newDataStoreExecutor(ClassMetaData meta,
boolean subclasses) {
return new SQLExecutor(this, meta);
}
@Override
public boolean requiresCandidateType() {
return false;
}
@Override
public boolean requiresParameterDeclarations() {
return false;
}
/**
* Executes the filter as a SQL query.
*/
public static class SQLExecutor
extends AbstractExecutor {
private final ClassMetaData _meta;
private final boolean _select;
private final boolean _call; // native call stored procedure
private final QueryResultMapping _resultMapping;
public SQLExecutor(SQLStoreQuery q, ClassMetaData candidate) {
QueryContext ctx = q.getContext();
String resultMapping = ctx.getResultMappingName();
if (resultMapping == null)
_resultMapping = null;
else {
ClassLoader envLoader = ctx.getStoreContext().getClassLoader();
MappingRepository repos = q.getStore().getConfiguration().
getMappingRepositoryInstance();
_resultMapping = repos.getQueryResultMapping
(ctx.getResultMappingScope(), resultMapping, envLoader,
true);
}
_meta = candidate;
String sql = StringUtil.trimToNull(ctx.getQueryString());
if (sql == null)
throw new UserException(_loc.get("no-sql"));
_select = q.getStore().getDBDictionary().isSelect(sql);
_call = sql.length() > 4
&& sql.substring(0, 4).equalsIgnoreCase("call");
}
@Override
public int getOperation(StoreQuery q) {
return _select ? OP_SELECT :
(q.getContext().getCandidateType() != null
|| q.getContext().getResultType() != null
|| q.getContext().getResultMappingName() != null
|| q.getContext().getResultMappingScope() != null)
? OP_SELECT : OP_UPDATE;
}
@Override
public Number executeUpdate(StoreQuery q, Object[] params) {
JDBCStore store = ((SQLStoreQuery) q).getStore();
DBDictionary dict = store.getDBDictionary();
String sql = q.getContext().getQueryString();
List paramList = new ArrayList(Arrays.asList(params));
SQLBuffer buf = new SQLBuffer(dict).append(sql);
// we need to make sure we have an active store connection
store.getContext().beginStore();
Connection conn = store.getConnection();
JDBCFetchConfiguration fetch = (JDBCFetchConfiguration)
q.getContext().getFetchConfiguration();
PreparedStatement stmnt = null;
try {
if (_call)
stmnt = prepareCall(conn, buf);
else
stmnt = prepareStatement(conn, buf);
buf.setParameters(paramList);
if (stmnt != null)
buf.setParameters(stmnt);
dict.setTimeouts(stmnt, fetch, true);
int count = executeUpdate(store, conn, stmnt, buf);
return count;
} catch (SQLException se) {
throw SQLExceptions.getStore(se, dict);
} finally {
if (stmnt != null) {
try {
stmnt.close();
} catch (SQLException se) {
// safe to ignore
} finally {
stmnt = null;
}
}
try {
conn.close();
} catch (SQLException se) {
}
}
}
@Override
public ResultObjectProvider executeQuery(StoreQuery q,
Object[] params, Range range) {
JDBCStore store = ((SQLStoreQuery) q).getStore();
DBDictionary dict = store.getDBDictionary();
String sql = q.getContext().getQueryString();
List paramList = new ArrayList(Arrays.asList(params));
SQLBuffer buf = new SQLBuffer(dict).append(sql);
Connection conn = store.getConnection();
JDBCFetchConfiguration fetch = (JDBCFetchConfiguration)
q.getContext().getFetchConfiguration();
ResultObjectProvider rop;
PreparedStatement stmnt = null;
try {
// use the right method depending on sel vs. proc, lrs setting
if (_select && !range.lrs)
stmnt = prepareStatement(conn, buf);
else if (_select)
stmnt = prepareStatement(conn, buf, fetch, -1, -1);
else if (!range.lrs)
stmnt = prepareCall(conn, buf);
else
stmnt = prepareCall(conn, buf, fetch, -1, -1);
int index = 0;
for (Iterator i = paramList.iterator(); i.hasNext() &&
stmnt != null;)
dict.setUnknown(stmnt, ++index, i.next(), null);
dict.setTimeouts(stmnt, fetch, false);
ResultSet rs = executeQuery(store, conn, stmnt, buf, paramList);
ResultSetResult res = stmnt != null ?
new ResultSetResult(conn, stmnt, rs, store) :
new ResultSetResult(conn, rs, dict);
if (_resultMapping != null)
rop = new MappedQueryResultObjectProvider(_resultMapping,
store, fetch, res);
else if (q.getContext().getCandidateType() != null)
rop = new GenericResultObjectProvider((ClassMapping) _meta,
store, fetch, res);
else
rop = new SQLProjectionResultObjectProvider(store, fetch,
res, q.getContext().getResultType());
} catch (SQLException se) {
if (stmnt != null)
try { stmnt.close(); } catch (SQLException se2) {}
try { conn.close(); } catch (SQLException se2) {}
throw SQLExceptions.getStore(se, dict);
}
if (range.start != 0 || range.end != Long.MAX_VALUE)
rop = new RangeResultObjectProvider(rop, range.start,range.end);
return rop;
}
@Override
public String[] getDataStoreActions(StoreQuery q, Object[] params,
Range range) {
return new String[]{ q.getContext().getQueryString() };
}
@Override
public boolean isPacking(StoreQuery q) {
return q.getContext().getCandidateType() == null;
}
/**
* This method is to provide override for non-JDBC or JDBC-like
* implementation of preparing call statement.
*/
protected PreparedStatement prepareCall(Connection conn, SQLBuffer buf)
throws SQLException {
return buf.prepareCall(conn);
}
/**
* This method is to provide override for non-JDBC or JDBC-like
* implementation of executing update.
*/
protected int executeUpdate(JDBCStore store, Connection conn,
PreparedStatement stmnt, SQLBuffer buf)
throws SQLException {
int count = 0;
if (_call && !stmnt.execute()) {
count = stmnt.getUpdateCount();
}
else {
// native insert, update, delete
count = stmnt.executeUpdate();
}
return count;
}
/**
* This method is to provide override for non-JDBC or JDBC-like
* implementation of preparing call statement.
*/
protected PreparedStatement prepareCall(Connection conn, SQLBuffer buf,
JDBCFetchConfiguration fetch, int rsType, int rsConcur)
throws SQLException {
return buf.prepareCall(conn, fetch, rsType, rsConcur);
}
/**
* This method is to provide override for non-JDBC or JDBC-like
* implementation of preparing statement.
*/
protected PreparedStatement prepareStatement(Connection conn,
SQLBuffer buf) throws SQLException {
return buf.prepareStatement(conn);
}
/**
* This method is to provide override for non-JDBC or JDBC-like
* implementation of preparing statement.
*/
protected PreparedStatement prepareStatement(Connection conn,
SQLBuffer buf, JDBCFetchConfiguration fetch, int rsType,
int rsConcur) throws SQLException {
return buf.prepareStatement(conn, fetch, rsType, rsConcur);
}
/**
* This method is to provide override for non-JDBC or JDBC-like
* implementation of executing query.
*/
protected ResultSet executeQuery(JDBCStore store, Connection conn,
PreparedStatement stmnt, SQLBuffer buf, List paramList)
throws SQLException {
return stmnt.executeQuery();
}
/**
* The given query is parsed to find the parameter tokens of the form
* <code>?n</code> which is different than <code>?</code> tokens in
* actual SQL parameter tokens. These <code>?n</code> style tokens
* are replaced in the query string by <code>?</code> tokens.
*
* During the token parsing, the ordering of the tokens is recorded.
* The given userParam must contain parameter keys as Integer and
* the same Integers must appear in the tokens.
*
*/
@Override
public Object[] toParameterArray(StoreQuery q, Map userParams) {
if (userParams == null || userParams.isEmpty())
return StoreQuery.EMPTY_OBJECTS;
String sql = q.getContext().getQueryString();
List<Integer> paramOrder = new ArrayList<>();
try {
sql = substituteParams(sql, paramOrder);
} catch (IOException ex) {
throw new UserException(ex.getLocalizedMessage());
}
Object[] result = new Object[paramOrder.size()];
int idx = 0;
for (Integer key : paramOrder) {
if (!userParams.containsKey(key))
throw new UserException(_loc.get("uparam-missing",
key, sql, userParams));
result[idx++] = userParams.get(key);
}
// modify original JPA-style SQL to proper SQL
q.getContext().getQuery().setQuery(sql);
return result;
}
}
/**
* Utility method to substitute '?num' for parameters in the given SQL
* statement, and fill-in the order of the parameter tokens
*/
public static String substituteParams(String sql, List<Integer> paramOrder)
throws IOException {
// if there's no "?" parameter marker, then we don't need to
// perform the parsing process
if (sql.indexOf("?") == -1)
return sql;
sql = sql.replaceAll("\\\\", "\\\\\\\\");
paramOrder.clear();
StreamTokenizer tok = new StreamTokenizer(new StringReader(sql));
tok.resetSyntax();
tok.quoteChar('\'');
tok.wordChars('0', '9');
tok.wordChars('?', '?');
StringBuilder buf = new StringBuilder(sql.length());
for (int ttype; (ttype = tok.nextToken()) != StreamTokenizer.TT_EOF;) {
switch (ttype) {
case StreamTokenizer.TT_WORD:
// a token is a positional parameter if it starts with
// a "?" and the rest of the token are all numbers
if (tok.sval.startsWith("?")) {
buf.append("?");
String pIndex = tok.sval.substring(1);
if (pIndex.length() > 0) {
paramOrder.add(Integer.valueOf(pIndex));
} else { // or nothing
paramOrder.add(paramOrder.size()+1);
}
} else
buf.append(tok.sval);
break;
case'\'':
buf.append('\'');
if (tok.sval != null) {
buf.append(tok.sval);
buf.append('\'');
}
// // StreamTokenizer can not differentiate the last quoted token as in ^.*'.*$ and ^.*',*'$
// // need to check the last quote ends with "'" and process accordingly.
// if(endsWithQuote) {
// buf.append('\'');
// } else if (tok.nextToken() != StreamTokenizer.TT_EOF) {
// tok.pushBack();
// buf.append('\'');
// }
break;
default:
buf.append((char) ttype);
}
}
return buf.toString();
}
}