001/**************************************************************** 002 * Licensed to the Apache Software Foundation (ASF) under one * 003 * or more contributor license agreements. See the NOTICE file * 004 * distributed with this work for additional information * 005 * regarding copyright ownership. The ASF licenses this file * 006 * to you under the Apache License, Version 2.0 (the * 007 * "License"); you may not use this file except in compliance * 008 * with the License. You may obtain a copy of the License at * 009 * * 010 * http://www.apache.org/licenses/LICENSE-2.0 * 011 * * 012 * Unless required by applicable law or agreed to in writing, * 013 * software distributed under the License is distributed on an * 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * 015 * KIND, either express or implied. See the License for the * 016 * specific language governing permissions and limitations * 017 * under the License. * 018 ****************************************************************/ 019 020package org.apache.james.repository.file; 021 022import java.io.File; 023import java.io.FileInputStream; 024import java.io.FileNotFoundException; 025import java.io.FileOutputStream; 026import java.io.FilenameFilter; 027import java.io.IOException; 028import java.io.InputStream; 029import java.io.OutputStream; 030import java.util.ArrayList; 031import java.util.Iterator; 032import java.util.List; 033 034import javax.annotation.PostConstruct; 035import javax.inject.Inject; 036 037import org.apache.commons.configuration.ConfigurationException; 038import org.apache.commons.configuration.HierarchicalConfiguration; 039import org.apache.commons.io.FileUtils; 040import org.apache.james.filesystem.api.FileSystem; 041import org.apache.james.lifecycle.api.Configurable; 042import org.apache.james.lifecycle.api.LogEnabled; 043import org.apache.james.repository.api.Repository; 044import org.slf4j.Logger; 045 046/** 047 * This an abstract class implementing functionality for creating a file-store. 048 */ 049public abstract class AbstractFileRepository implements Repository, Configurable, LogEnabled { 050 051 protected static final boolean DEBUG = false; 052 053 protected static final int BYTE_MASK = 0x0f; 054 055 protected static final char[] HEX_DIGITS = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; 056 057 protected String m_extension; 058 059 protected String m_name; 060 061 protected FilenameFilter m_filter; 062 063 protected File m_baseDirectory; 064 065 private FileSystem fileSystem; 066 067 private Logger logger; 068 069 private String destination; 070 071 public void configure(HierarchicalConfiguration configuration) throws ConfigurationException { 072 destination = configuration.getString("[@destinationURL]"); 073 } 074 075 @Inject 076 public void setFileSystem(FileSystem fileSystem) { 077 this.fileSystem = fileSystem; 078 } 079 080 public void setLog(Logger logger) { 081 this.logger = logger; 082 } 083 084 protected Logger getLogger() { 085 return logger; 086 } 087 088 protected abstract String getExtensionDecorator(); 089 090 @PostConstruct 091 public void init() throws Exception { 092 093 getLogger().info("Init " + getClass().getName() + " Store"); 094 setDestination(destination); 095 096 File directory; 097 098 try { 099 directory = m_baseDirectory.getCanonicalFile(); 100 } catch (IOException ioe) { 101 throw new ConfigurationException("Unable to form canonical representation of " + m_baseDirectory); 102 } 103 104 m_name = "Repository"; 105 String m_postfix = getExtensionDecorator(); 106 m_extension = "." + m_name + m_postfix; 107 m_filter = new ExtensionFileFilter(m_extension); 108 // m_filter = new NumberedRepositoryFileFilter(getExtensionDecorator()); 109 110 FileUtils.forceMkdir(directory); 111 112 getLogger().info(getClass().getName() + " opened in " + m_baseDirectory); 113 114 // We will look for all numbered repository files in this 115 // directory and rename them to non-numbered repositories, 116 // logging all the way. 117 118 FilenameFilter num_filter = new NumberedRepositoryFileFilter(getExtensionDecorator()); 119 final String[] names = directory.list(num_filter); 120 121 try { 122 for (String origFilename : names) { 123 // This needs to handle (skip over) the possible repository 124 // numbers 125 int pos = origFilename.length() - m_postfix.length(); 126 while (pos >= 1 && Character.isDigit(origFilename.charAt(pos - 1))) { 127 pos--; 128 } 129 pos -= ".".length() + m_name.length(); 130 String newFilename = origFilename.substring(0, pos) + m_extension; 131 132 File origFile = new File(directory, origFilename); 133 File newFile = new File(directory, newFilename); 134 135 if (origFile.renameTo(newFile)) { 136 getLogger().info("Renamed " + origFile + " to " + newFile); 137 } else { 138 getLogger().info("Unable to rename " + origFile + " to " + newFile); 139 } 140 } 141 } catch (Exception e) { 142 e.printStackTrace(); 143 throw e; 144 } 145 146 } 147 148 /** 149 * Set the destination for the repository 150 * 151 * @param destination 152 * the destination under which the repository get stored 153 * @throws ConfigurationException 154 * @throws ConfigurationException 155 * get thrown on invalid destintion syntax 156 */ 157 protected void setDestination(String destination) throws ConfigurationException { 158 159 if (!destination.startsWith(FileSystem.FILE_PROTOCOL)) { 160 throw new ConfigurationException("cannot handle destination " + destination); 161 } 162 163 try { 164 m_baseDirectory = fileSystem.getFile(destination); 165 } catch (FileNotFoundException e) { 166 throw new ConfigurationException("Unable to acces destination " + destination, e); 167 } 168 169 } 170 171 /** 172 * Return a new instance of this class 173 * 174 * @return class a new instance of AbstractFileRepository 175 * @throws Exception 176 * get thrown if an error is detected while create the new 177 * instance 178 */ 179 protected AbstractFileRepository createChildRepository() throws Exception { 180 return getClass().newInstance(); 181 } 182 183 @Override 184 public Repository getChildRepository(String childName) { 185 AbstractFileRepository child; 186 187 try { 188 child = createChildRepository(); 189 } catch (Exception e) { 190 throw new RuntimeException("Cannot create child repository " + childName + " : " + e); 191 } 192 193 child.setFileSystem(fileSystem); 194 child.setLog(logger); 195 196 try { 197 child.setDestination(m_baseDirectory.getAbsolutePath() + File.pathSeparatorChar + childName + File.pathSeparator); 198 } catch (ConfigurationException ce) { 199 throw new RuntimeException("Cannot set destination for child child " + "repository " + childName + " : " + ce); 200 } 201 202 try { 203 child.init(); 204 } catch (Exception e) { 205 throw new RuntimeException("Cannot initialize child " + "repository " + childName + " : " + e); 206 } 207 208 if (DEBUG) { 209 getLogger().debug("Child repository of " + m_name + " created in " + m_baseDirectory + File.pathSeparatorChar + childName + File.pathSeparator); 210 } 211 212 return child; 213 } 214 215 /** 216 * Return the File Object which belongs to the given key 217 * 218 * @param key 219 * the key for which the File get returned 220 * @return file the File associted with the given Key 221 * @throws IOException 222 * get thrown on IO error 223 */ 224 protected File getFile(String key) throws IOException { 225 return new File(encode(key)); 226 } 227 228 /** 229 * Return the InputStream which belongs to the given key 230 * 231 * @param key 232 * the key for which the InputStream get returned 233 * @return in the InputStram associted with the given key 234 * @throws IOException 235 * get thrown on IO error 236 */ 237 protected InputStream getInputStream(String key) throws IOException { 238 // This was changed to SharedFileInputStream but reverted to 239 // fix JAMES-559. Usign SharedFileInputStream should be a good 240 // performance improvement, but more checks have to be done 241 // on the repository side to avoid concurrency in reading and 242 // writing the same file. 243 return new FileInputStream(encode(key)); 244 } 245 246 /** 247 * Return the OutputStream which belongs to the given key 248 * 249 * @param key 250 * the key for which the OutputStream get returned 251 * @return out the OutputStream 252 * @throws IOException 253 * get thrown on IO error 254 */ 255 protected OutputStream getOutputStream(String key) throws IOException { 256 return new FileOutputStream(getFile(key)); 257 } 258 259 /** 260 * Remove the object associated to the given key. 261 * 262 * @param key 263 * the key to remove 264 * @throws IOException 265 */ 266 267 public synchronized boolean remove(String key) { 268 try { 269 FileUtils.forceDelete(getFile(key)); 270 return true; 271 } catch (FileNotFoundException e) { 272 return false; 273 } catch (Exception e) { 274 throw new RuntimeException("Exception caught while removing" + " an object: " + e); 275 } 276 } 277 /** 278 * 279 * Indicates if the given key is associated to a contained object 280 * 281 * @param key 282 * the key which checked for 283 * @return true if the repository contains the key 284 */ 285 public synchronized boolean containsKey(String key) { 286 try { 287 final File file = getFile(key); 288 if (DEBUG) 289 getLogger().debug("checking key " + key); 290 return file.exists(); 291 } catch (Exception e) { 292 throw new RuntimeException("Exception caught while searching " + "an object: " + e); 293 } 294 } 295 296 /** 297 * Returns the list of used keys. 298 */ 299 public Iterator<String> list() { 300 final File storeDir = new File(m_baseDirectory.getAbsolutePath()); 301 final String[] names = storeDir.list(m_filter); 302 final List<String> list = new ArrayList<String>(); 303 304 for (String name : names) { 305 String decoded = decode(name); 306 list.add(decoded); 307 } 308 309 return list.iterator(); 310 } 311 312 /** 313 * Returns a String that uniquely identifies the object. <b>Note:</b> since 314 * this method uses the Object.toString() method, it's up to the caller to 315 * make sure that this method doesn't change between different JVM 316 * executions (like it may normally happen). For this reason, it's highly 317 * recommended (even if not mandated) that Strings be used as keys. 318 * 319 * @param key 320 * the key for which the Object should be searched 321 * @return result a unique String represent the Object which belongs to the 322 * key 323 */ 324 protected String encode(String key) { 325 final byte[] bytes = key.getBytes(); 326 final char[] buffer = new char[bytes.length << 1]; 327 328 for (int i = 0, j = 0; i < bytes.length; i++) { 329 final int k = bytes[i]; 330 buffer[j++] = HEX_DIGITS[(k >>> 4) & BYTE_MASK]; 331 buffer[j++] = HEX_DIGITS[k & BYTE_MASK]; 332 } 333 334 StringBuilder result = new StringBuilder(); 335 result.append(m_baseDirectory.getAbsolutePath()); 336 result.append(File.separator); 337 result.append(buffer); 338 result.append(m_extension); 339 return result.toString(); 340 } 341 342 /** 343 * Inverse of encode exept it do not use path. So decode(encode(s) - m_path) 344 * = s. In other words it returns a String that can be used as key to 345 * retrieve the record contained in the 'filename' file. 346 * 347 * @param filename 348 * the filename for which the key should generated 349 * @return key a String which can be used to retrieve the filename 350 */ 351 protected String decode(String filename) { 352 filename = filename.substring(0, filename.length() - m_extension.length()); 353 final int size = filename.length(); 354 final byte[] bytes = new byte[size >>> 1]; 355 356 for (int i = 0, j = 0; i < size; j++) { 357 bytes[j] = Byte.parseByte(filename.substring(i, i + 2), 16); 358 i += 2; 359 } 360 361 return new String(bytes); 362 } 363}