001: /****************************************************************
002: * Licensed to the Apache Software Foundation (ASF) under one *
003: * or more contributor license agreements. See the NOTICE file *
004: * distributed with this work for additional information *
005: * regarding copyright ownership. The ASF licenses this file *
006: * to you under the Apache License, Version 2.0 (the *
007: * "License"); you may not use this file except in compliance *
008: * with the License. You may obtain a copy of the License at *
009: * *
010: * http://www.apache.org/licenses/LICENSE-2.0 *
011: * *
012: * Unless required by applicable law or agreed to in writing, *
013: * software distributed under the License is distributed on an *
014: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
015: * KIND, either express or implied. See the License for the *
016: * specific language governing permissions and limitations *
017: * under the License. *
018: ****************************************************************/package org.apache.james.nntpserver.repository;
019:
020: import org.apache.avalon.framework.activity.Initializable;
021: import org.apache.avalon.framework.configuration.Configurable;
022: import org.apache.avalon.framework.configuration.Configuration;
023: import org.apache.avalon.framework.configuration.ConfigurationException;
024: import org.apache.avalon.framework.container.ContainerUtil;
025: import org.apache.avalon.framework.context.Context;
026: import org.apache.avalon.framework.context.ContextException;
027: import org.apache.avalon.framework.context.Contextualizable;
028: import org.apache.avalon.framework.logger.AbstractLogEnabled;
029: import org.apache.james.context.AvalonContextUtilities;
030: import org.apache.james.nntpserver.DateSinceFileFilter;
031: import org.apache.james.nntpserver.NNTPException;
032: import org.apache.james.util.io.AndFileFilter;
033: import org.apache.james.util.io.DirectoryFileFilter;
034: import org.apache.oro.io.GlobFilenameFilter;
035:
036: import java.io.File;
037: import java.io.FileOutputStream;
038: import java.io.InputStream;
039: import java.io.IOException;
040: import java.util.ArrayList;
041: import java.util.Date;
042: import java.util.HashMap;
043: import java.util.Iterator;
044: import java.util.List;
045: import java.util.Set;
046:
047: /**
048: * NNTP Repository implementation.
049: */
050: public class NNTPRepositoryImpl extends AbstractLogEnabled implements
051: NNTPRepository, Contextualizable, Configurable, Initializable {
052:
053: /**
054: * The context employed by this repository
055: */
056: private Context context;
057:
058: /**
059: * The configuration employed by this repository
060: */
061: private Configuration configuration;
062:
063: /**
064: * Whether the repository is read only
065: */
066: private boolean readOnly;
067:
068: /**
069: * The groups are located under this path.
070: */
071: private File rootPath;
072:
073: /**
074: * Articles are temporarily written here and then sent to the spooler.
075: */
076: private File tempPath;
077:
078: /**
079: * The spooler for this repository.
080: */
081: private NNTPSpooler spool;
082:
083: /**
084: * The article ID repository associated with this NNTP repository.
085: */
086: private ArticleIDRepository articleIDRepo;
087:
088: /**
089: * A map to allow lookup of valid newsgroup names
090: */
091: private HashMap groupNameMap = null;
092:
093: /**
094: * Restrict use to newsgroups specified in config only
095: */
096: private boolean definedGroupsOnly = false;
097:
098: /**
099: * 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:", "From:", "Date:",
123: "Message-ID:", "References:", "Bytes:", "Lines:" };
124:
125: /**
126: * This is a mapping of group names to NNTP group objects.
127: *
128: * TODO: This needs to be addressed so it scales better
129: */
130: private HashMap repositoryGroups = new HashMap();
131:
132: /**
133: * @see org.apache.avalon.framework.context.Contextualizable#contextualize(Context)
134: */
135: public void contextualize(Context context) throws ContextException {
136: this .context = context;
137: }
138:
139: /**
140: * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
141: */
142: public void configure(Configuration aConfiguration)
143: throws ConfigurationException {
144: configuration = aConfiguration;
145: readOnly = configuration.getChild("readOnly")
146: .getValueAsBoolean(false);
147: articleIDDomainSuffix = configuration.getChild(
148: "articleIDDomainSuffix").getValue("foo.bar.sho.boo");
149: rootPathString = configuration.getChild("rootPath").getValue(
150: null);
151: if (rootPathString == null) {
152: throw new ConfigurationException(
153: "Root path URL is required.");
154: }
155: tempPathString = configuration.getChild("tempPath").getValue(
156: null);
157: if (tempPathString == null) {
158: throw new ConfigurationException(
159: "Temp path URL is required.");
160: }
161: articleIdPathString = configuration.getChild("articleIDPath")
162: .getValue(null);
163: if (articleIdPathString == null) {
164: throw new ConfigurationException(
165: "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(
174: "NNTP repository root path URL is "
175: + rootPathString);
176: getLogger().debug(
177: "NNTP repository temp path URL is "
178: + tempPathString);
179: getLogger().debug(
180: "NNTP repository article ID path URL is "
181: + articleIdPathString);
182: }
183: Configuration newsgroupConfiguration = configuration
184: .getChild("newsgroups");
185: definedGroupsOnly = newsgroupConfiguration
186: .getAttributeAsBoolean("only", false);
187: groupNameMap = new HashMap();
188: if (newsgroupConfiguration != null) {
189: Configuration[] children = newsgroupConfiguration
190: .getChildren("newsgroup");
191: if (children != null) {
192: for (int i = 0; i < children.length; i++) {
193: String groupName = children[i].getValue();
194: groupNameMap.put(groupName, groupName);
195: }
196: }
197: }
198: getLogger().debug("Repository configuration done");
199: }
200:
201: /**
202: * @see org.apache.avalon.framework.activity.Initializable#initialize()
203: */
204: public void initialize() throws Exception {
205:
206: getLogger().debug("Starting initialize");
207: File articleIDPath = null;
208:
209: try {
210: rootPath = AvalonContextUtilities.getFile(context,
211: rootPathString);
212: tempPath = AvalonContextUtilities.getFile(context,
213: tempPathString);
214: articleIDPath = AvalonContextUtilities.getFile(context,
215: articleIdPathString);
216: } catch (Exception e) {
217: getLogger().fatalError(e.getMessage(), e);
218: throw e;
219: }
220:
221: if (articleIDPath.exists() == false) {
222: articleIDPath.mkdirs();
223: }
224:
225: articleIDRepo = new ArticleIDRepository(articleIDPath,
226: articleIDDomainSuffix);
227: spool = (NNTPSpooler) createSpooler();
228: spool.setRepository(this );
229: spool.setArticleIDRepository(articleIDRepo);
230: if (getLogger().isDebugEnabled()) {
231: getLogger().debug("repository:readOnly=" + readOnly);
232: getLogger()
233: .debug(
234: "repository:rootPath="
235: + rootPath.getAbsolutePath());
236: getLogger()
237: .debug(
238: "repository:tempPath="
239: + tempPath.getAbsolutePath());
240: }
241:
242: if (rootPath.exists() == false) {
243: rootPath.mkdirs();
244: } else if (!(rootPath.isDirectory())) {
245: StringBuffer errorBuffer = new StringBuffer(128)
246: .append(
247: "NNTP repository root directory is improperly configured. The specified path ")
248: .append(rootPathString).append(
249: " is not a directory.");
250: throw new ConfigurationException(errorBuffer.toString());
251: }
252:
253: Set groups = groupNameMap.keySet();
254: Iterator groupIterator = groups.iterator();
255: while (groupIterator.hasNext()) {
256: String groupName = (String) groupIterator.next();
257: File groupFile = new File(rootPath, groupName);
258: if (groupFile.exists() == false) {
259: groupFile.mkdirs();
260: } else if (!(groupFile.isDirectory())) {
261: StringBuffer errorBuffer = new StringBuffer(128)
262: .append(
263: "A file exists in the NNTP root directory with the same name as a newsgroup. File ")
264: .append(groupName).append("in directory ")
265: .append(rootPathString).append(
266: " is not a directory.");
267: throw new ConfigurationException(errorBuffer.toString());
268: }
269: }
270: if (tempPath.exists() == false) {
271: tempPath.mkdirs();
272: } else if (!(tempPath.isDirectory())) {
273: StringBuffer errorBuffer = new StringBuffer(128)
274: .append(
275: "NNTP repository temp directory is improperly configured. The specified path ")
276: .append(tempPathString).append(
277: " is not a directory.");
278: throw new ConfigurationException(errorBuffer.toString());
279: }
280:
281: getLogger().debug("repository initialization done");
282: }
283:
284: /**
285: * @see org.apache.james.nntpserver.repository.NNTPRepository#isReadOnly()
286: */
287: public boolean isReadOnly() {
288: return readOnly;
289: }
290:
291: /**
292: * @see org.apache.james.nntpserver.repository.NNTPRepository#getGroup(String)
293: */
294: public NNTPGroup getGroup(String groupName) {
295: if (definedGroupsOnly && groupNameMap.get(groupName) == null) {
296: if (getLogger().isDebugEnabled()) {
297: getLogger()
298: .debug(
299: groupName
300: + " is not a newsgroup hosted on this server.");
301: }
302: return null;
303: }
304: File groupFile = new File(rootPath, groupName);
305: NNTPGroup groupToReturn = null;
306: synchronized (this ) {
307: groupToReturn = (NNTPGroup) repositoryGroups.get(groupName);
308: if ((groupToReturn == null) && groupFile.exists()
309: && groupFile.isDirectory()) {
310: try {
311: groupToReturn = new NNTPGroupImpl(groupFile);
312: ContainerUtil.enableLogging(groupToReturn,
313: getLogger());
314: ContainerUtil.contextualize(groupToReturn, context);
315: ContainerUtil.initialize(groupToReturn);
316: repositoryGroups.put(groupName, groupToReturn);
317: } catch (Exception e) {
318: getLogger().error("Couldn't create group object.",
319: e);
320: groupToReturn = null;
321: }
322: }
323: }
324: return groupToReturn;
325: }
326:
327: /**
328: * @see org.apache.james.nntpserver.repository.NNTPRepository#getArticleFromID(String)
329: */
330: public NNTPArticle getArticleFromID(String id) {
331: try {
332: return articleIDRepo.getArticle(this , id);
333: } catch (Exception ex) {
334: getLogger().error("Couldn't get article " + id + ": ", ex);
335: return null;
336: }
337: }
338:
339: /**
340: * @see org.apache.james.nntpserver.repository.NNTPRepository#createArticle(InputStream)
341: */
342: public void createArticle(InputStream in) {
343: StringBuffer fileBuffer = new StringBuffer(32).append(
344: System.currentTimeMillis()).append(".").append(
345: Math.random());
346: File f = new File(tempPath, fileBuffer.toString());
347: FileOutputStream fout = null;
348: try {
349: fout = new FileOutputStream(f);
350: byte[] readBuffer = new byte[1024];
351: int bytesRead = 0;
352: while ((bytesRead = in.read(readBuffer, 0, 1024)) > 0) {
353: fout.write(readBuffer, 0, bytesRead);
354: }
355: fout.flush();
356: fout.close();
357: fout = null;
358: boolean renamed = f.renameTo(new File(spool.getSpoolPath(),
359: f.getName()));
360: if (!renamed) {
361: throw new IOException(
362: "Could not create article on the spool.");
363: }
364: } catch (IOException ex) {
365: throw new NNTPException("create article failed", ex);
366: } finally {
367: if (fout != null) {
368: try {
369: fout.close();
370: } catch (IOException ioe) {
371: // Ignored
372: }
373: }
374: }
375: }
376:
377: class GroupFilter implements java.io.FilenameFilter {
378: public boolean accept(java.io.File dir, String name) {
379: if (getLogger().isDebugEnabled()) {
380: getLogger()
381: .debug(
382: ((definedGroupsOnly ? groupNameMap
383: .containsKey(name) : true) ? "Accepting "
384: : "Rejecting")
385: + name);
386: }
387:
388: return definedGroupsOnly ? groupNameMap.containsKey(name)
389: : true;
390: }
391: }
392:
393: /**
394: * @see org.apache.james.nntpserver.repository.NNTPRepository#getMatchedGroups(String)
395: */
396: public Iterator getMatchedGroups(String wildmat) {
397: File[] f = rootPath.listFiles(new AndFileFilter(
398: new GroupFilter(), new AndFileFilter(
399: new DirectoryFileFilter(),
400: new GlobFilenameFilter(wildmat))));
401: return getGroups(f);
402: }
403:
404: /**
405: * Gets an iterator of all news groups represented by the files
406: * in the parameter array.
407: *
408: * @param f the array of files that correspond to news groups
409: *
410: * @return an iterator of news groups
411: */
412: private Iterator getGroups(File[] f) {
413: List list = new ArrayList();
414: for (int i = 0; i < f.length; i++) {
415: if (f[i] != null) {
416: list.add(getGroup(f[i].getName()));
417: }
418: }
419: return list.iterator();
420: }
421:
422: /**
423: * @see org.apache.james.nntpserver.repository.NNTPRepository#getGroupsSince(Date)
424: */
425: public Iterator getGroupsSince(Date dt) {
426: File[] f = rootPath.listFiles(new AndFileFilter(
427: new GroupFilter(), new AndFileFilter(
428: new DirectoryFileFilter(),
429: new DateSinceFileFilter(dt.getTime()))));
430: return getGroups(f);
431: }
432:
433: // gets the list of groups.
434: // creates iterator that concatenates the article iterators in the list of groups.
435: // there is at most one article iterator reference for all the groups
436:
437: /**
438: * @see org.apache.james.nntpserver.repository.NNTPRepository#getArticlesSince(Date)
439: */
440: public Iterator getArticlesSince(final Date dt) {
441: final Iterator giter = getGroupsSince(dt);
442: return new Iterator() {
443:
444: private Iterator iter = null;
445:
446: public boolean hasNext() {
447: if (iter == null) {
448: if (giter.hasNext()) {
449: NNTPGroup group = (NNTPGroup) giter.next();
450: iter = group.getArticlesSince(dt);
451: } else {
452: return false;
453: }
454: }
455: if (iter.hasNext()) {
456: return true;
457: } else {
458: iter = null;
459: return hasNext();
460: }
461: }
462:
463: public Object next() {
464: return iter.next();
465: }
466:
467: public void remove() {
468: throw new UnsupportedOperationException(
469: "remove not supported");
470: }
471: };
472: }
473:
474: /**
475: * @see org.apache.james.nntpserver.repository.NNTPRepository#getOverviewFormat()
476: */
477: public String[] getOverviewFormat() {
478: return overviewFormat;
479: }
480:
481: /**
482: * Creates an instance of the spooler class.
483: *
484: * TODO: This method doesn't properly implement the Avalon lifecycle.
485: */
486: private NNTPSpooler createSpooler() throws ConfigurationException {
487: String className = "org.apache.james.nntpserver.repository.NNTPSpooler";
488: Configuration spoolerConfiguration = configuration
489: .getChild("spool");
490: try {
491: // Must be a subclass of org.apache.james.nntpserver.repository.NNTPSpooler
492: className = spoolerConfiguration.getAttribute("class");
493: } catch (ConfigurationException ce) {
494: // Use the default class.
495: }
496: try {
497: Object obj = getClass().getClassLoader().loadClass(
498: className).newInstance();
499: // TODO: Need to support service
500: ContainerUtil.enableLogging(obj, getLogger());
501: ContainerUtil.contextualize(obj, context);
502: ContainerUtil.configure(obj, spoolerConfiguration
503: .getChild("configuration"));
504: ContainerUtil.initialize(obj);
505: return (NNTPSpooler) obj;
506: } catch (ClassCastException cce) {
507: StringBuffer errorBuffer = new StringBuffer(128)
508: .append(
509: "Spooler initialization failed because the spooler class ")
510: .append(className)
511: .append(
512: " was not a subclass of org.apache.james.nntpserver.repository.NNTPSpooler");
513: String errorString = errorBuffer.toString();
514: getLogger().error(errorString, cce);
515: throw new ConfigurationException(errorString, cce);
516: } catch (Exception ex) {
517: getLogger().error("Spooler initialization failed", ex);
518: throw new ConfigurationException(
519: "Spooler initialization failed", ex);
520: }
521: }
522: }
|