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