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