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.nntpserver.repository;
23  
24  import org.apache.avalon.framework.activity.Initializable;
25  import org.apache.avalon.framework.configuration.Configurable;
26  import org.apache.avalon.framework.configuration.Configuration;
27  import org.apache.avalon.framework.configuration.ConfigurationException;
28  import org.apache.avalon.framework.container.ContainerUtil;
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.nntpserver.DateSinceFileFilter;
34  import org.apache.james.nntpserver.NNTPException;
35  import org.apache.james.services.FileSystem;
36  import org.apache.james.util.io.AndFileFilter;
37  import org.apache.james.util.io.DirectoryFileFilter;
38  import org.apache.oro.io.GlobFilenameFilter;
39  
40  import java.io.File;
41  import java.io.FileOutputStream;
42  import java.io.InputStream;
43  import java.io.IOException;
44  import java.util.ArrayList;
45  import java.util.Date;
46  import java.util.HashMap;
47  import java.util.Iterator;
48  import java.util.List;
49  import java.util.Set;
50  
51  /**
52   * NNTP Repository implementation.
53   */
54  public class NNTPRepositoryImpl extends AbstractLogEnabled 
55      implements NNTPRepository, Serviceable, Configurable, Initializable {
56  
57      /**
58       * The configuration employed by this repository
59       */
60      private Configuration configuration;
61  
62      /**
63       * Whether the repository is read only
64       */
65      private boolean readOnly;
66  
67      /**
68       * The groups are located under this path.
69       */
70      private File rootPath;
71  
72      /**
73       * Articles are temporarily written here and then sent to the spooler.
74       */
75      private File tempPath;
76  
77      /**
78       * The spooler for this repository.
79       */
80      private NNTPSpooler spool;
81  
82      /**
83       * The article ID repository associated with this NNTP repository.
84       */
85      private ArticleIDRepository articleIDRepo;
86  
87      /**
88       * A map to allow lookup of valid newsgroup names
89       */
90      private HashMap groupNameMap = null;
91  
92      /**
93       * Restrict use to newsgroups specified in config only
94       */
95      private boolean definedGroupsOnly = false;
96  
97      /**
98       * The root path as a String.
99       */
100     private String rootPathString = null;
101 
102     /**
103      * The temp path as a String.
104      */
105     private String tempPathString = null;
106 
107     /**
108      * The article ID path as a String.
109      */
110     private String articleIdPathString = null;
111 
112     /**
113      * The domain suffix used for files in the article ID repository.
114      */
115     private String articleIDDomainSuffix = null;
116 
117     /**
118      * The ordered list of fields returned in the overview format for
119      * articles stored in this repository.
120      */
121     private String[] overviewFormat = { "Subject:",
122                                         "From:",
123                                         "Date:",
124                                         "Message-ID:",
125                                         "References:",
126                                         "Bytes:",
127                                         "Lines:"
128                                       };
129 
130     /**
131      * This is a mapping of group names to NNTP group objects.
132      *
133      * TODO: This needs to be addressed so it scales better
134      */
135     private HashMap repositoryGroups = new HashMap();
136 
137     /**
138      * The service manager
139      */
140     private ServiceManager serviceManager;
141 
142     /**
143      * The fileSystem service
144      */
145     private FileSystem fileSystem;
146 
147     /**
148      * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
149      */
150     public void configure( Configuration aConfiguration ) throws ConfigurationException {
151         configuration = aConfiguration;
152         readOnly = configuration.getChild("readOnly").getValueAsBoolean(false);
153         articleIDDomainSuffix = configuration.getChild("articleIDDomainSuffix")
154             .getValue("foo.bar.sho.boo");
155         rootPathString = configuration.getChild("rootPath").getValue(null);
156         if (rootPathString == null) {
157             throw new ConfigurationException("Root path URL is required.");
158         }
159         tempPathString = configuration.getChild("tempPath").getValue(null);
160         if (tempPathString == null) {
161             throw new ConfigurationException("Temp path URL is required.");
162         }
163         articleIdPathString = configuration.getChild("articleIDPath").getValue(null);
164         if (articleIdPathString == null) {
165             throw new ConfigurationException("Article ID path URL is required.");
166         }
167         if (getLogger().isDebugEnabled()) {
168             if (readOnly) {
169                 getLogger().debug("NNTP repository is read only.");
170             } else {
171                 getLogger().debug("NNTP repository is writeable.");
172             }
173             getLogger().debug("NNTP repository root path URL is " + rootPathString);
174             getLogger().debug("NNTP repository temp path URL is " + tempPathString);
175             getLogger().debug("NNTP repository article ID path URL is " + articleIdPathString);
176         }
177         Configuration newsgroupConfiguration = configuration.getChild("newsgroups");
178         definedGroupsOnly = newsgroupConfiguration.getAttributeAsBoolean("only", false);
179         groupNameMap = new HashMap();
180         if ( newsgroupConfiguration != null ) {
181             Configuration[] children = newsgroupConfiguration.getChildren("newsgroup");
182             if ( children != null ) {
183                 for ( int i = 0 ; i < children.length ; i++ ) {
184                     String groupName = children[i].getValue();
185                     groupNameMap.put(groupName, groupName);
186                 }
187             }
188         }
189         getLogger().debug("Repository configuration done");
190     }
191 
192     /**
193      * @see org.apache.avalon.framework.activity.Initializable#initialize()
194      */
195     public void initialize() throws Exception {
196 
197         getLogger().debug("Starting initialize");
198         File articleIDPath = null;
199 
200         try {
201             rootPath = fileSystem.getFile(rootPathString);
202             tempPath = fileSystem.getFile(tempPathString);
203             articleIDPath = fileSystem.getFile(articleIdPathString);
204         } catch (Exception e) {
205             getLogger().fatalError(e.getMessage(), e);
206             throw e;
207         }
208 
209         if ( articleIDPath.exists() == false ) {
210             articleIDPath.mkdirs();
211         }
212 
213         articleIDRepo = new ArticleIDRepository(articleIDPath, articleIDDomainSuffix);
214         spool = (NNTPSpooler)createSpooler();
215         spool.setRepository(this);
216         spool.setArticleIDRepository(articleIDRepo);
217         if (getLogger().isDebugEnabled()) {
218             getLogger().debug("repository:readOnly=" + readOnly);
219             getLogger().debug("repository:rootPath=" + rootPath.getAbsolutePath());
220             getLogger().debug("repository:tempPath=" + tempPath.getAbsolutePath());
221         }
222 
223         if ( rootPath.exists() == false ) {
224             rootPath.mkdirs();
225         } else if (!(rootPath.isDirectory())) {
226             StringBuffer errorBuffer =
227                 new StringBuffer(128)
228                     .append("NNTP repository root directory is improperly configured.  The specified path ")
229                     .append(rootPathString)
230                     .append(" is not a directory.");
231             throw new ConfigurationException(errorBuffer.toString());
232         }
233 
234         Set groups = groupNameMap.keySet();
235         Iterator groupIterator = groups.iterator();
236         while( groupIterator.hasNext() ) {
237             String groupName = (String)groupIterator.next();
238             File groupFile = new File(rootPath,groupName);
239             if ( groupFile.exists() == false ) {
240                 groupFile.mkdirs();
241             } else if (!(groupFile.isDirectory())) {
242                 StringBuffer errorBuffer =
243                     new StringBuffer(128)
244                         .append("A file exists in the NNTP root directory with the same name as a newsgroup.  File ")
245                         .append(groupName)
246                         .append("in directory ")
247                         .append(rootPathString)
248                         .append(" is not a directory.");
249                 throw new ConfigurationException(errorBuffer.toString());
250             }
251         }
252         if ( tempPath.exists() == false ) {
253             tempPath.mkdirs();
254         } else if (!(tempPath.isDirectory())) {
255             StringBuffer errorBuffer =
256                 new StringBuffer(128)
257                     .append("NNTP repository temp directory is improperly configured.  The specified path ")
258                     .append(tempPathString)
259                     .append(" is not a directory.");
260             throw new ConfigurationException(errorBuffer.toString());
261         }
262 
263         getLogger().debug("repository initialization done");
264     }
265 
266     /**
267      * @see org.apache.james.nntpserver.repository.NNTPRepository#isReadOnly()
268      */
269     public boolean isReadOnly() {
270         return readOnly;
271     }
272 
273     /**
274      * @see org.apache.james.nntpserver.repository.NNTPRepository#getGroup(String)
275      */
276     public NNTPGroup getGroup(String groupName) {
277         if (definedGroupsOnly && groupNameMap.get(groupName) == null) {
278             if (getLogger().isDebugEnabled()) {
279                 getLogger().debug(groupName + " is not a newsgroup hosted on this server.");
280             }
281             return null;
282         }
283         File groupFile = new File(rootPath,groupName);
284         NNTPGroup groupToReturn = null;
285         synchronized(this) {
286             groupToReturn = (NNTPGroup)repositoryGroups.get(groupName);
287             if ((groupToReturn == null) && groupFile.exists() && groupFile.isDirectory() ) {
288                 try {
289                     groupToReturn = new NNTPGroupImpl(groupFile);
290                     ContainerUtil.enableLogging(groupToReturn, getLogger());
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.avalon.framework.service.Serviceable#service(org.apache.avalon.framework.service.ServiceManager)
441      */
442     public void service(ServiceManager serviceManager) throws ServiceException {
443         this.serviceManager = serviceManager;
444         setFileSystem((FileSystem) serviceManager.lookup(FileSystem.ROLE));
445     }
446 
447     /**
448      * Setter for the FileSystem dependency
449      * @param system filesystem service
450      */
451     private void setFileSystem(FileSystem system) {
452         this.fileSystem = system;
453     }
454 
455     /**
456      * @see org.apache.james.nntpserver.repository.NNTPRepository#getOverviewFormat()
457      */
458     public String[] getOverviewFormat() {
459         return overviewFormat;
460     }
461 
462     /**
463      * Creates an instance of the spooler class.
464      *
465      * TODO: This method doesn't properly implement the Avalon lifecycle.
466      */
467     private NNTPSpooler createSpooler() 
468             throws ConfigurationException {
469         String className = NNTPSpooler.class.getName();
470         Configuration spoolerConfiguration = configuration.getChild("spool");
471         try {
472             // Must be a subclass of org.apache.james.nntpserver.repository.NNTPSpooler
473             className = spoolerConfiguration.getAttribute("class");
474         } catch(ConfigurationException ce) {
475             // Use the default class.
476         }
477         try {
478             Object obj = Thread.currentThread().getContextClassLoader().loadClass(className).newInstance();
479             ContainerUtil.enableLogging(obj, getLogger());
480             ContainerUtil.service(obj, serviceManager);
481             ContainerUtil.configure(obj, spoolerConfiguration.getChild("configuration"));
482             ContainerUtil.initialize(obj);
483             return (NNTPSpooler)obj;
484         } catch(ClassCastException cce) {
485             StringBuffer errorBuffer =
486                 new StringBuffer(128)
487                     .append("Spooler initialization failed because the spooler class ")
488                     .append(className)
489                     .append(" was not a subclass of org.apache.james.nntpserver.repository.NNTPSpooler");
490             String errorString = errorBuffer.toString();
491             getLogger().error(errorString, cce);
492             throw new ConfigurationException(errorString, cce);
493         } catch(Exception ex) {
494             getLogger().error("Spooler initialization failed",ex);
495             throw new ConfigurationException("Spooler initialization failed",ex);
496         }
497     }
498 }