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    }