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