View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.transport.jdbc;
8   
9   import org.mule.api.MuleContext;
10  import org.mule.api.MuleException;
11  import org.mule.api.MuleMessage;
12  import org.mule.api.construct.FlowConstruct;
13  import org.mule.api.endpoint.ImmutableEndpoint;
14  import org.mule.api.endpoint.InboundEndpoint;
15  import org.mule.api.expression.ExpressionManager;
16  import org.mule.api.lifecycle.InitialisationException;
17  import org.mule.api.retry.RetryContext;
18  import org.mule.api.transaction.Transaction;
19  import org.mule.api.transaction.TransactionException;
20  import org.mule.api.transport.MessageReceiver;
21  import org.mule.config.ExceptionHelper;
22  import org.mule.config.i18n.MessageFactory;
23  import org.mule.transaction.TransactionCoordination;
24  import org.mule.transport.AbstractConnector;
25  import org.mule.transport.ConnectException;
26  import org.mule.transport.jdbc.sqlstrategy.DefaultSqlStatementStrategyFactory;
27  import org.mule.transport.jdbc.sqlstrategy.SqlStatementStrategyFactory;
28  import org.mule.transport.jdbc.xa.DataSourceWrapper;
29  import org.mule.util.StringUtils;
30  import org.mule.util.TemplateParser;
31  
32  import java.sql.Connection;
33  import java.text.MessageFormat;
34  import java.util.List;
35  import java.util.Map;
36  import java.util.regex.Matcher;
37  import java.util.regex.Pattern;
38  
39  import javax.sql.DataSource;
40  import javax.sql.XADataSource;
41  
42  import org.apache.commons.dbutils.QueryRunner;
43  import org.apache.commons.dbutils.ResultSetHandler;
44  
45  public class JdbcConnector extends AbstractConnector
46  {
47      public static final String JDBC = "jdbc";
48  
49      // These are properties that can be overridden on the Receiver by the endpoint
50      // declaration
51      public static final String PROPERTY_POLLING_FREQUENCY = "pollingFrequency";
52      public static final long DEFAULT_POLLING_FREQUENCY = 1000;
53  
54      private static final Pattern STATEMENT_ARGS = TemplateParser.WIGGLY_MULE_TEMPLATE_PATTERN;
55  
56      private SqlStatementStrategyFactory sqlStatementStrategyFactory = new DefaultSqlStatementStrategyFactory();
57  
58  
59      /* Register the SQL Exception reader if this class gets loaded */
60      static
61      {
62          ExceptionHelper.registerExceptionReader(new SQLExceptionReader());
63      }
64  
65      protected long pollingFrequency = 0;
66      protected Map queries;
67  
68      protected DataSource dataSource;
69      protected ResultSetHandler resultSetHandler;
70      protected QueryRunner queryRunner;
71      
72      private int queryTimeout;
73      
74      /** 
75       * Should each DB record be received in a separate transaction or should 
76       * there be a single transaction for the entire ResultSet? 
77       */
78      protected boolean transactionPerMessage = true;
79  
80      public JdbcConnector(MuleContext context)
81      {
82          super(context);
83      }
84      
85      protected void doInitialise() throws InitialisationException
86      {
87          createMultipleTransactedReceivers = false;
88  
89          if (dataSource == null)
90          {
91              throw new InitialisationException(MessageFactory.createStaticMessage("Missing data source"), this);
92          }
93          if (resultSetHandler == null)
94          {
95              resultSetHandler = new org.apache.commons.dbutils.handlers.MapListHandler(new ColumnAliasRowProcessor());
96          }
97          if (queryRunner == null)
98          {
99              if (this.queryTimeout >= 0)
100             {
101                 queryRunner = new ExtendedQueryRunner(dataSource, this.queryTimeout);
102             }
103             else
104             {
105                 // Fix for MULE-5825 (Mariano Capurro) -> Adding dataSource parameter
106                 queryRunner = new QueryRunner(dataSource);
107             }
108         }
109     }
110 
111     public MessageReceiver createReceiver(FlowConstruct flowConstruct, InboundEndpoint endpoint) throws Exception
112     {
113         Map props = endpoint.getProperties();
114         if (props != null)
115         {
116             String tempPolling = (String) props.get(PROPERTY_POLLING_FREQUENCY);
117             if (tempPolling != null)
118             {
119                 pollingFrequency = Long.parseLong(tempPolling);
120             }
121         }
122 
123         if (pollingFrequency <= 0)
124         {
125             pollingFrequency = DEFAULT_POLLING_FREQUENCY;
126         }
127 
128         String[] params = getReadAndAckStatements(endpoint);
129         return getServiceDescriptor().createMessageReceiver(this, flowConstruct, endpoint, params);
130     }
131 
132     public String[] getReadAndAckStatements(ImmutableEndpoint endpoint)
133     {
134         String str;
135 
136         // Find read statement
137         String readStmt;
138         if ((str = (String) endpoint.getProperty("sql")) != null)
139         {
140             readStmt = str;
141         }
142         else
143         {
144             readStmt = endpoint.getEndpointURI().getAddress();
145         }
146 
147         // Find ack statement
148         String ackStmt;
149         if ((str = (String) endpoint.getProperty("ack")) != null)
150         {
151             ackStmt = str;
152             if ((str = getQuery(endpoint, ackStmt)) != null)
153             {
154                 ackStmt = str;
155             }
156             ackStmt = ackStmt.trim();
157         }
158         else
159         {
160             ackStmt = readStmt + ".ack";
161             if ((str = getQuery(endpoint, ackStmt)) != null)
162             {
163                 ackStmt = str.trim();
164             }
165             else
166             {
167                 ackStmt = null;
168             }
169         }
170 
171         // Translate both using queries map
172         if ((str = getQuery(endpoint, readStmt)) != null)
173         {
174             readStmt = str;
175         }
176         if (readStmt == null)
177         {
178             throw new IllegalArgumentException("Read statement should not be null");
179         }
180         else
181         {
182             // MULE-3109: trim the readStatement for better user experience
183             readStmt = readStmt.trim();
184         }
185 
186         if (!"select".equalsIgnoreCase(readStmt.substring(0, 6)) && !"call".equalsIgnoreCase(readStmt.substring(0, 4)))
187         {
188             throw new IllegalArgumentException("Read statement should be a select sql statement or a stored procedure");
189         }
190         if (ackStmt != null)
191         {
192             if (!"insert".equalsIgnoreCase(ackStmt.substring(0, 6))
193                     && !"update".equalsIgnoreCase(ackStmt.substring(0, 6))
194                     && !"delete".equalsIgnoreCase(ackStmt.substring(0, 6)))
195             {
196                 throw new IllegalArgumentException(
197                         "Ack statement should be an insert / update / delete sql statement");
198             }
199         }
200         return new String[]{readStmt, ackStmt};
201     }
202 
203     public String getQuery(ImmutableEndpoint endpoint, String stmt)
204     {
205         Object query = null;
206         if (endpoint != null && endpoint.getProperties() != null)
207         {
208             Object queries = endpoint.getProperties().get("queries");
209             if (queries instanceof Map)
210             {
211                 query = ((Map) queries).get(stmt);
212             }
213         }
214         if (query == null)
215         {
216             if (this.queries != null)
217             {
218                 query = this.queries.get(stmt);
219             }
220         }
221         return query == null ? null : query.toString();
222     }
223 
224     public Connection getConnection() throws Exception
225     {
226         Transaction tx = TransactionCoordination.getInstance().getTransaction();
227         if (tx != null)
228         {
229             if (tx.hasResource(dataSource))
230             {
231                 logger.debug("Retrieving connection from current transaction: " + tx);
232                 return (Connection) tx.getResource(dataSource);
233             }
234         }
235         logger.debug("Retrieving new connection from data source");
236         
237         Connection con;
238         try
239         {
240             con = dataSource.getConnection();
241         }
242         catch (Exception e)
243         {
244             throw new ConnectException(e, this);
245         }
246 
247         if (tx != null)
248         {
249             logger.debug("Binding connection " + con + " to current transaction: " + tx);
250             try
251             {
252                 tx.bindResource(dataSource, con);
253             }
254             catch (TransactionException e)
255             {
256                 JdbcUtils.close(con);
257                 throw new RuntimeException("Could not bind connection to current transaction: " + tx, e);
258             }
259         }
260         return con;
261     }
262 
263     public boolean isTransactionPerMessage()
264     {
265         return transactionPerMessage;
266     }
267 
268     public void setTransactionPerMessage(boolean transactionPerMessage)
269     {
270         this.transactionPerMessage = transactionPerMessage;
271         if (!transactionPerMessage)
272         {
273             logger.warn("transactionPerMessage property is set to false so setting createMultipleTransactedReceivers " +
274                     "to false also to prevent creation of multiple JdbcMessageReceivers");
275             setCreateMultipleTransactedReceivers(transactionPerMessage);
276         }
277     }
278 
279     /**
280      * Parse the given statement filling the parameter list and return the ready to
281      * use statement.
282      *
283      * @param stmt
284      * @param params
285      */
286     public String parseStatement(String stmt, List params)
287     {
288         if (stmt == null)
289         {
290             return stmt;
291         }
292         Matcher m = STATEMENT_ARGS.matcher(stmt);
293         StringBuffer sb = new StringBuffer(200);
294         while (m.find())
295         {
296             String key = m.group();
297             m.appendReplacement(sb, "?");
298             //Special legacy handling for #[payload]
299             if (key.equals("#[payload]"))
300             {
301                 //MULE-3597
302                 logger.error("invalid expression template #[payload]. It should be replaced with #[payload:] to conform with the correct expression syntax. Mule has replaced this for you, but may not in future versions.");
303                 key = "#[payload:]";
304             }
305             params.add(key);
306         }
307         m.appendTail(sb);
308         return sb.toString();
309     }
310 
311     public Object[] getParams(ImmutableEndpoint endpoint, List paramNames, MuleMessage message, String query)
312             throws Exception
313     {
314         Object[] params = new Object[paramNames.size()];
315         for (int i = 0; i < paramNames.size(); i++)
316         {
317             String param = (String) paramNames.get(i);
318             Object value = getParamValue(endpoint, message, param);
319 
320             params[i] = value;
321         }
322         return params;
323     }
324 
325     protected Object getParamValue(ImmutableEndpoint endpoint, MuleMessage message, String param)
326     {
327         Object value = null;
328         // If we find a value and it happens to be null, that is acceptable
329         boolean foundValue = false;
330         boolean validExpression = muleContext.getExpressionManager().isValidExpression(param);
331 
332         //There must be an expression namespace to use the ExpressionEvaluator i.e. header:type
333         if (message != null && validExpression)
334         {
335             value = muleContext.getExpressionManager().evaluate(param, message);
336             foundValue = value != null;
337         }
338         if (!foundValue)
339         {
340             String name = getNameFromParam(param);
341             //MULE-3597
342             if (!validExpression)
343             {
344                 logger.warn(MessageFormat.format("Config is using the legacy param format {0} (no evaluator defined)." +
345                                                  " This expression can be replaced with {1}header:{2}{3}",
346                                                  param, ExpressionManager.DEFAULT_EXPRESSION_PREFIX,
347                                                  name, ExpressionManager.DEFAULT_EXPRESSION_POSTFIX));
348             }
349             value = endpoint.getProperty(name);
350         }
351         return value;
352     }
353 
354     protected String getNameFromParam(String param)
355     {
356         return param.substring(2, param.length() - 1);
357     }
358 
359     protected void doDispose()
360     {
361         // template method
362     }
363 
364     protected void doConnect() throws Exception
365     {
366         Connection connection = null;
367 
368         try
369         {
370             connection = getConnection();
371         }
372         finally
373         {
374             if (connection != null)
375             {
376                 connection.close();
377             }
378         }
379     }
380 
381     /** 
382      * Verify that we are able to connect to the DataSource (needed for retry policies)
383      * @param retryContext
384      */
385     public RetryContext validateConnection(RetryContext retryContext)
386     {
387         Connection con;
388         try
389         {
390             con = getConnection();
391             if (con != null)
392             {
393                 con.close();
394             }
395             retryContext.setOk();
396         }
397         catch (Exception ex)
398         {
399             retryContext.setFailed(ex);
400         }
401         finally
402         {
403             con = null;
404         }
405 
406         return retryContext;
407     }
408 
409     protected void doDisconnect() throws Exception
410     {
411         // template method
412     }
413 
414     protected void doStart() throws MuleException
415     {
416         // template method
417     }
418 
419     protected void doStop() throws MuleException
420     {
421         // template method
422     }
423 
424     //////////////////////////////////////////////////////////////////////////////////////
425     // Getters and Setters
426     //////////////////////////////////////////////////////////////////////////////////////
427 
428     public String getProtocol()
429     {
430         return JDBC;
431     }
432 
433     public DataSource getDataSource()
434     {
435         return dataSource;
436     }
437 
438     public void setDataSource(DataSource dataSource)
439     {
440         if (dataSource instanceof XADataSource)
441         {
442             this.dataSource = new DataSourceWrapper((XADataSource) dataSource);
443         }
444         else
445         {
446             this.dataSource = dataSource;
447         }
448     }
449 
450     public ResultSetHandler getResultSetHandler()
451     {
452         return resultSetHandler;
453     }
454 
455     public void setResultSetHandler(ResultSetHandler resultSetHandler)
456     {
457         this.resultSetHandler = resultSetHandler;
458     }
459 
460     public QueryRunner getQueryRunnerFor(ImmutableEndpoint endpoint)
461     {
462         String queryTimeoutAsString = (String) endpoint.getProperty("queryTimeout");
463         Integer queryTimeout = -1;
464         
465         try
466         {
467             queryTimeout = Integer.valueOf(queryTimeoutAsString);
468         }
469         catch (NumberFormatException e)
470         {
471 
472         }
473         
474         if (queryTimeout >= 0)
475         {
476 			ExtendedQueryRunner extendedQueryRunner = new ExtendedQueryRunner(
477 					this.queryRunner.getDataSource(), queryTimeout);
478 			return extendedQueryRunner;
479         }
480         else
481         {
482             return queryRunner;
483         }
484     }
485 
486     public QueryRunner getQueryRunner()
487     {
488         return queryRunner;
489     }
490 
491     public void setQueryRunner(QueryRunner queryRunner)
492     {
493         this.queryRunner = queryRunner;
494     }
495 
496     /**
497      * @return Returns the pollingFrequency.
498      */
499     public long getPollingFrequency()
500     {
501         return pollingFrequency;
502     }
503 
504     /**
505      * @param pollingFrequency The pollingFrequency to set.
506      */
507     public void setPollingFrequency(long pollingFrequency)
508     {
509         this.pollingFrequency = pollingFrequency;
510     }
511 
512     /**
513      * @return Returns the queries.
514      */
515     public Map getQueries()
516     {
517         return queries;
518     }
519 
520     /**
521      * @param queries The queries to set.
522      */
523     public void setQueries(Map queries)
524     {
525         this.queries = queries;
526     }
527 
528     public SqlStatementStrategyFactory getSqlStatementStrategyFactory()
529     {
530         return sqlStatementStrategyFactory;
531     }
532 
533     public void setSqlStatementStrategyFactory(SqlStatementStrategyFactory sqlStatementStrategyFactory)
534     {
535         this.sqlStatementStrategyFactory = sqlStatementStrategyFactory;
536     }
537 
538     public String getStatement(ImmutableEndpoint endpoint)
539     {
540         String writeStmt = endpoint.getEndpointURI().getAddress();
541         String str;
542         if ((str = getQuery(endpoint, writeStmt)) != null)
543         {
544             writeStmt = str;
545         }
546         writeStmt = StringUtils.trimToEmpty(writeStmt);
547         if (StringUtils.isBlank(writeStmt))
548         {
549             throw new IllegalArgumentException("Missing statement");
550         }
551 
552         return writeStmt;
553     }
554 
555     public int getQueryTimeout()
556     {
557         return queryTimeout;
558     }
559 
560     public void setQueryTimeout(int queryTimeout)
561     {
562         this.queryTimeout = queryTimeout;
563     }
564 }