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.idempotent;
018
019 import java.io.File;
020 import java.io.FileOutputStream;
021 import java.io.IOException;
022 import java.util.Map;
023 import java.util.Scanner;
024 import java.util.concurrent.atomic.AtomicBoolean;
025
026 import org.apache.camel.api.management.ManagedAttribute;
027 import org.apache.camel.api.management.ManagedOperation;
028 import org.apache.camel.api.management.ManagedResource;
029 import org.apache.camel.spi.IdempotentRepository;
030 import org.apache.camel.support.ServiceSupport;
031 import org.apache.camel.util.IOHelper;
032 import org.apache.camel.util.LRUCache;
033 import org.apache.camel.util.ObjectHelper;
034 import org.slf4j.Logger;
035 import org.slf4j.LoggerFactory;
036
037 /**
038 * A file based implementation of {@link org.apache.camel.spi.IdempotentRepository}.
039 * <p/>
040 * Care should be taken to use a suitable underlying {@link java.util.Map} to avoid this class being a
041 * memory leak.
042 *
043 * @version
044 */
045 @ManagedResource(description = "File based idempotent repository")
046 public class FileIdempotentRepository extends ServiceSupport implements IdempotentRepository<String> {
047 private static final transient Logger LOG = LoggerFactory.getLogger(FileIdempotentRepository.class);
048 private static final String STORE_DELIMITER = "\n";
049 private Map<String, Object> cache;
050 private File fileStore;
051 private long maxFileStoreSize = 1024 * 1000L; // 1mb store file
052 private AtomicBoolean init = new AtomicBoolean();
053
054 public FileIdempotentRepository() {
055 // default use a 1st level cache
056 this.cache = new LRUCache<String, Object>(1000);
057 }
058
059 public FileIdempotentRepository(File fileStore, Map<String, Object> set) {
060 this.fileStore = fileStore;
061 this.cache = set;
062 }
063
064 /**
065 * Creates a new file based repository using a {@link org.apache.camel.util.LRUCache}
066 * as 1st level cache with a default of 1000 entries in the cache.
067 *
068 * @param fileStore the file store
069 */
070 public static IdempotentRepository<String> fileIdempotentRepository(File fileStore) {
071 return fileIdempotentRepository(fileStore, 1000);
072 }
073
074 /**
075 * Creates a new file based repository using a {@link org.apache.camel.util.LRUCache}
076 * as 1st level cache.
077 *
078 * @param fileStore the file store
079 * @param cacheSize the cache size
080 */
081 public static IdempotentRepository<String> fileIdempotentRepository(File fileStore, int cacheSize) {
082 return fileIdempotentRepository(fileStore, new LRUCache<String, Object>(cacheSize));
083 }
084
085 /**
086 * Creates a new file based repository using a {@link org.apache.camel.util.LRUCache}
087 * as 1st level cache.
088 *
089 * @param fileStore the file store
090 * @param cacheSize the cache size
091 * @param maxFileStoreSize the max size in bytes for the filestore file
092 */
093 public static IdempotentRepository<String> fileIdempotentRepository(File fileStore, int cacheSize, long maxFileStoreSize) {
094 FileIdempotentRepository repository = new FileIdempotentRepository(fileStore, new LRUCache<String, Object>(cacheSize));
095 repository.setMaxFileStoreSize(maxFileStoreSize);
096 return repository;
097 }
098
099 /**
100 * Creates a new file based repository using the given {@link java.util.Map}
101 * as 1st level cache.
102 * <p/>
103 * Care should be taken to use a suitable underlying {@link java.util.Map} to avoid this class being a
104 * memory leak.
105 *
106 * @param store the file store
107 * @param cache the cache to use as 1st level cache
108 */
109 public static IdempotentRepository<String> fileIdempotentRepository(File store, Map<String, Object> cache) {
110 return new FileIdempotentRepository(store, cache);
111 }
112
113 @ManagedOperation(description = "Adds the key to the store")
114 public boolean add(String key) {
115 synchronized (cache) {
116 if (cache.containsKey(key)) {
117 return false;
118 } else {
119 cache.put(key, key);
120 if (fileStore.length() < maxFileStoreSize) {
121 // just append to store
122 appendToStore(key);
123 } else {
124 // trunk store and flush the cache
125 trunkStore();
126 }
127
128 return true;
129 }
130 }
131 }
132
133 @ManagedOperation(description = "Does the store contain the given key")
134 public boolean contains(String key) {
135 synchronized (cache) {
136 return cache.containsKey(key);
137 }
138 }
139
140 @ManagedOperation(description = "Remove the key from the store")
141 public boolean remove(String key) {
142 boolean answer;
143 synchronized (cache) {
144 answer = cache.remove(key) != null;
145 // trunk store and flush the cache on remove
146 trunkStore();
147 }
148 return answer;
149 }
150
151 public boolean confirm(String key) {
152 // noop
153 return true;
154 }
155
156 public File getFileStore() {
157 return fileStore;
158 }
159
160 public void setFileStore(File fileStore) {
161 this.fileStore = fileStore;
162 }
163
164 @ManagedAttribute(description = "The file path for the store")
165 public String getFilePath() {
166 return fileStore.getPath();
167 }
168
169 public Map<String, Object> getCache() {
170 return cache;
171 }
172
173 public void setCache(Map<String, Object> cache) {
174 this.cache = cache;
175 }
176
177 @ManagedAttribute(description = "The maximum file size for the file store in bytes")
178 public long getMaxFileStoreSize() {
179 return maxFileStoreSize;
180 }
181
182 /**
183 * Sets the maximum file size for the file store in bytes.
184 * <p/>
185 * The default is 1mb.
186 */
187 @ManagedAttribute(description = "The maximum file size for the file store in bytes")
188 public void setMaxFileStoreSize(long maxFileStoreSize) {
189 this.maxFileStoreSize = maxFileStoreSize;
190 }
191
192 /**
193 * Sets the cache size
194 */
195 public void setCacheSize(int size) {
196 if (cache != null) {
197 cache.clear();
198 }
199 cache = new LRUCache<String, Object>(size);
200 }
201
202 @ManagedAttribute(description = "The current cache size")
203 public int getCacheSize() {
204 if (cache != null) {
205 return cache.size();
206 }
207 return 0;
208 }
209
210 /**
211 * Reset and clears the store to force it to reload from file
212 */
213 @ManagedOperation(description = "Reset and reloads the file store")
214 public synchronized void reset() {
215 synchronized (cache) {
216 // trunk and clear, before we reload the store
217 trunkStore();
218 cache.clear();
219 loadStore();
220 }
221 }
222
223 /**
224 * Appends the given message id to the file store
225 *
226 * @param messageId the message id
227 */
228 protected void appendToStore(final String messageId) {
229 LOG.debug("Appending {} to idempotent filestore: {}", messageId, fileStore);
230 FileOutputStream fos = null;
231 try {
232 // create store if missing
233 if (!fileStore.exists()) {
234 fileStore.createNewFile();
235 }
236 // append to store
237 fos = new FileOutputStream(fileStore, true);
238 fos.write(messageId.getBytes());
239 fos.write(STORE_DELIMITER.getBytes());
240 } catch (IOException e) {
241 throw ObjectHelper.wrapRuntimeCamelException(e);
242 } finally {
243 IOHelper.close(fos, "Appending to file idempotent repository", LOG);
244 }
245 }
246
247 /**
248 * Trunks the file store when the max store size is hit by rewriting the 1st level cache
249 * to the file store.
250 */
251 protected void trunkStore() {
252 LOG.info("Trunking idempotent filestore: {}", fileStore);
253 FileOutputStream fos = null;
254 try {
255 fos = new FileOutputStream(fileStore);
256 for (String key : cache.keySet()) {
257 fos.write(key.getBytes());
258 fos.write(STORE_DELIMITER.getBytes());
259 }
260 } catch (IOException e) {
261 throw ObjectHelper.wrapRuntimeCamelException(e);
262 } finally {
263 IOHelper.close(fos, "Trunking file idempotent repository", LOG);
264 }
265 }
266
267 /**
268 * Loads the given file store into the 1st level cache
269 */
270 protected void loadStore() {
271 LOG.trace("Loading to 1st level cache from idempotent filestore: {}", fileStore);
272
273 if (!fileStore.exists()) {
274 return;
275 }
276
277 cache.clear();
278 Scanner scanner = null;
279 try {
280 scanner = new Scanner(fileStore);
281 scanner.useDelimiter(STORE_DELIMITER);
282 while (scanner.hasNextLine()) {
283 String line = scanner.nextLine();
284 cache.put(line, line);
285 }
286 } catch (IOException e) {
287 throw ObjectHelper.wrapRuntimeCamelException(e);
288 } finally {
289 if (scanner != null) {
290 scanner.close();
291 }
292 }
293
294 LOG.debug("Loaded {} to the 1st level cache from idempotent filestore: {}", cache.size(), fileStore);
295 }
296
297 @Override
298 protected void doStart() throws Exception {
299 // init store if not loaded before
300 if (init.compareAndSet(false, true)) {
301 loadStore();
302 }
303 }
304
305 @Override
306 protected void doStop() throws Exception {
307 // reset will trunk and clear the cache
308 trunkStore();
309 cache.clear();
310 init.set(false);
311 }
312
313 }