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}