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
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
399
400
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
457 className = spoolerConfiguration.getAttribute("class");
458 } catch(ConfigurationException ce) {
459
460 }
461 try {
462 Object obj = getClass().getClassLoader().loadClass(className).newInstance();
463
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 }