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 package org.apache.camel.processor.aggregate.jdbc;
018
019 import java.io.IOException;
020 import java.sql.PreparedStatement;
021 import java.sql.ResultSet;
022 import java.sql.SQLException;
023 import java.util.LinkedHashSet;
024 import java.util.List;
025 import java.util.Set;
026 import java.util.concurrent.TimeUnit;
027
028 import javax.sql.DataSource;
029
030 import org.apache.camel.CamelContext;
031 import org.apache.camel.Exchange;
032 import org.apache.camel.impl.ServiceSupport;
033 import org.apache.camel.spi.RecoverableAggregationRepository;
034 import org.apache.camel.util.ObjectHelper;
035 import org.slf4j.Logger;
036 import org.slf4j.LoggerFactory;
037
038 import org.springframework.dao.EmptyResultDataAccessException;
039 import org.springframework.jdbc.core.JdbcTemplate;
040 import org.springframework.jdbc.core.RowMapper;
041 import org.springframework.jdbc.core.support.AbstractLobCreatingPreparedStatementCallback;
042 import org.springframework.jdbc.support.lob.DefaultLobHandler;
043 import org.springframework.jdbc.support.lob.LobCreator;
044 import org.springframework.jdbc.support.lob.LobHandler;
045 import org.springframework.transaction.PlatformTransactionManager;
046 import org.springframework.transaction.TransactionStatus;
047 import org.springframework.transaction.support.TransactionCallback;
048 import org.springframework.transaction.support.TransactionCallbackWithoutResult;
049 import org.springframework.transaction.support.TransactionTemplate;
050
051 /**
052 * JDBC based {@link org.apache.camel.spi.AggregationRepository}
053 */
054 public class JdbcAggregationRepository extends ServiceSupport implements RecoverableAggregationRepository {
055
056 private static final transient Logger LOG = LoggerFactory.getLogger(JdbcAggregationRepository.class);
057 private static final String ID = "id";
058 private static final String EXCHANGE = "exchange";
059 private PlatformTransactionManager transactionManager;
060 private DataSource dataSource;
061 private TransactionTemplate transactionTemplate;
062 private TransactionTemplate transactionTemplateReadOnly;
063 private JdbcTemplate jdbcTemplate;
064 private LobHandler lobHandler = new DefaultLobHandler();
065 private String repositoryName;
066 private boolean returnOldExchange;
067 private JdbcCamelCodec codec = new JdbcCamelCodec();
068 private long recoveryInterval = 5000;
069 private boolean useRecovery = true;
070 private int maximumRedeliveries;
071 private String deadLetterUri;
072
073 /**
074 * Creates an aggregation repository
075 */
076 public JdbcAggregationRepository() {
077 }
078
079 /**
080 * Creates an aggregation repository with the three mandatory parameters
081 */
082 public JdbcAggregationRepository(PlatformTransactionManager transactionManager, String repositoryName, DataSource dataSource) {
083 this.setRepositoryName(repositoryName);
084 this.setTransactionManager(transactionManager);
085 this.setDataSource(dataSource);
086 }
087
088 /**
089 * @param repositoryName the repositoryName to set
090 */
091 public final void setRepositoryName(String repositoryName) {
092 this.repositoryName = repositoryName;
093 }
094
095 public final void setTransactionManager(PlatformTransactionManager transactionManager) {
096 this.transactionManager = transactionManager;
097
098 transactionTemplate = new TransactionTemplate(transactionManager);
099 transactionTemplate.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
100
101 transactionTemplateReadOnly = new TransactionTemplate(transactionManager);
102 transactionTemplateReadOnly.setPropagationBehavior(TransactionTemplate.PROPAGATION_REQUIRED);
103 transactionTemplateReadOnly.setReadOnly(true);
104 }
105
106 public final void setDataSource(DataSource dataSource) {
107 this.dataSource = dataSource;
108
109 jdbcTemplate = new JdbcTemplate(dataSource);
110 }
111
112 @SuppressWarnings("unchecked")
113 public Exchange add(final CamelContext camelContext, final String correlationId, final Exchange exchange) {
114 return (Exchange) transactionTemplate.execute(new TransactionCallback() {
115
116 public Exchange doInTransaction(TransactionStatus status) {
117 String sql;
118 Exchange result = null;
119 final String key = correlationId;
120
121 try {
122 final byte[] data = codec.marshallExchange(camelContext, exchange);
123
124 if (LOG.isDebugEnabled()) {
125 LOG.debug("Adding exchange with key: [" + key + "]");
126 }
127
128 String insert = "INSERT INTO " + getRepositoryName() + " (" + EXCHANGE + ", " + ID + ") VALUES (?, ?)";
129 String update = "UPDATE " + getRepositoryName() + " SET " + EXCHANGE + " = ? WHERE " + ID + " = ?";
130
131 boolean present = jdbcTemplate.queryForInt(
132 "SELECT COUNT (*) FROM " + getRepositoryName() + " WHERE " + ID + " = ?", key) != 0;
133 sql = present ? update : insert;
134
135 // Recover existing exchange with that ID
136 if (isReturnOldExchange() && present) {
137 result = get(key, getRepositoryName(), camelContext);
138 }
139
140 jdbcTemplate.execute(sql,
141 new AbstractLobCreatingPreparedStatementCallback(getLobHandler()) {
142 @Override
143 protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
144 lobCreator.setBlobAsBytes(ps, 1, data);
145 ps.setString(2, key);
146 }
147 });
148
149 } catch (IOException e) {
150 throw new RuntimeException("Error adding to repository " + repositoryName + " with key " + key, e);
151 }
152
153 return result;
154 }
155 });
156
157 }
158
159 public Exchange get(final CamelContext camelContext, final String correlationId) {
160 final String key = correlationId;
161 Exchange result = get(key, getRepositoryName(), camelContext);
162
163 if (LOG.isDebugEnabled()) {
164 LOG.debug("Getting key [" + key + "] -> " + result);
165 }
166
167 return result;
168 }
169
170 @SuppressWarnings("unchecked")
171 private Exchange get(final String key, final String repositoryName, final CamelContext camelContext) {
172 return (Exchange) transactionTemplateReadOnly.execute(new TransactionCallback() {
173 public Exchange doInTransaction(TransactionStatus status) {
174 try {
175 final byte[] data = jdbcTemplate.queryForObject(
176 "SELECT " + EXCHANGE + " FROM " + repositoryName + " WHERE " + ID + " = ?",
177 new Object[]{key}, byte[].class);
178 return codec.unmarshallExchange(camelContext, data);
179 } catch (EmptyResultDataAccessException ex) {
180 return null;
181 } catch (IOException ex) {
182 // Rollback the transaction
183 throw new RuntimeException("Error getting key " + key + " from repository " + repositoryName, ex);
184 } catch (ClassNotFoundException ex) {
185 // Rollback the transaction
186 throw new RuntimeException(ex);
187 }
188 }
189 });
190 }
191
192 public void remove(final CamelContext camelContext, final String correlationId, final Exchange exchange) {
193 transactionTemplate.execute(new TransactionCallbackWithoutResult() {
194 protected void doInTransactionWithoutResult(TransactionStatus status) {
195 final String key = correlationId;
196 final String confirmKey = exchange.getExchangeId();
197 try {
198 final byte[] data = codec.marshallExchange(camelContext, exchange);
199
200 if (LOG.isDebugEnabled()) {
201 LOG.debug("Removing key [" + key + "]");
202 }
203
204 jdbcTemplate.update("DELETE FROM " + getRepositoryName() + " WHERE " + ID + " = ?",
205 new Object[]{key});
206
207 jdbcTemplate.execute("INSERT INTO " + getRepositoryNameCompleted() + " (" + EXCHANGE + ", " + ID + ") VALUES (?, ?)",
208 new AbstractLobCreatingPreparedStatementCallback(getLobHandler()) {
209 @Override
210 protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
211 lobCreator.setBlobAsBytes(ps, 1, data);
212 ps.setString(2, confirmKey);
213 }
214 });
215 } catch (IOException e) {
216 throw new RuntimeException("Error removing key " + key + " from repository " + repositoryName, e);
217 }
218 }
219 });
220 }
221
222 public void confirm(final CamelContext camelContext, final String exchangeId) {
223 transactionTemplate.execute(new TransactionCallbackWithoutResult() {
224 protected void doInTransactionWithoutResult(TransactionStatus status) {
225 if (LOG.isDebugEnabled()) {
226 LOG.debug("Confirming exchangeId [" + exchangeId + "]");
227 }
228 final String confirmKey = exchangeId;
229
230 jdbcTemplate.update("DELETE FROM " + getRepositoryNameCompleted() + " WHERE " + ID + " = ?",
231 new Object[]{confirmKey});
232
233 }
234 });
235 }
236
237 @SuppressWarnings("unchecked")
238 public Set<String> getKeys() {
239 return (LinkedHashSet<String>) transactionTemplateReadOnly.execute(new TransactionCallback() {
240 public LinkedHashSet<String> doInTransaction(TransactionStatus status) {
241 List<String> keys = jdbcTemplate.query("SELECT " + ID + " FROM " + getRepositoryName(),
242 new RowMapper<String>() {
243 public String mapRow(ResultSet rs, int rowNum) throws SQLException {
244 String id = rs.getString(ID);
245 if (LOG.isTraceEnabled()) {
246 LOG.trace("getKey [" + id + "]");
247 }
248 return id;
249 }
250 });
251 return new LinkedHashSet<String>(keys);
252 }
253 });
254 }
255
256 @SuppressWarnings("unchecked")
257 public Set<String> scan(CamelContext camelContext) {
258 return (LinkedHashSet<String>) transactionTemplateReadOnly.execute(new TransactionCallback() {
259 public LinkedHashSet<String> doInTransaction(TransactionStatus status) {
260 List<String> keys = jdbcTemplate.query("SELECT " + ID + " FROM " + getRepositoryNameCompleted(),
261 new RowMapper<String>() {
262 public String mapRow(ResultSet rs, int rowNum) throws SQLException {
263 String id = rs.getString(ID);
264 if (LOG.isTraceEnabled()) {
265 LOG.trace("getKey [" + id + "]");
266 }
267 return id;
268 }
269 });
270 return new LinkedHashSet<String>(keys);
271 }
272 });
273 }
274
275 public Exchange recover(CamelContext camelContext, String exchangeId) {
276 final String key = exchangeId;
277 Exchange answer = get(key, getRepositoryNameCompleted(), camelContext);
278
279 if (LOG.isDebugEnabled()) {
280 LOG.debug("Recovering exchangeId [" + key + "] -> " + answer);
281 }
282
283 return answer;
284 }
285
286 public void setRecoveryInterval(long interval, TimeUnit timeUnit) {
287 this.recoveryInterval = timeUnit.toMillis(interval);
288 }
289
290 public void setRecoveryInterval(long interval) {
291 this.recoveryInterval = interval;
292 }
293
294 public long getRecoveryIntervalInMillis() {
295 return recoveryInterval;
296 }
297
298 public boolean isUseRecovery() {
299 return useRecovery;
300 }
301
302 public void setUseRecovery(boolean useRecovery) {
303 this.useRecovery = useRecovery;
304 }
305
306 public int getMaximumRedeliveries() {
307 return maximumRedeliveries;
308 }
309
310 public void setMaximumRedeliveries(int maximumRedeliveries) {
311 this.maximumRedeliveries = maximumRedeliveries;
312 }
313
314 public String getDeadLetterUri() {
315 return deadLetterUri;
316 }
317
318 public void setDeadLetterUri(String deadLetterUri) {
319 this.deadLetterUri = deadLetterUri;
320 }
321
322 public boolean isReturnOldExchange() {
323 return returnOldExchange;
324 }
325
326 public void setReturnOldExchange(boolean returnOldExchange) {
327 this.returnOldExchange = returnOldExchange;
328 }
329
330 /**
331 * @return the lobHandler
332 */
333 public LobHandler getLobHandler() {
334 return lobHandler;
335 }
336
337 /**
338 * @param lobHandler the lobHandler to set
339 */
340 public void setLobHandler(LobHandler lobHandler) {
341 this.lobHandler = lobHandler;
342 }
343
344 public String getRepositoryName() {
345 return repositoryName;
346 }
347
348 public String getRepositoryNameCompleted() {
349 return getRepositoryName() + "_completed";
350 }
351
352 @Override
353 protected void doStart() throws Exception {
354 ObjectHelper.notNull(repositoryName, "RepositoryName");
355 ObjectHelper.notNull(transactionManager, "TransactionManager");
356 ObjectHelper.notNull(dataSource, "DataSource");
357
358 // log number of existing exchanges
359 int current = getKeys().size();
360 int completed = scan(null).size();
361
362 if (current > 0) {
363 LOG.info("On startup there are " + current + " aggregate exchanges (not completed) in repository: " + getRepositoryName());
364 } else {
365 LOG.info("On startup there are no existing aggregate exchanges (not completed) in repository: " + getRepositoryName());
366 }
367 if (completed > 0) {
368 LOG.warn("On startup there are " + completed + " completed exchanges to be recovered in repository: " + getRepositoryNameCompleted());
369 } else {
370 LOG.info("On startup there are no completed exchanges to be recovered in repository: " + getRepositoryNameCompleted());
371 }
372 }
373
374 @Override
375 protected void doStop() throws Exception {
376 // noop
377 }
378
379 }