001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: */
018:
019: package org.apache.jmeter.protocol.jdbc.sampler;
020:
021: import java.lang.reflect.Field;
022: import java.sql.CallableStatement;
023: import java.sql.Connection;
024: import java.sql.PreparedStatement;
025: import java.sql.ResultSet;
026: import java.sql.ResultSetMetaData;
027: import java.sql.SQLException;
028: import java.sql.Statement;
029: import java.util.Collection;
030: import java.util.HashMap;
031: import java.util.Iterator;
032: import java.util.LinkedHashMap;
033: import java.util.Map;
034:
035: import org.apache.jmeter.protocol.jdbc.config.DataSourceElement;
036: import org.apache.jmeter.samplers.AbstractSampler;
037: import org.apache.jmeter.samplers.Entry;
038: import org.apache.jmeter.samplers.SampleResult;
039: import org.apache.jmeter.testbeans.TestBean;
040: import org.apache.jmeter.util.JMeterUtils;
041: import org.apache.jorphan.collections.Data;
042: import org.apache.jorphan.logging.LoggingManager;
043: import org.apache.log.Logger;
044:
045: /**
046: * A sampler which understands JDBC database requests.
047: *
048: */
049: public class JDBCSampler extends AbstractSampler implements TestBean {
050: private static final Logger log = LoggingManager
051: .getLoggerForClass();
052:
053: // This value is used for both the connection (perConnCache) and statement (preparedStatementMap) caches.
054: // TODO - do they have to be the same size?
055: private static final int MAX_ENTRIES = JMeterUtils.getPropDefault(
056: "jdbcsampler.cachesize", 200); // $NON-NLS-1$
057:
058: // String used to indicate a null value
059: private static final String NULL_MARKER = JMeterUtils
060: .getPropDefault("jdbcsampler.nullmarker", "]NULL["); // $NON-NLS-1$
061:
062: private static final String INOUT = "INOUT"; // $NON-NLS-1$
063:
064: private static final String OUT = "OUT"; // $NON-NLS-1$
065:
066: private static final Map mapJdbcNameToInt;
067:
068: static {
069: // based on e291. Getting the Name of a JDBC Type from javaalmanac.com
070: // http://javaalmanac.com/egs/java.sql/JdbcInt2Str.html
071: mapJdbcNameToInt = new HashMap();
072:
073: //Get all fields in java.sql.Types and store the corresponding int values
074: Field[] fields = java.sql.Types.class.getFields();
075: for (int i = 0; i < fields.length; i++) {
076: try {
077: String name = fields[i].getName();
078: Integer value = (Integer) fields[i].get(null);
079: mapJdbcNameToInt.put(name.toLowerCase(), value);
080: } catch (IllegalAccessException e) {
081: throw new RuntimeException(e); // should not happen
082: }
083: }
084: }
085:
086: // Query types (used to communicate with GUI)
087: // N.B. These must not be changed, as they are used in the JMX files
088: static final String SELECT = "Select Statement"; // $NON-NLS-1$
089: static final String UPDATE = "Update Statement"; // $NON-NLS-1$
090: static final String CALLABLE = "Callable Statement"; // $NON-NLS-1$
091: static final String PREPARED_SELECT = "Prepared Select Statement"; // $NON-NLS-1$
092: static final String PREPARED_UPDATE = "Prepared Update Statement"; // $NON-NLS-1$
093: static final String COMMIT = "Commit"; // $NON-NLS-1$
094: static final String ROLLBACK = "Rollback"; // $NON-NLS-1$
095: static final String AUTOCOMMIT_FALSE = "AutoCommit(false)"; // $NON-NLS-1$
096: static final String AUTOCOMMIT_TRUE = "AutoCommit(true)"; // $NON-NLS-1$
097:
098: private String query = ""; // $NON-NLS-1$
099:
100: private String dataSource = ""; // $NON-NLS-1$
101:
102: private String queryType = SELECT;
103: private String queryArguments = ""; // $NON-NLS-1$
104: private String queryArgumentsTypes = ""; // $NON-NLS-1$
105:
106: /**
107: * Cache of PreparedStatements stored in a per-connection basis. Each entry of this
108: * cache is another Map mapping the statement string to the actual PreparedStatement.
109: * The cache has a fixed size of MAX_ENTRIES and it will throw aways all PreparedStatements
110: * from the least recently used connections.
111: */
112: private static Map perConnCache = new LinkedHashMap(MAX_ENTRIES) {
113: private static final long serialVersionUID = 1L;
114:
115: protected boolean removeEldestEntry(java.util.Map.Entry arg0) {
116: if (size() > MAX_ENTRIES) {
117: final Object value = arg0.getValue();
118: if (value instanceof Map) {
119: closeAllStatements(((Map) value).values());
120: }
121: return true;
122: }
123: return false;
124: }
125: };
126:
127: /**
128: * Creates a JDBCSampler.
129: */
130: public JDBCSampler() {
131: }
132:
133: public SampleResult sample(Entry e) {
134: log.debug("sampling jdbc");
135:
136: SampleResult res = new SampleResult();
137: res.setSampleLabel(getName());
138: res.setSamplerData(toString());
139: res.setDataType(SampleResult.TEXT);
140: // Bug 31184 - make sure encoding is specified
141: res.setDataEncoding(System.getProperty("file.encoding")); // $NON-NLS-1$
142:
143: // Assume we will be successful
144: res.setSuccessful(true);
145:
146: res.sampleStart();
147: Connection conn = null;
148: Statement stmt = null;
149:
150: try {
151:
152: try {
153: conn = DataSourceElement.getConnection(getDataSource());
154: } finally {
155: res.latencyEnd(); // use latency to measure connection time
156: }
157: res.setResponseHeaders(conn.toString());
158:
159: // Based on query return value, get results
160: String _queryType = getQueryType();
161: if (SELECT.equals(_queryType)) {
162: stmt = conn.createStatement();
163: ResultSet rs = null;
164: try {
165: rs = stmt.executeQuery(getQuery());
166: Data data = getDataFromResultSet(rs);
167: res.setResponseData(data.toString().getBytes());
168: } finally {
169: close(rs);
170: }
171: } else if (CALLABLE.equals(_queryType)) {
172: CallableStatement cstmt = getCallableStatement(conn);
173: int out[] = setArguments(cstmt);
174: // A CallableStatement can return more than 1 ResultSets
175: // plus a number of update counts.
176: boolean hasResultSet = cstmt.execute();
177: String sb = resultSetsToString(cstmt, hasResultSet, out);
178: res.setResponseData(sb.toString().getBytes());
179: } else if (UPDATE.equals(_queryType)) {
180: stmt = conn.createStatement();
181: stmt.executeUpdate(getQuery());
182: int updateCount = stmt.getUpdateCount();
183: String results = updateCount + " updates";
184: res.setResponseData(results.getBytes());
185: } else if (PREPARED_SELECT.equals(_queryType)) {
186: PreparedStatement pstmt = getPreparedStatement(conn);
187: setArguments(pstmt);
188: pstmt.executeQuery();
189: String sb = resultSetsToString(pstmt, true, null);
190: res.setResponseData(sb.toString().getBytes());
191: } else if (PREPARED_UPDATE.equals(_queryType)) {
192: PreparedStatement pstmt = getPreparedStatement(conn);
193: setArguments(pstmt);
194: pstmt.executeUpdate();
195: String sb = resultSetsToString(pstmt, false, null);
196: res.setResponseData(sb.toString().getBytes());
197: } else if (ROLLBACK.equals(_queryType)) {
198: conn.rollback();
199: res.setResponseData(ROLLBACK.getBytes());
200: } else if (COMMIT.equals(_queryType)) {
201: conn.commit();
202: res.setResponseData(COMMIT.getBytes());
203: } else if (AUTOCOMMIT_FALSE.equals(_queryType)) {
204: conn.setAutoCommit(false);
205: res.setResponseData(AUTOCOMMIT_FALSE.getBytes());
206: } else if (AUTOCOMMIT_TRUE.equals(_queryType)) {
207: conn.setAutoCommit(true);
208: res.setResponseData(AUTOCOMMIT_TRUE.getBytes());
209: } else { // User provided incorrect query type
210: String results = "Unexpected query type: " + _queryType;
211: res.setResponseMessage(results);
212: res.setSuccessful(false);
213: }
214:
215: } catch (SQLException ex) {
216: final String errCode = Integer.toString(ex.getErrorCode());
217: res.setResponseMessage(ex.toString());
218: res.setResponseCode(ex.getSQLState() + " " + errCode);
219: res.setSuccessful(false);
220: } finally {
221: close(stmt);
222: close(conn);
223: }
224:
225: // TODO: process warnings? Set Code and Message to success?
226: res.sampleEnd();
227: return res;
228: }
229:
230: private String resultSetsToString(PreparedStatement pstmt,
231: boolean result, int[] out) throws SQLException {
232: StringBuffer sb = new StringBuffer();
233: sb.append("\n"); // $NON-NLS-1$
234: int updateCount = 0;
235: if (!result) {
236: updateCount = pstmt.getUpdateCount();
237: }
238: do {
239: if (result) {
240: ResultSet rs = null;
241: try {
242: rs = pstmt.getResultSet();
243: Data data = getDataFromResultSet(rs);
244: sb.append(data.toString()).append("\n"); // $NON-NLS-1$
245: } finally {
246: close(rs);
247: }
248: } else {
249: sb.append(updateCount).append(" updates.\n");
250: }
251: result = pstmt.getMoreResults();
252: if (!result) {
253: updateCount = pstmt.getUpdateCount();
254: }
255: } while (result || (updateCount != -1));
256: if (out != null && pstmt instanceof CallableStatement) {
257: CallableStatement cs = (CallableStatement) pstmt;
258: sb.append("Output variables by position:\n");
259: for (int i = 0; i < out.length; i++) {
260: if (out[i] != java.sql.Types.NULL) {
261: sb.append("[");
262: sb.append(i + 1);
263: sb.append("] ");
264: sb.append(cs.getObject(i + 1));
265: sb.append("\n");
266: }
267: }
268: }
269: return sb.toString();
270: }
271:
272: private int[] setArguments(PreparedStatement pstmt)
273: throws SQLException {
274: if (getQueryArguments().trim().length() == 0) {
275: return new int[] {};
276: }
277: String[] arguments = getQueryArguments().split(","); // $NON-NLS-1$
278: String[] argumentsTypes = getQueryArgumentsTypes().split(","); // $NON-NLS-1$
279: if (arguments.length != argumentsTypes.length) {
280: throw new SQLException("number of arguments ("
281: + arguments.length + ") and number of types ("
282: + argumentsTypes.length + ") are not equal");
283: }
284: int[] outputs = new int[arguments.length];
285: for (int i = 0; i < arguments.length; i++) {
286: String argument = arguments[i];
287: String argumentType = argumentsTypes[i];
288: String[] arg = argumentType.split(" ");
289: String inputOutput = "";
290: if (arg.length > 1) {
291: argumentType = arg[1];
292: inputOutput = arg[0];
293: }
294: int targetSqlType = getJdbcType(argumentType);
295: try {
296: if (!OUT.equalsIgnoreCase(inputOutput)) {
297: if (argument.equals(NULL_MARKER)) {
298: pstmt.setNull(i + 1, targetSqlType);
299: } else {
300: pstmt.setObject(i + 1, argument, targetSqlType);
301: }
302: }
303: if (OUT.equalsIgnoreCase(inputOutput)
304: || INOUT.equalsIgnoreCase(inputOutput)) {
305: CallableStatement cs = (CallableStatement) pstmt;
306: cs.registerOutParameter(i + 1, targetSqlType);
307: outputs[i] = targetSqlType;
308: } else {
309: outputs[i] = java.sql.Types.NULL; // can't have an output parameter type null
310: }
311: } catch (NullPointerException e) { // thrown by Derby JDBC (at least) if there are no "?" markers in statement
312: throw new SQLException("Could not set argument no: "
313: + (i + 1) + " - missing parameter marker?");
314: }
315: }
316: return outputs;
317: }
318:
319: private static int getJdbcType(String jdbcType) throws SQLException {
320: Integer entry = (Integer) mapJdbcNameToInt.get(jdbcType
321: .toLowerCase());
322: if (entry == null) {
323: throw new SQLException("Invalid data type: " + jdbcType);
324: }
325: return (entry).intValue();
326: }
327:
328: private CallableStatement getCallableStatement(Connection conn)
329: throws SQLException {
330: return (CallableStatement) getPreparedStatement(conn, true);
331:
332: }
333:
334: private PreparedStatement getPreparedStatement(Connection conn)
335: throws SQLException {
336: return getPreparedStatement(conn, false);
337: }
338:
339: private PreparedStatement getPreparedStatement(Connection conn,
340: boolean callable) throws SQLException {
341: Map preparedStatementMap = (Map) perConnCache.get(conn);
342: if (null == preparedStatementMap) {
343: // MRU PreparedStatements cache.
344: preparedStatementMap = new LinkedHashMap(MAX_ENTRIES) {
345: protected boolean removeEldestEntry(
346: java.util.Map.Entry arg0) {
347: final int theSize = size();
348: if (theSize > MAX_ENTRIES) {
349: Object value = arg0.getValue();
350: if (value instanceof PreparedStatement) {
351: PreparedStatement pstmt = (PreparedStatement) value;
352: close(pstmt);
353: }
354: return true;
355: }
356: return false;
357: }
358: };
359: perConnCache.put(conn, preparedStatementMap);
360: }
361: PreparedStatement pstmt = (PreparedStatement) preparedStatementMap
362: .get(getQuery());
363: if (null == pstmt) {
364: if (callable) {
365: pstmt = conn.prepareCall(getQuery());
366: } else {
367: pstmt = conn.prepareStatement(getQuery());
368: }
369: preparedStatementMap.put(getQuery(), pstmt);
370: }
371: pstmt.clearParameters();
372: return pstmt;
373: }
374:
375: private static void closeAllStatements(Collection collection) {
376: Iterator iterator = collection.iterator();
377: while (iterator.hasNext()) {
378: PreparedStatement pstmt = (PreparedStatement) iterator
379: .next();
380: close(pstmt);
381: }
382: }
383:
384: /**
385: * Gets a Data object from a ResultSet.
386: *
387: * @param rs
388: * ResultSet passed in from a database query
389: * @return a Data object
390: * @throws java.sql.SQLException
391: */
392: private Data getDataFromResultSet(ResultSet rs) throws SQLException {
393: ResultSetMetaData meta = rs.getMetaData();
394: Data data = new Data();
395:
396: int numColumns = meta.getColumnCount();
397: String[] dbCols = new String[numColumns];
398: for (int i = 0; i < numColumns; i++) {
399: dbCols[i] = meta.getColumnName(i + 1);
400: data.addHeader(dbCols[i]);
401: }
402:
403: while (rs.next()) {
404: data.next();
405: for (int i = 0; i < numColumns; i++) {
406: Object o = rs.getObject(i + 1);
407: if (o instanceof byte[]) {
408: o = new String((byte[]) o);
409: }
410: data.addColumnValue(dbCols[i], o);
411: }
412: }
413: return data;
414: }
415:
416: public static void close(Connection c) {
417: try {
418: if (c != null)
419: c.close();
420: } catch (SQLException e) {
421: log.warn("Error closing Connection", e);
422: }
423: }
424:
425: public static void close(Statement s) {
426: try {
427: if (s != null)
428: s.close();
429: } catch (SQLException e) {
430: log.warn("Error closing Statement " + s.toString(), e);
431: }
432: }
433:
434: public static void close(ResultSet rs) {
435: try {
436: if (rs != null)
437: rs.close();
438: } catch (SQLException e) {
439: log.warn("Error closing ResultSet", e);
440: }
441: }
442:
443: public String getQuery() {
444: return query;
445: }
446:
447: public String toString() {
448: StringBuffer sb = new StringBuffer(80);
449: sb.append("["); // $NON-NLS-1$
450: sb.append(getQueryType());
451: sb.append("] "); // $NON-NLS-1$
452: sb.append(getQuery());
453: return sb.toString();
454: }
455:
456: /**
457: * @param query
458: * The query to set.
459: */
460: public void setQuery(String query) {
461: this .query = query;
462: }
463:
464: /**
465: * @return Returns the dataSource.
466: */
467: public String getDataSource() {
468: return dataSource;
469: }
470:
471: /**
472: * @param dataSource
473: * The dataSource to set.
474: */
475: public void setDataSource(String dataSource) {
476: this .dataSource = dataSource;
477: }
478:
479: /**
480: * @return Returns the queryType.
481: */
482: public String getQueryType() {
483: return queryType;
484: }
485:
486: /**
487: * @param queryType The queryType to set.
488: */
489: public void setQueryType(String queryType) {
490: this .queryType = queryType;
491: }
492:
493: public String getQueryArguments() {
494: return queryArguments;
495: }
496:
497: public void setQueryArguments(String queryArguments) {
498: this .queryArguments = queryArguments;
499: }
500:
501: public String getQueryArgumentsTypes() {
502: return queryArgumentsTypes;
503: }
504:
505: public void setQueryArgumentsTypes(String queryArgumentsType) {
506: this.queryArgumentsTypes = queryArgumentsType;
507: }
508: }
|