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