| /* |
| * 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(); |
| } |
| } |