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    }