View Javadoc

1   /************************************************************************
2    * Copyright (c) 2000-2006 The Apache Software Foundation.             *
3    * All rights reserved.                                                *
4    * ------------------------------------------------------------------- *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you *
6    * may not use this file except in compliance with the License. You    *
7    * may obtain a copy of the License at:                                *
8    *                                                                     *
9    *     http://www.apache.org/licenses/LICENSE-2.0                      *
10   *                                                                     *
11   * Unless required by applicable law or agreed to in writing, software *
12   * distributed under the License is distributed on an "AS IS" BASIS,   *
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or     *
14   * implied.  See the License for the specific language governing       *
15   * permissions and limitations under the License.                      *
16   ***********************************************************************/
17  
18  package org.apache.james.nntpserver.repository;
19  
20  import org.apache.avalon.framework.activity.Initializable;
21  import org.apache.avalon.framework.configuration.Configurable;
22  import org.apache.avalon.framework.configuration.Configuration;
23  import org.apache.avalon.framework.configuration.ConfigurationException;
24  import org.apache.avalon.framework.container.ContainerUtil;
25  import org.apache.avalon.framework.context.Context;
26  import org.apache.avalon.framework.context.ContextException;
27  import org.apache.avalon.framework.context.Contextualizable;
28  import org.apache.avalon.framework.logger.AbstractLogEnabled;
29  import org.apache.james.context.AvalonContextUtilities;
30  import org.apache.james.nntpserver.DateSinceFileFilter;
31  import org.apache.james.nntpserver.NNTPException;
32  import org.apache.james.util.io.AndFileFilter;
33  import org.apache.james.util.io.DirectoryFileFilter;
34  import org.apache.oro.io.GlobFilenameFilter;
35  
36  import java.io.File;
37  import java.io.FileOutputStream;
38  import java.io.InputStream;
39  import java.io.IOException;
40  import java.util.ArrayList;
41  import java.util.Date;
42  import java.util.HashMap;
43  import java.util.Iterator;
44  import java.util.List;
45  import java.util.Set;
46  
47  /***
48   * NNTP Repository implementation.
49   */
50  public class NNTPRepositoryImpl extends AbstractLogEnabled 
51      implements NNTPRepository, Contextualizable, Configurable, Initializable {
52  
53      /***
54       * The context employed by this repository
55       */
56      private Context context;
57  
58      /***
59       * The configuration employed by this repository
60       */
61      private Configuration configuration;
62  
63      /***
64       * Whether the repository is read only
65       */
66      private boolean readOnly;
67  
68      /***
69       * The groups are located under this path.
70       */
71      private File rootPath;
72  
73      /***
74       * Articles are temporarily written here and then sent to the spooler.
75       */
76      private File tempPath;
77  
78      /***
79       * The spooler for this repository.
80       */
81      private NNTPSpooler spool;
82  
83      /***
84       * The article ID repository associated with this NNTP repository.
85       */
86      private ArticleIDRepository articleIDRepo;
87  
88      /***
89       * A map to allow lookup of valid newsgroup names
90       */
91      private HashMap groupNameMap = null;
92  
93      /***
94       * Restrict use to newsgroups specified in config only
95       */
96      private boolean definedGroupsOnly = false;
97  
98      /***
99       * The root path as a String.
100      */
101     private String rootPathString = null;
102 
103     /***
104      * The temp path as a String.
105      */
106     private String tempPathString = null;
107 
108     /***
109      * The article ID path as a String.
110      */
111     private String articleIdPathString = null;
112 
113     /***
114      * The domain suffix used for files in the article ID repository.
115      */
116     private String articleIDDomainSuffix = null;
117 
118     /***
119      * The ordered list of fields returned in the overview format for
120      * articles stored in this repository.
121      */
122     private String[] overviewFormat = { "Subject:",
123                                         "From:",
124                                         "Date:",
125                                         "Message-ID:",
126                                         "References:",
127                                         "Bytes:",
128                                         "Lines:"
129                                       };
130 
131     /***
132      * This is a mapping of group names to NNTP group objects.
133      *
134      * TODO: This needs to be addressed so it scales better
135      */
136     private HashMap repositoryGroups = new HashMap();
137 
138     /***
139      * @see org.apache.avalon.framework.context.Contextualizable#contextualize(Context)
140      */
141     public void contextualize(Context context)
142             throws ContextException {
143         this.context = context;
144     }
145 
146     /***
147      * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
148      */
149     public void configure( Configuration aConfiguration ) throws ConfigurationException {
150         configuration = aConfiguration;
151         readOnly = configuration.getChild("readOnly").getValueAsBoolean(false);
152         articleIDDomainSuffix = configuration.getChild("articleIDDomainSuffix")
153             .getValue("foo.bar.sho.boo");
154         rootPathString = configuration.getChild("rootPath").getValue(null);
155         if (rootPathString == null) {
156             throw new ConfigurationException("Root path URL is required.");
157         }
158         tempPathString = configuration.getChild("tempPath").getValue(null);
159         if (tempPathString == null) {
160             throw new ConfigurationException("Temp path URL is required.");
161         }
162         articleIdPathString = configuration.getChild("articleIDPath").getValue(null);
163         if (articleIdPathString == null) {
164             throw new ConfigurationException("Article ID path URL is required.");
165         }
166         if (getLogger().isDebugEnabled()) {
167             if (readOnly) {
168                 getLogger().debug("NNTP repository is read only.");
169             } else {
170                 getLogger().debug("NNTP repository is writeable.");
171             }
172             getLogger().debug("NNTP repository root path URL is " + rootPathString);
173             getLogger().debug("NNTP repository temp path URL is " + tempPathString);
174             getLogger().debug("NNTP repository article ID path URL is " + articleIdPathString);
175         }
176         Configuration newsgroupConfiguration = configuration.getChild("newsgroups");
177         definedGroupsOnly = newsgroupConfiguration.getAttributeAsBoolean("only", false);
178         groupNameMap = new HashMap();
179         if ( newsgroupConfiguration != null ) {
180             Configuration[] children = newsgroupConfiguration.getChildren("newsgroup");
181             if ( children != null ) {
182                 for ( int i = 0 ; i < children.length ; i++ ) {
183                     String groupName = children[i].getValue();
184                     groupNameMap.put(groupName, groupName);
185                 }
186             }
187         }
188         getLogger().debug("Repository configuration done");
189     }
190 
191     /***
192      * @see org.apache.avalon.framework.activity.Initializable#initialize()
193      */
194     public void initialize() throws Exception {
195 
196         getLogger().debug("Starting initialize");
197         File articleIDPath = null;
198 
199         try {
200             rootPath = AvalonContextUtilities.getFile(context, rootPathString);
201             tempPath = AvalonContextUtilities.getFile(context, tempPathString);
202             articleIDPath = AvalonContextUtilities.getFile(context, articleIdPathString);
203         } catch (Exception e) {
204             getLogger().fatalError(e.getMessage(), e);
205             throw e;
206         }
207 
208         if ( articleIDPath.exists() == false ) {
209             articleIDPath.mkdirs();
210         }
211 
212         articleIDRepo = new ArticleIDRepository(articleIDPath, articleIDDomainSuffix);
213         spool = (NNTPSpooler)createSpooler();
214         spool.setRepository(this);
215         spool.setArticleIDRepository(articleIDRepo);
216         if (getLogger().isDebugEnabled()) {
217             getLogger().debug("repository:readOnly=" + readOnly);
218             getLogger().debug("repository:rootPath=" + rootPath.getAbsolutePath());
219             getLogger().debug("repository:tempPath=" + tempPath.getAbsolutePath());
220         }
221 
222         if ( rootPath.exists() == false ) {
223             rootPath.mkdirs();
224         } else if (!(rootPath.isDirectory())) {
225             StringBuffer errorBuffer =
226                 new StringBuffer(128)
227                     .append("NNTP repository root directory is improperly configured.  The specified path ")
228                     .append(rootPathString)
229                     .append(" is not a directory.");
230             throw new ConfigurationException(errorBuffer.toString());
231         }
232 
233         Set groups = groupNameMap.keySet();
234         Iterator groupIterator = groups.iterator();
235         while( groupIterator.hasNext() ) {
236             String groupName = (String)groupIterator.next();
237             File groupFile = new File(rootPath,groupName);
238             if ( groupFile.exists() == false ) {
239                 groupFile.mkdirs();
240             } else if (!(groupFile.isDirectory())) {
241                 StringBuffer errorBuffer =
242                     new StringBuffer(128)
243                         .append("A file exists in the NNTP root directory with the same name as a newsgroup.  File ")
244                         .append(groupName)
245                         .append("in directory ")
246                         .append(rootPathString)
247                         .append(" is not a directory.");
248                 throw new ConfigurationException(errorBuffer.toString());
249             }
250         }
251         if ( tempPath.exists() == false ) {
252             tempPath.mkdirs();
253         } else if (!(tempPath.isDirectory())) {
254             StringBuffer errorBuffer =
255                 new StringBuffer(128)
256                     .append("NNTP repository temp directory is improperly configured.  The specified path ")
257                     .append(tempPathString)
258                     .append(" is not a directory.");
259             throw new ConfigurationException(errorBuffer.toString());
260         }
261 
262         getLogger().debug("repository initialization done");
263     }
264 
265     /***
266      * @see org.apache.james.nntpserver.repository.NNTPRepository#isReadOnly()
267      */
268     public boolean isReadOnly() {
269         return readOnly;
270     }
271 
272     /***
273      * @see org.apache.james.nntpserver.repository.NNTPRepository#getGroup(String)
274      */
275     public NNTPGroup getGroup(String groupName) {
276         if (definedGroupsOnly && groupNameMap.get(groupName) == null) {
277             if (getLogger().isDebugEnabled()) {
278                 getLogger().debug(groupName + " is not a newsgroup hosted on this server.");
279             }
280             return null;
281         }
282         File groupFile = new File(rootPath,groupName);
283         NNTPGroup groupToReturn = null;
284         synchronized(this) {
285             groupToReturn = (NNTPGroup)repositoryGroups.get(groupName);
286             if ((groupToReturn == null) && groupFile.exists() && groupFile.isDirectory() ) {
287                 try {
288                     groupToReturn = new NNTPGroupImpl(groupFile);
289                     ContainerUtil.enableLogging(groupToReturn, getLogger());
290                     ContainerUtil.contextualize(groupToReturn, context);
291                     ContainerUtil.initialize(groupToReturn);
292                     repositoryGroups.put(groupName, groupToReturn);
293                 } catch (Exception e) {
294                     getLogger().error("Couldn't create group object.", e);
295                     groupToReturn = null;
296                 }
297             }
298         }
299         return groupToReturn;
300     }
301 
302     /***
303      * @see org.apache.james.nntpserver.repository.NNTPRepository#getArticleFromID(String)
304      */
305     public NNTPArticle getArticleFromID(String id) {
306         try {
307             return articleIDRepo.getArticle(this,id);
308         } catch(Exception ex) {
309             getLogger().error("Couldn't get article " + id + ": ", ex);
310             return null;
311         }
312     }
313 
314     /***
315      * @see org.apache.james.nntpserver.repository.NNTPRepository#createArticle(InputStream)
316      */
317     public void createArticle(InputStream in) {
318         StringBuffer fileBuffer =
319             new StringBuffer(32)
320                     .append(System.currentTimeMillis())
321                     .append(".")
322                     .append(Math.random());
323         File f = new File(tempPath, fileBuffer.toString());
324         FileOutputStream fout = null;
325         try {
326             fout = new FileOutputStream(f);
327             byte[] readBuffer = new byte[1024];
328             int bytesRead = 0;
329             while ( ( bytesRead = in.read(readBuffer, 0, 1024) ) > 0 ) {
330                 fout.write(readBuffer, 0, bytesRead);
331             }
332             fout.flush();
333             fout.close();
334             fout = null;
335             boolean renamed = f.renameTo(new File(spool.getSpoolPath(),f.getName()));
336             if (!renamed) {
337                 throw new IOException("Could not create article on the spool.");
338             }
339         } catch(IOException ex) {
340             throw new NNTPException("create article failed",ex);
341         } finally {
342             if (fout != null) {
343                 try {
344                     fout.close();
345                 } catch (IOException ioe) {
346                     // Ignored
347                 }
348             }
349         }
350     }
351 
352     class GroupFilter implements java.io.FilenameFilter {
353         public boolean accept(java.io.File dir, String name) {
354             if (getLogger().isDebugEnabled()) {
355                 getLogger().debug(((definedGroupsOnly ? groupNameMap.containsKey(name) : true) ? "Accepting ": "Rejecting") + name);
356             }
357 
358             return definedGroupsOnly ? groupNameMap.containsKey(name) : true;
359         }
360     }
361 
362     /***
363      * @see org.apache.james.nntpserver.repository.NNTPRepository#getMatchedGroups(String)
364      */
365     public Iterator getMatchedGroups(String wildmat) {
366         File[] f = rootPath.listFiles(new AndFileFilter(new GroupFilter(), new AndFileFilter
367             (new DirectoryFileFilter(),new GlobFilenameFilter(wildmat))));
368         return getGroups(f);
369     }
370 
371     /***
372      * Gets an iterator of all news groups represented by the files
373      * in the parameter array.
374      *
375      * @param f the array of files that correspond to news groups
376      *
377      * @return an iterator of news groups
378      */
379     private Iterator getGroups(File[] f) {
380         List list = new ArrayList();
381         for ( int i = 0 ; i < f.length ; i++ ) {
382             if (f[i] != null) {
383                 list.add(getGroup(f[i].getName()));
384             }
385         }
386         return list.iterator();
387     }
388 
389     /***
390      * @see org.apache.james.nntpserver.repository.NNTPRepository#getGroupsSince(Date)
391      */
392     public Iterator getGroupsSince(Date dt) {
393         File[] f = rootPath.listFiles(new AndFileFilter(new GroupFilter(), new AndFileFilter
394             (new DirectoryFileFilter(),new DateSinceFileFilter(dt.getTime()))));
395         return getGroups(f);
396     }
397 
398     // gets the list of groups.
399     // creates iterator that concatenates the article iterators in the list of groups.
400     // there is at most one article iterator reference for all the groups
401 
402     /***
403      * @see org.apache.james.nntpserver.repository.NNTPRepository#getArticlesSince(Date)
404      */
405     public Iterator getArticlesSince(final Date dt) {
406         final Iterator giter = getGroupsSince(dt);
407         return new Iterator() {
408 
409                 private Iterator iter = null;
410 
411                 public boolean hasNext() {
412                     if ( iter == null ) {
413                         if ( giter.hasNext() ) {
414                             NNTPGroup group = (NNTPGroup)giter.next();
415                             iter = group.getArticlesSince(dt);
416                         }
417                         else {
418                             return false;
419                         }
420                     }
421                     if ( iter.hasNext() ) {
422                         return true;
423                     } else {
424                         iter = null;
425                         return hasNext();
426                     }
427                 }
428 
429                 public Object next() {
430                     return iter.next();
431                 }
432 
433                 public void remove() {
434                     throw new UnsupportedOperationException("remove not supported");
435                 }
436             };
437     }
438 
439     /***
440      * @see org.apache.james.nntpserver.repository.NNTPRepository#getOverviewFormat()
441      */
442     public String[] getOverviewFormat() {
443         return overviewFormat;
444     }
445 
446     /***
447      * Creates an instance of the spooler class.
448      *
449      * TODO: This method doesn't properly implement the Avalon lifecycle.
450      */
451     private NNTPSpooler createSpooler() 
452             throws ConfigurationException {
453         String className = "org.apache.james.nntpserver.repository.NNTPSpooler";
454         Configuration spoolerConfiguration = configuration.getChild("spool");
455         try {
456             // Must be a subclass of org.apache.james.nntpserver.repository.NNTPSpooler
457             className = spoolerConfiguration.getAttribute("class");
458         } catch(ConfigurationException ce) {
459             // Use the default class.
460         }
461         try {
462             Object obj = getClass().getClassLoader().loadClass(className).newInstance();
463             // TODO: Need to support service
464             ContainerUtil.enableLogging(obj, getLogger());
465             ContainerUtil.contextualize(obj, context);
466             ContainerUtil.configure(obj, spoolerConfiguration.getChild("configuration"));
467             ContainerUtil.initialize(obj);
468             return (NNTPSpooler)obj;
469         } catch(ClassCastException cce) {
470             StringBuffer errorBuffer =
471                 new StringBuffer(128)
472                     .append("Spooler initialization failed because the spooler class ")
473                     .append(className)
474                     .append(" was not a subclass of org.apache.james.nntpserver.repository.NNTPSpooler");
475             String errorString = errorBuffer.toString();
476             getLogger().error(errorString, cce);
477             throw new ConfigurationException(errorString, cce);
478         } catch(Exception ex) {
479             getLogger().error("Spooler initialization failed",ex);
480             throw new ConfigurationException("Spooler initialization failed",ex);
481         }
482     }
483 }