View Javadoc

1   /****************************************************************
2    * Licensed to the Apache Software Foundation (ASF) under one   *
3    * or more contributor license agreements.  See the NOTICE file *
4    * distributed with this work for additional information        *
5    * regarding copyright ownership.  The ASF licenses this file   *
6    * to you under the Apache License, Version 2.0 (the            *
7    * "License"); you may not use this file except in compliance   *
8    * with the License.  You may obtain a copy of the License at   *
9    *                                                              *
10   *   http://www.apache.org/licenses/LICENSE-2.0                 *
11   *                                                              *
12   * Unless required by applicable law or agreed to in writing,   *
13   * software distributed under the License is distributed on an  *
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
15   * KIND, either express or implied.  See the License for the    *
16   * specific language governing permissions and limitations      *
17   * under the License.                                           *
18   ****************************************************************/
19  
20  
21  
22  package org.apache.james.mailrepository.filepair;
23  
24  import org.apache.avalon.cornerstone.services.store.Repository;
25  import org.apache.avalon.framework.activity.Initializable;
26  import org.apache.avalon.framework.configuration.Configurable;
27  import org.apache.avalon.framework.configuration.Configuration;
28  import org.apache.avalon.framework.configuration.ConfigurationException;
29  import org.apache.avalon.framework.logger.AbstractLogEnabled;
30  import org.apache.avalon.framework.service.ServiceException;
31  import org.apache.avalon.framework.service.ServiceManager;
32  import org.apache.avalon.framework.service.Serviceable;
33  import org.apache.james.services.FileSystem;
34  import org.apache.james.util.io.ExtensionFileFilter;
35  
36  import java.io.File;
37  import java.io.FileInputStream;
38  import java.io.FileNotFoundException;
39  import java.io.FileOutputStream;
40  import java.io.FilenameFilter;
41  import java.io.IOException;
42  import java.io.InputStream;
43  import java.io.OutputStream;
44  import java.util.ArrayList;
45  import java.util.Iterator;
46  
47  /**
48   * This an abstract class implementing functionality for creating a file-store.
49   *
50   */
51  public abstract class AbstractFileRepository
52      extends AbstractLogEnabled
53      implements Repository, Serviceable, Configurable, Initializable
54  {
55      protected static final boolean DEBUG = false;
56  
57      protected static final String HANDLED_URL = "file://";
58      protected static final int BYTE_MASK = 0x0f;
59      protected static final char[] HEX_DIGITS = new char[]
60      {
61          '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
62      };
63  
64      protected String m_path;
65      protected String m_destination;
66      protected String m_extension;
67      protected String m_name;
68      protected FilenameFilter m_filter;
69      protected File m_baseDirectory;
70  
71      protected ServiceManager m_serviceManager;
72  
73      protected abstract String getExtensionDecorator();
74  
75      /**
76       * @see org.apache.avalon.framework.service.Serviceable#service(ServiceManager)
77       */
78      public void service( final ServiceManager serviceManager )
79          throws ServiceException
80      {
81          m_serviceManager = serviceManager;
82          try {
83              m_baseDirectory = ((FileSystem) serviceManager.lookup(FileSystem.ROLE)).getBasedir();
84          } catch (FileNotFoundException e) {
85              throw new ServiceException(FileSystem.ROLE, "Cannot find the base directory of the application", e);
86          }
87      }
88  
89      /**
90       * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
91       */
92      public void configure( final Configuration configuration )
93          throws ConfigurationException
94      {
95          if( null == m_destination )
96          {
97              final String destination = configuration.getAttribute( "destinationURL" );
98              setDestination( destination );
99          }
100     }
101 
102     /**
103      * @see org.apache.avalon.framework.activity.Initializable#initialize()
104      */
105     public void initialize()
106         throws Exception
107     {
108         getLogger().info( "Init " + getClass().getName() + " Store" );
109 
110         m_name = "Repository";
111         String m_postfix = getExtensionDecorator();
112         m_extension = "." + m_name + m_postfix;
113         m_filter = new ExtensionFileFilter(m_extension);
114         //m_filter = new NumberedRepositoryFileFilter(getExtensionDecorator());
115 
116         final File directory = new File( m_path );
117         directory.mkdirs();
118 
119         getLogger().info( getClass().getName() + " opened in " + m_path );
120 
121         //We will look for all numbered repository files in this
122         //  directory and rename them to non-numbered repositories,
123         //  logging all the way.
124 
125         FilenameFilter num_filter = new NumberedRepositoryFileFilter(getExtensionDecorator());
126         final String[] names = directory.list( num_filter );
127 
128         try {
129             for( int i = 0; i < names.length; i++ ) {
130                 String origFilename = names[i];
131 
132                 //This needs to handle (skip over) the possible repository numbers
133                 int pos = origFilename.length() - m_postfix.length();
134                 while (pos >= 1 && Character.isDigit(origFilename.charAt(pos - 1))) {
135                     pos--;
136                 }
137                 pos -= ".".length() + m_name.length();
138                 String newFilename = origFilename.substring(0, pos) + m_extension;
139 
140                 File origFile = new File(directory, origFilename);
141                 File newFile = new File(directory, newFilename);
142 
143                 if (origFile.renameTo(newFile)) {
144                     getLogger().info("Renamed " + origFile + " to " + newFile);
145                 } else {
146                     getLogger().info("Unable to rename " + origFile + " to " + newFile);
147                 }
148             }
149         } catch (Exception e) {
150             e.printStackTrace();
151             throw e;
152         }
153 
154     }
155 
156     /**
157      * Set the destination for the repository
158      * 
159      * @param destination the destination under which the repository get stored
160      * @throws ConfigurationException get thrown on invalid destintion syntax
161      */
162     protected void setDestination( final String destination )
163         throws ConfigurationException
164     {
165         if( !destination.startsWith( HANDLED_URL ) )
166         {
167             throw new ConfigurationException( "cannot handle destination " + destination );
168         }
169 
170         m_path = destination.substring( HANDLED_URL.length() );
171 
172         File directory;
173 
174         // Check for absolute path
175         if( m_path.startsWith( "/" ) )
176         {
177             directory = new File( m_path );
178         }
179         else
180         {
181             directory = new File( m_baseDirectory, m_path );
182         }
183 
184         try
185         {
186             directory = directory.getCanonicalFile();
187         }
188         catch( final IOException ioe )
189         {
190             throw new ConfigurationException( "Unable to form canonical representation of " +
191                                               directory );
192         }
193 
194         m_path = directory.toString();
195 
196         m_destination = destination;
197     }
198 
199     /**
200      * Return a new instance of this class
201      * 
202      * @return class a new instance of AbstractFileRepository
203      * @throws Exception get thrown if an error is detected while create the new instance
204      */
205     protected AbstractFileRepository createChildRepository()
206         throws Exception
207     {
208         return (AbstractFileRepository) getClass().newInstance();
209     }
210 
211     /**
212      * @see org.apache.avalon.cornerstone.services.store.Repository#getChildRepository(String)
213      */
214     public Repository getChildRepository( final String childName )
215     {
216         AbstractFileRepository child = null;
217 
218         try
219         {
220             child = createChildRepository();
221         }
222         catch( final Exception e )
223         {
224             throw new RuntimeException( "Cannot create child repository " +
225                                         childName + " : " + e );
226         }
227 
228         try
229         {
230             child.service( m_serviceManager );
231         }
232         catch( final ServiceException cme )
233         {
234             throw new RuntimeException( "Cannot service child " +
235                                         "repository " + childName +
236                                         " : " + cme );
237         }
238 
239         try
240         {
241             child.setDestination( m_destination + File.pathSeparatorChar +
242                                   childName + File.pathSeparator );
243         }
244         catch( final ConfigurationException ce )
245         {
246             throw new RuntimeException( "Cannot set destination for child child " +
247                                         "repository " + childName +
248                                         " : " + ce );
249         }
250 
251         try
252         {
253             child.initialize();
254         }
255         catch( final Exception e )
256         {
257             throw new RuntimeException( "Cannot initialize child " +
258                                         "repository " + childName +
259                                         " : " + e );
260         }
261 
262         if( DEBUG )
263         {
264             getLogger().debug( "Child repository of " + m_name + " created in " +
265                                m_destination + File.pathSeparatorChar +
266                                childName + File.pathSeparator );
267         }
268 
269         return child;
270     }
271 
272     /**
273      * Return the File Object which belongs to the given key
274      * 
275      * @param key  the key for which the File get returned
276      * @return file the File associted with the given Key
277      * @throws IOException get thrown on IO error
278      */
279     protected File getFile( final String key )
280         throws IOException
281     {
282         return new File( encode( key ) );
283     }
284 
285     /**
286      * Return the InputStream which belongs to the given key
287      * 
288      * @param key the key for which the InputStream get returned
289      * @return in the InputStram associted with the given key
290      * @throws IOException get thrown on IO error
291      */
292     protected InputStream getInputStream( final String key )
293         throws IOException
294     {
295         // This was changed to SharedFileInputStream but reverted to 
296         // fix JAMES-559. Usign SharedFileInputStream should be a good
297         // performance improvement, but more checks have to be done
298         // on the repository side to avoid concurrency in reading and
299         // writing the same file.
300         return new FileInputStream( encode( key ) );
301     }
302 
303     /**
304      * Return the OutputStream which belongs to the given key
305      * 
306      * @param key the key for which the OutputStream get returned
307      * @return out the OutputStream
308      * @throws IOException get thrown on IO error
309      */
310     protected OutputStream getOutputStream( final String key )
311         throws IOException
312     {
313         return new FileOutputStream( getFile( key ) );
314     }
315     
316     /**
317      * Remove the object associated to the given key.
318      *
319      * @param key the key to remove
320      */
321     public synchronized void remove( final String key )
322     {
323         try
324         {
325             final File file = getFile( key );
326             file.delete();
327             if( DEBUG ) getLogger().debug( "removed key " + key );
328         }
329         catch( final Exception e )
330         {
331             throw new RuntimeException( "Exception caught while removing" +
332                                         " an object: " + e );
333         }
334     }
335 
336     /**
337      * 
338      * Indicates if the given key is associated to a contained object
339      * 
340      * @param key the key which checked for
341      * @return true if the repository contains the key
342      */
343     public synchronized boolean containsKey( final String key )
344     {
345         try
346         {
347             final File file = getFile( key );
348             if( DEBUG ) getLogger().debug( "checking key " + key );
349             return file.exists();
350         }
351         catch( final Exception e )
352         {
353             throw new RuntimeException( "Exception caught while searching " +
354                                         "an object: " + e );
355         }
356     }
357 
358     /**
359      * Returns the list of used keys.
360      */
361     public Iterator list()
362     {
363         final File storeDir = new File( m_path );
364         final String[] names = storeDir.list( m_filter );
365         final ArrayList list = new ArrayList();
366 
367         for( int i = 0; i < names.length; i++ )
368         {
369             String decoded = decode(names[i]);
370             list.add(decoded);
371         }
372 
373         return list.iterator();
374     }
375 
376 
377     /**
378      * Returns a String that uniquely identifies the object.
379      * <b>Note:</b> since this method uses the Object.toString()
380      * method, it's up to the caller to make sure that this method
381      * doesn't change between different JVM executions (like
382      * it may normally happen). For this reason, it's highly recommended
383      * (even if not mandated) that Strings be used as keys.
384      *
385      * @param key the key for which the Object should be searched
386      * @return result a unique String represent the Object which belongs to the key
387      */
388     protected String encode( final String key )
389     {
390         final byte[] bytes = key.getBytes();
391         final char[] buffer = new char[ bytes.length << 1 ];
392 
393         for( int i = 0, j = 0; i < bytes.length; i++ )
394         {
395             final int k = bytes[ i ];
396             buffer[ j++ ] = HEX_DIGITS[ ( k >>> 4 ) & BYTE_MASK ];
397             buffer[ j++ ] = HEX_DIGITS[ k & BYTE_MASK ];
398         }
399 
400         StringBuffer result = new StringBuffer();
401         result.append( m_path );
402         result.append( File.separator );
403         result.append( buffer );
404         result.append( m_extension );
405         return result.toString();
406     }
407 
408 
409     /**
410      * Inverse of encode exept it do not use path.
411      * So decode(encode(s) - m_path) = s.
412      * In other words it returns a String that can be used as key to retive
413      * the record contained in the 'filename' file.
414      *
415      * @param filename the filename for which the key should generated
416      * @return key a String which can be used to retrieve the filename
417      */
418     protected String decode( String filename )
419     {
420         filename = filename.substring( 0, filename.length() - m_extension.length() );
421         final int size = filename.length();
422         final byte[] bytes = new byte[ size >>> 1 ];
423 
424         for( int i = 0, j = 0; i < size; j++ )
425         {
426             bytes[ j ] = Byte.parseByte( filename.substring( i, i + 2 ), 16 );
427             i +=2;
428         }
429 
430         return new String( bytes );
431     }
432 }