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 public Exchange add(final CamelContext camelContext, final String correlationId, final Exchange exchange) {
113 return transactionTemplate.execute(new TransactionCallback<Exchange>() {
114
115 public Exchange doInTransaction(TransactionStatus status) {
116 String sql;
117 Exchange result = null;
118 final String key = correlationId;
119
120 try {
121 final byte[] data = codec.marshallExchange(camelContext, exchange);
122
123 LOG.debug("Adding exchange with key: [{}]", key);
124
125 String insert = "INSERT INTO " + getRepositoryName() + " (" + EXCHANGE + ", " + ID + ") VALUES (?, ?)";
126 String update = "UPDATE " + getRepositoryName() + " SET " + EXCHANGE + " = ? WHERE " + ID + " = ?";
127
128 boolean present = jdbcTemplate.queryForInt(
129 "SELECT COUNT(*) FROM " + getRepositoryName() + " WHERE " + ID + " = ?", key) != 0;
130 sql = present ? update : insert;
131
132 // Recover existing exchange with that ID
133 if (isReturnOldExchange() && present) {
134 result = get(key, getRepositoryName(), camelContext);
135 }
136
137 jdbcTemplate.execute(sql,
138 new AbstractLobCreatingPreparedStatementCallback(getLobHandler()) {
139 @Override
140 protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
141 lobCreator.setBlobAsBytes(ps, 1, data);
142 ps.setString(2, key);
143 }
144 });
145
146 } catch (IOException e) {
147 throw new RuntimeException("Error adding to repository " + repositoryName + " with key " + key, e);
148 }
149
150 return result;
151 }
152 });
153
154 }
155
156 public Exchange get(final CamelContext camelContext, final String correlationId) {
157 final String key = correlationId;
158 Exchange result = get(key, getRepositoryName(), camelContext);
159
160 LOG.debug("Getting key [{}] -> {}", key, result);
161
162 return result;
163 }
164
165 private Exchange get(final String key, final String repositoryName, final CamelContext camelContext) {
166 return transactionTemplateReadOnly.execute(new TransactionCallback<Exchange>() {
167 public Exchange doInTransaction(TransactionStatus status) {
168 try {
169 final byte[] data = jdbcTemplate.queryForObject(
170 "SELECT " + EXCHANGE + " FROM " + repositoryName + " WHERE " + ID + " = ?",
171 new Object[]{key}, byte[].class);
172 return codec.unmarshallExchange(camelContext, data);
173 } catch (EmptyResultDataAccessException ex) {
174 return null;
175 } catch (IOException ex) {
176 // Rollback the transaction
177 throw new RuntimeException("Error getting key " + key + " from repository " + repositoryName, ex);
178 } catch (ClassNotFoundException ex) {
179 // Rollback the transaction
180 throw new RuntimeException(ex);
181 }
182 }
183 });
184 }
185
186 public void remove(final CamelContext camelContext, final String correlationId, final Exchange exchange) {
187 transactionTemplate.execute(new TransactionCallbackWithoutResult() {
188 protected void doInTransactionWithoutResult(TransactionStatus status) {
189 final String key = correlationId;
190 final String confirmKey = exchange.getExchangeId();
191 try {
192 final byte[] data = codec.marshallExchange(camelContext, exchange);
193
194 LOG.debug("Removing key [{}]", key);
195
196 jdbcTemplate.update("DELETE FROM " + getRepositoryName() + " WHERE " + ID + " = ?",
197 new Object[]{key});
198
199 jdbcTemplate.execute("INSERT INTO " + getRepositoryNameCompleted() + " (" + EXCHANGE + ", " + ID + ") VALUES (?, ?)",
200 new AbstractLobCreatingPreparedStatementCallback(getLobHandler()) {
201 @Override
202 protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
203 lobCreator.setBlobAsBytes(ps, 1, data);
204 ps.setString(2, confirmKey);
205 }
206 });
207 } catch (IOException e) {
208 throw new RuntimeException("Error removing key " + key + " from repository " + repositoryName, e);
209 }
210 }
211 });
212 }
213
214 public void confirm(final CamelContext camelContext, final String exchangeId) {
215 transactionTemplate.execute(new TransactionCallbackWithoutResult() {
216 protected void doInTransactionWithoutResult(TransactionStatus status) {
217 LOG.debug("Confirming exchangeId [{}]", exchangeId);
218 final String confirmKey = exchangeId;
219
220 jdbcTemplate.update("DELETE FROM " + getRepositoryNameCompleted() + " WHERE " + ID + " = ?",
221 new Object[]{confirmKey});
222
223 }
224 });
225 }
226
227 public Set<String> getKeys() {
228 return transactionTemplateReadOnly.execute(new TransactionCallback<LinkedHashSet<String>>() {
229 public LinkedHashSet<String> doInTransaction(TransactionStatus status) {
230 List<String> keys = jdbcTemplate.query("SELECT " + ID + " FROM " + getRepositoryName(),
231 new RowMapper<String>() {
232 public String mapRow(ResultSet rs, int rowNum) throws SQLException {
233 String id = rs.getString(ID);
234 LOG.trace("getKey [{}]", id);
235 return id;
236 }
237 });
238 return new LinkedHashSet<String>(keys);
239 }
240 });
241 }
242
243 public Set<String> scan(CamelContext camelContext) {
244 return transactionTemplateReadOnly.execute(new TransactionCallback<LinkedHashSet<String>>() {
245 public LinkedHashSet<String> doInTransaction(TransactionStatus status) {
246 List<String> keys = jdbcTemplate.query("SELECT " + ID + " FROM " + getRepositoryNameCompleted(),
247 new RowMapper<String>() {
248 public String mapRow(ResultSet rs, int rowNum) throws SQLException {
249 String id = rs.getString(ID);
250 LOG.trace("getKey [{}]", id);
251 return id;
252 }
253 });
254 return new LinkedHashSet<String>(keys);
255 }
256 });
257 }
258
259 public Exchange recover(CamelContext camelContext, String exchangeId) {
260 final String key = exchangeId;
261 Exchange answer = get(key, getRepositoryNameCompleted(), camelContext);
262
263 LOG.debug("Recovering exchangeId [{}] -> {}", key, answer);
264
265 return answer;
266 }
267
268 public void setRecoveryInterval(long interval, TimeUnit timeUnit) {
269 this.recoveryInterval = timeUnit.toMillis(interval);
270 }
271
272 public void setRecoveryInterval(long interval) {
273 this.recoveryInterval = interval;
274 }
275
276 public long getRecoveryIntervalInMillis() {
277 return recoveryInterval;
278 }
279
280 public boolean isUseRecovery() {
281 return useRecovery;
282 }
283
284 public void setUseRecovery(boolean useRecovery) {
285 this.useRecovery = useRecovery;
286 }
287
288 public int getMaximumRedeliveries() {
289 return maximumRedeliveries;
290 }
291
292 public void setMaximumRedeliveries(int maximumRedeliveries) {
293 this.maximumRedeliveries = maximumRedeliveries;
294 }
295
296 public String getDeadLetterUri() {
297 return deadLetterUri;
298 }
299
300 public void setDeadLetterUri(String deadLetterUri) {
301 this.deadLetterUri = deadLetterUri;
302 }
303
304 public boolean isReturnOldExchange() {
305 return returnOldExchange;
306 }
307
308 public void setReturnOldExchange(boolean returnOldExchange) {
309 this.returnOldExchange = returnOldExchange;
310 }
311
312 /**
313 * @return the lobHandler
314 */
315 public LobHandler getLobHandler() {
316 return lobHandler;
317 }
318
319 /**
320 * @param lobHandler the lobHandler to set
321 */
322 public void setLobHandler(LobHandler lobHandler) {
323 this.lobHandler = lobHandler;
324 }
325
326 public String getRepositoryName() {
327 return repositoryName;
328 }
329
330 public String getRepositoryNameCompleted() {
331 return getRepositoryName() + "_completed";
332 }
333
334 @Override
335 protected void doStart() throws Exception {
336 ObjectHelper.notNull(repositoryName, "RepositoryName");
337 ObjectHelper.notNull(transactionManager, "TransactionManager");
338 ObjectHelper.notNull(dataSource, "DataSource");
339
340 // log number of existing exchanges
341 int current = getKeys().size();
342 int completed = scan(null).size();
343
344 if (current > 0) {
345 LOG.info("On startup there are " + current + " aggregate exchanges (not completed) in repository: " + getRepositoryName());
346 } else {
347 LOG.info("On startup there are no existing aggregate exchanges (not completed) in repository: " + getRepositoryName());
348 }
349 if (completed > 0) {
350 LOG.warn("On startup there are " + completed + " completed exchanges to be recovered in repository: " + getRepositoryNameCompleted());
351 } else {
352 LOG.info("On startup there are no completed exchanges to be recovered in repository: " + getRepositoryNameCompleted());
353 }
354 }
355
356 @Override
357 protected void doStop() throws Exception {
358 // noop
359 }
360
361 }