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    }