FileDocCategorySizeDatePackage
NNTPRepositoryImpl.javaAPI DocApache James 2.3.118524Fri Jan 12 12:56:24 GMT 2007org.apache.james.nntpserver.repository

NNTPRepositoryImpl.java

/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 ****************************************************************/

package org.apache.james.nntpserver.repository;

import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.container.ContainerUtil;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.context.ContextException;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.james.context.AvalonContextUtilities;
import org.apache.james.nntpserver.DateSinceFileFilter;
import org.apache.james.nntpserver.NNTPException;
import org.apache.james.util.io.AndFileFilter;
import org.apache.james.util.io.DirectoryFileFilter;
import org.apache.oro.io.GlobFilenameFilter;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * NNTP Repository implementation.
 */
public class NNTPRepositoryImpl extends AbstractLogEnabled 
    implements NNTPRepository, Contextualizable, Configurable, Initializable {

    /**
     * The context employed by this repository
     */
    private Context context;

    /**
     * The configuration employed by this repository
     */
    private Configuration configuration;

    /**
     * Whether the repository is read only
     */
    private boolean readOnly;

    /**
     * The groups are located under this path.
     */
    private File rootPath;

    /**
     * Articles are temporarily written here and then sent to the spooler.
     */
    private File tempPath;

    /**
     * The spooler for this repository.
     */
    private NNTPSpooler spool;

    /**
     * The article ID repository associated with this NNTP repository.
     */
    private ArticleIDRepository articleIDRepo;

    /**
     * A map to allow lookup of valid newsgroup names
     */
    private HashMap groupNameMap = null;

    /**
     * Restrict use to newsgroups specified in config only
     */
    private boolean definedGroupsOnly = false;

    /**
     * The root path as a String.
     */
    private String rootPathString = null;

    /**
     * The temp path as a String.
     */
    private String tempPathString = null;

    /**
     * The article ID path as a String.
     */
    private String articleIdPathString = null;

    /**
     * The domain suffix used for files in the article ID repository.
     */
    private String articleIDDomainSuffix = null;

    /**
     * The ordered list of fields returned in the overview format for
     * articles stored in this repository.
     */
    private String[] overviewFormat = { "Subject:",
                                        "From:",
                                        "Date:",
                                        "Message-ID:",
                                        "References:",
                                        "Bytes:",
                                        "Lines:"
                                      };

    /**
     * This is a mapping of group names to NNTP group objects.
     *
     * TODO: This needs to be addressed so it scales better
     */
    private HashMap repositoryGroups = new HashMap();

    /**
     * @see org.apache.avalon.framework.context.Contextualizable#contextualize(Context)
     */
    public void contextualize(Context context)
            throws ContextException {
        this.context = context;
    }

    /**
     * @see org.apache.avalon.framework.configuration.Configurable#configure(Configuration)
     */
    public void configure( Configuration aConfiguration ) throws ConfigurationException {
        configuration = aConfiguration;
        readOnly = configuration.getChild("readOnly").getValueAsBoolean(false);
        articleIDDomainSuffix = configuration.getChild("articleIDDomainSuffix")
            .getValue("foo.bar.sho.boo");
        rootPathString = configuration.getChild("rootPath").getValue(null);
        if (rootPathString == null) {
            throw new ConfigurationException("Root path URL is required.");
        }
        tempPathString = configuration.getChild("tempPath").getValue(null);
        if (tempPathString == null) {
            throw new ConfigurationException("Temp path URL is required.");
        }
        articleIdPathString = configuration.getChild("articleIDPath").getValue(null);
        if (articleIdPathString == null) {
            throw new ConfigurationException("Article ID path URL is required.");
        }
        if (getLogger().isDebugEnabled()) {
            if (readOnly) {
                getLogger().debug("NNTP repository is read only.");
            } else {
                getLogger().debug("NNTP repository is writeable.");
            }
            getLogger().debug("NNTP repository root path URL is " + rootPathString);
            getLogger().debug("NNTP repository temp path URL is " + tempPathString);
            getLogger().debug("NNTP repository article ID path URL is " + articleIdPathString);
        }
        Configuration newsgroupConfiguration = configuration.getChild("newsgroups");
        definedGroupsOnly = newsgroupConfiguration.getAttributeAsBoolean("only", false);
        groupNameMap = new HashMap();
        if ( newsgroupConfiguration != null ) {
            Configuration[] children = newsgroupConfiguration.getChildren("newsgroup");
            if ( children != null ) {
                for ( int i = 0 ; i < children.length ; i++ ) {
                    String groupName = children[i].getValue();
                    groupNameMap.put(groupName, groupName);
                }
            }
        }
        getLogger().debug("Repository configuration done");
    }

    /**
     * @see org.apache.avalon.framework.activity.Initializable#initialize()
     */
    public void initialize() throws Exception {

        getLogger().debug("Starting initialize");
        File articleIDPath = null;

        try {
            rootPath = AvalonContextUtilities.getFile(context, rootPathString);
            tempPath = AvalonContextUtilities.getFile(context, tempPathString);
            articleIDPath = AvalonContextUtilities.getFile(context, articleIdPathString);
        } catch (Exception e) {
            getLogger().fatalError(e.getMessage(), e);
            throw e;
        }

        if ( articleIDPath.exists() == false ) {
            articleIDPath.mkdirs();
        }

        articleIDRepo = new ArticleIDRepository(articleIDPath, articleIDDomainSuffix);
        spool = (NNTPSpooler)createSpooler();
        spool.setRepository(this);
        spool.setArticleIDRepository(articleIDRepo);
        if (getLogger().isDebugEnabled()) {
            getLogger().debug("repository:readOnly=" + readOnly);
            getLogger().debug("repository:rootPath=" + rootPath.getAbsolutePath());
            getLogger().debug("repository:tempPath=" + tempPath.getAbsolutePath());
        }

        if ( rootPath.exists() == false ) {
            rootPath.mkdirs();
        } else if (!(rootPath.isDirectory())) {
            StringBuffer errorBuffer =
                new StringBuffer(128)
                    .append("NNTP repository root directory is improperly configured.  The specified path ")
                    .append(rootPathString)
                    .append(" is not a directory.");
            throw new ConfigurationException(errorBuffer.toString());
        }

        Set groups = groupNameMap.keySet();
        Iterator groupIterator = groups.iterator();
        while( groupIterator.hasNext() ) {
            String groupName = (String)groupIterator.next();
            File groupFile = new File(rootPath,groupName);
            if ( groupFile.exists() == false ) {
                groupFile.mkdirs();
            } else if (!(groupFile.isDirectory())) {
                StringBuffer errorBuffer =
                    new StringBuffer(128)
                        .append("A file exists in the NNTP root directory with the same name as a newsgroup.  File ")
                        .append(groupName)
                        .append("in directory ")
                        .append(rootPathString)
                        .append(" is not a directory.");
                throw new ConfigurationException(errorBuffer.toString());
            }
        }
        if ( tempPath.exists() == false ) {
            tempPath.mkdirs();
        } else if (!(tempPath.isDirectory())) {
            StringBuffer errorBuffer =
                new StringBuffer(128)
                    .append("NNTP repository temp directory is improperly configured.  The specified path ")
                    .append(tempPathString)
                    .append(" is not a directory.");
            throw new ConfigurationException(errorBuffer.toString());
        }

        getLogger().debug("repository initialization done");
    }

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#isReadOnly()
     */
    public boolean isReadOnly() {
        return readOnly;
    }

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#getGroup(String)
     */
    public NNTPGroup getGroup(String groupName) {
        if (definedGroupsOnly && groupNameMap.get(groupName) == null) {
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(groupName + " is not a newsgroup hosted on this server.");
            }
            return null;
        }
        File groupFile = new File(rootPath,groupName);
        NNTPGroup groupToReturn = null;
        synchronized(this) {
            groupToReturn = (NNTPGroup)repositoryGroups.get(groupName);
            if ((groupToReturn == null) && groupFile.exists() && groupFile.isDirectory() ) {
                try {
                    groupToReturn = new NNTPGroupImpl(groupFile);
                    ContainerUtil.enableLogging(groupToReturn, getLogger());
                    ContainerUtil.contextualize(groupToReturn, context);
                    ContainerUtil.initialize(groupToReturn);
                    repositoryGroups.put(groupName, groupToReturn);
                } catch (Exception e) {
                    getLogger().error("Couldn't create group object.", e);
                    groupToReturn = null;
                }
            }
        }
        return groupToReturn;
    }

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#getArticleFromID(String)
     */
    public NNTPArticle getArticleFromID(String id) {
        try {
            return articleIDRepo.getArticle(this,id);
        } catch(Exception ex) {
            getLogger().error("Couldn't get article " + id + ": ", ex);
            return null;
        }
    }

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#createArticle(InputStream)
     */
    public void createArticle(InputStream in) {
        StringBuffer fileBuffer =
            new StringBuffer(32)
                    .append(System.currentTimeMillis())
                    .append(".")
                    .append(Math.random());
        File f = new File(tempPath, fileBuffer.toString());
        FileOutputStream fout = null;
        try {
            fout = new FileOutputStream(f);
            byte[] readBuffer = new byte[1024];
            int bytesRead = 0;
            while ( ( bytesRead = in.read(readBuffer, 0, 1024) ) > 0 ) {
                fout.write(readBuffer, 0, bytesRead);
            }
            fout.flush();
            fout.close();
            fout = null;
            boolean renamed = f.renameTo(new File(spool.getSpoolPath(),f.getName()));
            if (!renamed) {
                throw new IOException("Could not create article on the spool.");
            }
        } catch(IOException ex) {
            throw new NNTPException("create article failed",ex);
        } finally {
            if (fout != null) {
                try {
                    fout.close();
                } catch (IOException ioe) {
                    // Ignored
                }
            }
        }
    }

    class GroupFilter implements java.io.FilenameFilter {
        public boolean accept(java.io.File dir, String name) {
            if (getLogger().isDebugEnabled()) {
                getLogger().debug(((definedGroupsOnly ? groupNameMap.containsKey(name) : true) ? "Accepting ": "Rejecting") + name);
            }

            return definedGroupsOnly ? groupNameMap.containsKey(name) : true;
        }
    }

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#getMatchedGroups(String)
     */
    public Iterator getMatchedGroups(String wildmat) {
        File[] f = rootPath.listFiles(new AndFileFilter(new GroupFilter(), new AndFileFilter
            (new DirectoryFileFilter(),new GlobFilenameFilter(wildmat))));
        return getGroups(f);
    }

    /**
     * Gets an iterator of all news groups represented by the files
     * in the parameter array.
     *
     * @param f the array of files that correspond to news groups
     *
     * @return an iterator of news groups
     */
    private Iterator getGroups(File[] f) {
        List list = new ArrayList();
        for ( int i = 0 ; i < f.length ; i++ ) {
            if (f[i] != null) {
                list.add(getGroup(f[i].getName()));
            }
        }
        return list.iterator();
    }

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#getGroupsSince(Date)
     */
    public Iterator getGroupsSince(Date dt) {
        File[] f = rootPath.listFiles(new AndFileFilter(new GroupFilter(), new AndFileFilter
            (new DirectoryFileFilter(),new DateSinceFileFilter(dt.getTime()))));
        return getGroups(f);
    }

    // gets the list of groups.
    // creates iterator that concatenates the article iterators in the list of groups.
    // there is at most one article iterator reference for all the groups

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#getArticlesSince(Date)
     */
    public Iterator getArticlesSince(final Date dt) {
        final Iterator giter = getGroupsSince(dt);
        return new Iterator() {

                private Iterator iter = null;

                public boolean hasNext() {
                    if ( iter == null ) {
                        if ( giter.hasNext() ) {
                            NNTPGroup group = (NNTPGroup)giter.next();
                            iter = group.getArticlesSince(dt);
                        }
                        else {
                            return false;
                        }
                    }
                    if ( iter.hasNext() ) {
                        return true;
                    } else {
                        iter = null;
                        return hasNext();
                    }
                }

                public Object next() {
                    return iter.next();
                }

                public void remove() {
                    throw new UnsupportedOperationException("remove not supported");
                }
            };
    }

    /**
     * @see org.apache.james.nntpserver.repository.NNTPRepository#getOverviewFormat()
     */
    public String[] getOverviewFormat() {
        return overviewFormat;
    }

    /**
     * Creates an instance of the spooler class.
     *
     * TODO: This method doesn't properly implement the Avalon lifecycle.
     */
    private NNTPSpooler createSpooler() 
            throws ConfigurationException {
        String className = "org.apache.james.nntpserver.repository.NNTPSpooler";
        Configuration spoolerConfiguration = configuration.getChild("spool");
        try {
            // Must be a subclass of org.apache.james.nntpserver.repository.NNTPSpooler
            className = spoolerConfiguration.getAttribute("class");
        } catch(ConfigurationException ce) {
            // Use the default class.
        }
        try {
            Object obj = getClass().getClassLoader().loadClass(className).newInstance();
            // TODO: Need to support service
            ContainerUtil.enableLogging(obj, getLogger());
            ContainerUtil.contextualize(obj, context);
            ContainerUtil.configure(obj, spoolerConfiguration.getChild("configuration"));
            ContainerUtil.initialize(obj);
            return (NNTPSpooler)obj;
        } catch(ClassCastException cce) {
            StringBuffer errorBuffer =
                new StringBuffer(128)
                    .append("Spooler initialization failed because the spooler class ")
                    .append(className)
                    .append(" was not a subclass of org.apache.james.nntpserver.repository.NNTPSpooler");
            String errorString = errorBuffer.toString();
            getLogger().error(errorString, cce);
            throw new ConfigurationException(errorString, cce);
        } catch(Exception ex) {
            getLogger().error("Spooler initialization failed",ex);
            throw new ConfigurationException("Spooler initialization failed",ex);
        }
    }
}