org.atomserver.AtomServer.java Source code

Java tutorial

Introduction

Here is the source code for org.atomserver.AtomServer.java

Source

/* Copyright (c) 2007 HomeAway, Inc.
 *  All rights reserved.  http://www.atomserver.org
 *
 * Licensed 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.atomserver;

import org.apache.abdera.Abdera;
import org.apache.abdera.factory.Factory;
import org.apache.abdera.i18n.iri.IRISyntaxException;
import org.apache.abdera.model.*;
import org.apache.abdera.protocol.error.Error;
import org.apache.abdera.protocol.server.RequestContext;
import org.apache.abdera.protocol.server.ResponseContext;
import org.apache.abdera.protocol.server.impl.*;
import org.apache.abdera.util.EntityTag;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.atomserver.core.etc.AtomServerConstants;
import org.atomserver.exceptions.*;
import org.atomserver.ext.batch.Operation;
import org.atomserver.ext.batch.Results;
import org.atomserver.ext.batch.Status;
import org.atomserver.uri.EntryTarget;
import org.atomserver.uri.FeedTarget;
import org.atomserver.utils.IOCLog;
import org.atomserver.utils.perf.AtomServerPerfLogTagFormatter;
import org.atomserver.utils.perf.AtomServerStopWatch;
import org.perf4j.StopWatch;

import javax.servlet.http.HttpServletResponse;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * AtomServer - This class is the true entry point for an AtomServer web application - the AbderaServlet
 * ultimately delegates to this class. AtomServer is an Abdera Provider which delegates to an underlying AtomService.
 * It provides a basic wrapper to the standard Atom "control flow", which is explained briefly below;
 * <p/>
 * Central to the Atom Publishing Protocol is the concept of collections of editable resources that are represented
 * by Atom Feed and Entry documents. A collection has a unique URI. Issuing an HTTP GET request
 * to that URI returns an Atom Feed Document. To create new entries in that feed, clients either send HTTP POST
 * requests to the collection's URI, or alternately send HTTP PUT requests to the entries URI.
 * When POST-ed those newly created entries will be assigned their own unique entry Id (edit URI).
 * And when PUT, the newly created entries will be assigned the unique entry Id as determined from the URI.
 * To modify those entries, the client simply retrieves the resource from the collection, makes its modifications,
 * then puts it back. Removing the entry from the feed is a simple matter of issuing an HTTP DELETE request to the
 * appropriate edit URI. All operations are performed using simple HTTP requests and can usually be performed
 * with nothing more than a simple text editor and a command prompt.
 * <p/>
 * <p/>
 * <pre>
 * HTTP STATUS CODES
 * Code                                     Explanation
 * 200 OK                        No error.
 * 201 CREATED                    Creation of a resource was successful.
 * 304 NOT MODIFIED              The resource hasn't changed since the time specified in the request's If-Modified-Since header.
 * 400 BAD REQUEST                Invalid request URI or header, or unsupported nonstandard parameter.
 * 401 UNAUTHORIZED            Authorization required.
 * 403 FORBIDDEN                Unsupported standard parameter, or authentication or authorization failed.
 * 404 NOT FOUND                Resource (such as a feed or entry) not found.
 * 409 CONFLICT                 Specified version number doesn't match resource's latest version number.
 * 422 BAD CONTENT              The Entry <content> is not valid
 * 500 INTERNAL SERVER ERROR    Internal error. This is the default code that is used for all unrecognized errors.
 * </pre>
 * <p/>
 *
 * @author Chris Berry  (chriswberry at gmail.com)
 * @author Bryon Jacob (bryon at jacob.net)
 */
public class AtomServer extends AbstractProvider {

    // ------------
    //   statics 
    //--------------
    static protected Log logger = LogFactory.getLog(AtomServer.class);
    private static final Pattern BATCH_ENTRY_PATTERN = Pattern.compile("/([^/#?]+)/([^/#?]+)/\\$batch");
    private static int DEFAULT_PAGE_SIZE = 100;

    static public Factory getFactory(Abdera abdera) {
        return new org.apache.abdera.parser.stax.FOMFactory(abdera);
    }

    // ------------
    //   instance
    //--------------
    private EntityTag service_etag = new EntityTag("service");
    private AtomService atomService;
    private IOCLog errlog;

    /**
     * Set the REQUIRED AtomService for use by this AtomServer. This method is meant to
     * inject an AtomService which has been configured externally in an IOC container like Spring.
     *
     * @param atomService Value to set
     */
    public void setService(AtomService atomService) {
        if (logger.isDebugEnabled()) {
            logger.debug("AtomServer.setService: service= " + atomService);
        }
        this.atomService = atomService;
    }

    protected AtomService getAtomService() {
        return atomService;
    }

    /**
     * Set the optional "500 Error" log. If present a logger will log all Server Error (i.e. 500 Errors)
     * to a special log, which makes them much easier to find than trolling through the log of
     * standard output, access logs, etc. In addition, the errors are logged with much more detail that they are
     * when logged to standard out.  This method is meant to
     * inject an IOCLog which has been configured externally in an IOC container like Spring.
     * It is in this external configuration that you specify such details as the logger name, which, in turn,
     * will define the actual name of the error log file.
     *
     * @param errlog Value to set
     */
    public void setErrorLog(IOCLog errlog) {
        if (logger.isDebugEnabled()) {
            logger.debug("AtomServer.setErrorLog: errlog= " + errlog);
        }
        this.errlog = errlog;
    }

    /**
     * Called for a HTTP GET to an SERVICE URL.
     * <p/>
     * The first step to using any APP-enabled service is to determine what collections are available
     * and what types of resources those collections can contain.
     * The Atom protocol specification defines an XML format known as a service document that
     * a client can use to introspect an endpoint. This method returns that document
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext getService(RequestContext request) {
        StopWatch stopWatch = new AtomServerStopWatch();
        if (logger.isInfoEnabled()) {
            logger.info("GET Service:: [ " + request.getUri() + " ]");
        }
        Abdera abdera = request.getServiceContext().getAbdera();
        try {
            Factory factory = AtomServer.getFactory(abdera);

            org.apache.abdera.model.Service service = factory.newService();

            java.util.Collection<String> workspaces = this.atomService.listWorkspaces(request);
            if (logger.isTraceEnabled()) {
                logger.trace("AtomServer.getService:: workspaces= " + workspaces);
            }
            if (workspaces == null) {
                throw new BadRequestException(
                        "The workspace indicated in the Service Request: " + request.getUri() + " does NOT exist");
            }

            for (String wsName : workspaces) {
                Workspace workspace = service.addWorkspace(wsName);

                AtomWorkspace atomWorkspace = this.atomService.getAtomWorkspace(wsName);
                java.util.Collection<Collection> collections = atomWorkspace.listCollections(request);
                if (logger.isTraceEnabled()) {
                    logger.trace("AtomServer.getService:: [" + wsName + "] collections= " + collections);
                }

                if (collections == null) {
                    return servererror(abdera, request, "The collections was null", null);
                } else {
                    for (Collection coll : collections) {
                        try {
                            String collName = coll.getTitle();
                            org.apache.abdera.model.Collection collection = workspace.addCollection(collName,
                                    wsName + '/' + coll.getTitle() + '/');
                            collection.setAccept("entry");

                            java.util.Collection<Category> categoryList = atomWorkspace.getAtomCollection(collName)
                                    .listCategories(request);
                            if (logger.isTraceEnabled()) {
                                logger.trace("AtomServer.getService:: [" + wsName + ", " + collName
                                        + "] categoryList= " + categoryList);
                            }

                            if (categoryList != null) {
                                Categories categories = collection.addCategories();
                                for (Category category : categoryList) {
                                    categories.addCategory(category);
                                }
                            } else {
                                collection.addCategories().setFixed(false);
                            }

                        } catch (IRISyntaxException e) {
                            throw new BadRequestException(e);
                        }
                    }
                }
            }
            Document<org.apache.abdera.model.Service> doc = service.getDocument();
            BaseResponseContext rc = new BaseResponseContext<Document<org.apache.abdera.model.Service>>(doc);
            rc.setEntityTag(service_etag);
            return rc;
        } catch (Throwable e) {
            return handleTopLevelException(e, abdera, request);
        } finally {
            stopWatch.stop("GET.service", request.getUri().getPath());
        }
    }

    /**
     * Called for a HTTP GET to an FEED URL.
     * <p/>
     * The Atom protocol specification defines an XML format known as a feed document.
     * This method returns that document, responding to a HTTP GET. The URL defines precisely
     * which Atom workspace and collection to return.
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext getFeed(RequestContext request) {
        StopWatch stopWatch = new AtomServerStopWatch();
        if (logger.isInfoEnabled()) {
            logger.info("GET Feed:: [ " + request.getUri() + " ]");
        }
        Abdera abdera = request.getServiceContext().getAbdera();
        try {
            FeedTarget feedTarget = atomService.getURIHandler().getFeedTarget(request);
            Feed feed = atomService.getAtomWorkspace(feedTarget.getWorkspace())
                    .getAtomCollection(feedTarget.getCollection()).getEntries(request);

            StopWatch stopWatch2 = new AtomServerStopWatch();
            try {
                if (feed == null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("AtomServer.getFeed() THE FEED IS NOT MODIFIED -- RETURNING 304");
                    }
                    return notmodified(abdera, request,
                            "No Entries were found modified since " + request.getIfModifiedSince());
                } else {
                    Document<Feed> doc = feed.getDocument();
                    BaseResponseContext rc = new BaseResponseContext<Document<Feed>>(doc);
                    try {
                        rc.setEntityTag(new EntityTag(feed.getId().toString()));
                    } catch (IRISyntaxException e) {
                        throw new BadRequestException(e);
                    }
                    return rc;
                }
            } finally {
                stopWatch2.stop("AS.buildResp.GET.feed", "");
            }
        } catch (Throwable e) {
            return handleTopLevelException(e, abdera, request);
        } finally {
            stopWatch.stop("GET.feed", request.getUri().getPath());
        }
    }

    /**
     * Called for a HTTP GET to an ENTRY URL.
     * <p/>
     * The Atom protocol specification defines an XML format known as a entry document.
     * This method returns that document, responding to an HTTP GET.  The URL defines precisely
     * which Atom workspace and collection to return.
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above
     */
    public ResponseContext getEntry(RequestContext request) {
        StopWatch stopWatch = new AtomServerStopWatch();
        if (logger.isInfoEnabled()) {
            logger.info("GET Entry:: [ " + request.getUri() + " ]");
        }
        Abdera abdera = request.getServiceContext().getAbdera();
        try {
            EntryTarget entryTarget = atomService.getURIHandler().getEntryTarget(request, false);
            Entry entry = atomService.getAtomWorkspace(entryTarget.getWorkspace())
                    .getAtomCollection(entryTarget.getCollection()).getEntry(request);

            StopWatch stopWatch2 = new AtomServerStopWatch();
            try {
                if (entry == null) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("AtomServer.getEntry() THE ENTRY IS NOT MODIFIED -- RETURNING 304");
                    }
                    return notmodified(abdera, request,
                            "No Entry was found modified since " + request.getIfModifiedSince());
                } else {
                    Document<Entry> doc = entry.getDocument();
                    BaseResponseContext rc = new BaseResponseContext<Document<Entry>>(doc);
                    try {
                        rc.setEntityTag(new EntityTag(entry.getId().toString()));
                    } catch (IRISyntaxException e) {
                        throw new BadRequestException(e);
                    }
                    return rc;
                }
            } finally {
                stopWatch2.stop("AS.buildResp.GET.entry",
                        AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryTarget));
            }
        } catch (Throwable e) {
            return handleTopLevelException(e, abdera, request);
        } finally {
            stopWatch.stop("GET.entry", request.getUri().getPath());
        }
    }

    /**
     * Called for a HTTP POST to a FEED URL
     * <p/>
     * The Atom protocol specification defines an XML format known as a entry document.
     * This method accepts AND returns that document, responding to an HTTP POST.  The URL defines precisely
     * which Atom entry to create (i.e which workspace, collection, and entry).
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above. (201 indicates a successful creation)
     */
    public ResponseContext createEntry(RequestContext request) {
        StopWatch stopWatch = new AtomServerStopWatch();
        if (logger.isInfoEnabled()) {
            logger.info("POST Entry:: [ " + request.getUri() + " ]");
        }
        Abdera abdera = request.getServiceContext().getAbdera();
        try {
            return handleSingleEntry(request, abdera);
        } finally {
            stopWatch.stop("POST.entry", request.getUri().getPath());
        }
    }

    /**
     * Called for a HTTP DELETE to an ENTRY URL.
     * <p/>
     * The URL defines precisely which Atom entry to delete. The semantics of what actually happens when
     * you delete an entry are, of course, undefined. In the default version of AtomServer (i.e. using
     * the DBBasedAtomService), we do not destroy the actual entry (i.e. remove the corresponding row from
     * the database). Instead we simply mark it as deleted, so that Feed consumers can be alerted to the entries
     * new state.
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext deleteEntry(RequestContext request) {
        StopWatch stopWatch = new AtomServerStopWatch();
        if (logger.isInfoEnabled()) {
            logger.info("DELETE Entry:: [ " + request.getUri() + " ]");
        }
        Abdera abdera = request.getServiceContext().getAbdera();
        try {

            EntryTarget entryTarget = atomService.getURIHandler().getEntryTarget(request, false);
            Entry entry = atomService.getAtomWorkspace(entryTarget.getWorkspace())
                    .getAtomCollection(entryTarget.getCollection()).deleteEntry(request);

            StopWatch stopWatch2 = new AtomServerStopWatch();
            try {
                AbstractResponseContext rc = null;
                if (entry == null) {
                    rc = new EmptyResponseContext(200);
                } else {
                    Document<Entry> doc = entry.getDocument();
                    rc = new BaseResponseContext<Document<Entry>>(doc);
                    rc.setEntityTag(new EntityTag(entry.getId().toString()));
                }
                return rc;

            } finally {
                stopWatch2.stop("AS.buildResp.DELETE",
                        AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryTarget));
            }

        } catch (Throwable e) {
            return handleTopLevelException(e, abdera, request);
        } finally {
            stopWatch.stop("DELETE.entry", request.getUri().getPath());
        }
    }

    /**
     * Called for a HTTP PUT to an ENTRY URL.
     * <p/>
     * The Atom protocol specification defines an XML format known as a entry document.
     * This method accepts AND returns that document, responding to an HTTP PUT.  The URL defines precisely
     * which Atom workspace and collection to create. This method is used for EITHER document creation or update.
     * <p/>
     * The essential difference between using this method (PUT) and using the POST is whether the document is
     * truly owned by the AtomServer or not. Often a document is actually created by some other system and,
     * in turn, labeled (i.e. given an Id) by that system. So the default version of AtomServer (i.e. using
     * the DBBasedAtomService) allows you to PUT that entry as you would PUT any entry, and then determines under
     * the covers whether this is a create or modify operation.
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above. Will return a status code of 201 if the document was successfully created,
     *         and otherwise, 200 for success.
     */
    public ResponseContext updateEntry(RequestContext request) {
        StopWatch stopWatch = new AtomServerStopWatch();
        if (logger.isInfoEnabled()) {
            logger.info("PUT Entry:: [ " + request.getUri() + " ]");
        }
        String relativeUri = atomService.getServiceBaseUri() == null ? request.getUri().toString()
                : request.getUri().toString().replace(atomService.getServiceBaseUri(), "").replaceAll("\\/+", "/");

        Abdera abdera = request.getServiceContext().getAbdera();

        Matcher matcher = BATCH_ENTRY_PATTERN.matcher(relativeUri);
        try {
            if (matcher.matches()) {
                return handleBatch(request, abdera);
            } else {
                return handleSingleEntry(request, abdera);
            }
        } finally {
            stopWatch.stop("PUT.entry", request.getUri().getPath());
        }
    }

    /**
     * Returns the default page size
     *
     * @return the default page size
     */
    public int getDefaultPageSize() {
        return DEFAULT_PAGE_SIZE;
    }

    //====================================
    //  Methods Not Currently Implemented
    //====================================

    /**
     * Called for a HTTP POST to an ENTRY URL.
     * NOT CURRENTLY IMPLEMENTED
     * <p/>
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext entryPost(RequestContext request) {
        return notImplemented(request, "POST to Entry");
    }

    /**
     * Called for a HTTP GET for a Categories document.
     * NOT CURRENTLY IMPLEMENTED
     * <p/>
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext getCategories(RequestContext request) {
        return notImplemented(request, "GET categories");
    }

    /**
     * Called for a HTTP PUT to an ENTRY URL, where the Content type is some media.
     * NOT CURRENTLY IMPLEMENTED
     * <p/>
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext updateMedia(RequestContext request) {
        return notImplemented(request, "PUT media");
    }

    /**
     * Called for a HTTP DELETE to an ENTRY URL, where the Content type is some media.
     * NOT CURRENTLY IMPLEMENTED
     * <p/>
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext deleteMedia(RequestContext request) {
        return notImplemented(request, "DELETE media");
    }

    /**
     * Called for a HTTP GET to an ENTRY URL, where the Content type is some media.
     * NOT CURRENTLY IMPLEMENTED
     * <p/>
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext getMedia(RequestContext request) {
        return notImplemented(request, "GET media");
    }

    /**
     * Called for a HTTP POST to an ENTRY URL,, where the Content type is some media.
     * NOT CURRENTLY IMPLEMENTED
     * <p/>
     *
     * @param request The Abdera RequestContext
     * @return response The Abdera ResponseContext, which will contain the appropriate HTTP status code,
     *         as detailed above.
     */
    public ResponseContext mediaPost(RequestContext request) {
        return notImplemented(request, "POST media");
    }

    private ResponseContext handleSingleEntry(RequestContext request, Abdera abdera) {
        try {
            EntryTarget entryTarget = atomService.getURIHandler().getEntryTarget(request, false);
            UpdateCreateOrDeleteEntry.CreateOrUpdateEntry uEntry = atomService
                    .getAtomWorkspace(entryTarget.getWorkspace()).getAtomCollection(entryTarget.getCollection())
                    .updateEntry(request);

            StopWatch stopWatch = new AtomServerStopWatch();
            try {
                Entry entry = uEntry.getEntry();
                Document<Entry> doc = entry.getDocument();
                AbstractResponseContext rc = new BaseResponseContext<Document<Entry>>(doc);
                rc.setEntityTag(new EntityTag(entry.getId().toString()));

                if (uEntry.isNewlyCreated()) {
                    rc.setStatus(201);
                }
                rc.setLocation(entry.getEditLinkResolvedHref().toString());
                return rc;

            } finally {
                stopWatch.stop("AS.buildResp.PUT.entry",
                        AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryTarget));
            }

        } catch (Throwable e) {
            return handleTopLevelException(e, abdera, request);
        }
    }

    private ResponseContext handleBatch(RequestContext request, Abdera abdera) {

        final Factory factory = getFactory(abdera);

        try {
            EntryTarget entryTarget = atomService.getURIHandler().getEntryTarget(request, false);
            java.util.Collection<UpdateCreateOrDeleteEntry> updateEntries = atomService
                    .getAtomWorkspace(entryTarget.getWorkspace()).getAtomCollection(entryTarget.getCollection())
                    .updateEntries(request);

            StopWatch stopWatch = new AtomServerStopWatch();
            try {
                Document<Feed> doc = factory.newFeed().getDocument();

                int inserts = 0, updates = 0, deletes = 0, errors = 0;
                for (UpdateCreateOrDeleteEntry updateOrDeleteEntry : updateEntries) {
                    Entry entry = updateOrDeleteEntry.getEntry();

                    Operation operation = factory.newExtensionElement(AtomServerConstants.OPERATION);
                    entry.addExtension(operation);

                    operation.setType(updateOrDeleteEntry.isDeleted() ? "delete"
                            : updateOrDeleteEntry.isNewlyCreated() ? "insert" : "update");
                    if (updateOrDeleteEntry.getException() == null) {

                        Status status = factory.newExtensionElement(AtomServerConstants.STATUS);
                        entry.addExtension(status);

                        status.setCode(updateOrDeleteEntry.isNewlyCreated() ? "201" : "200");
                        status.setReason(updateOrDeleteEntry.isNewlyCreated() ? "CREATED" : "OK");

                        if (updateOrDeleteEntry.isDeleted()) {
                            deletes++;
                        } else if (updateOrDeleteEntry.isNewlyCreated()) {
                            inserts++;
                        } else {
                            updates++;
                        }
                    } else {
                        handleBatchItemException(updateOrDeleteEntry.getException(), abdera, request, entry);
                        errors++;
                    }
                    doc.getRoot().addEntry(entry);
                }

                Results results = factory.newExtensionElement(AtomServerConstants.RESULTS);
                doc.getRoot().addExtension(results);
                results.setInserts(inserts);
                results.setUpdates(updates);
                results.setDeletes(deletes);
                results.setErrors(errors);

                BaseResponseContext<Document<Feed>> responseContext = new BaseResponseContext<Document<Feed>>(doc);
                responseContext.setStatus(200);
                responseContext.setStatusText("BATCH OK");
                return responseContext;
            } finally {
                stopWatch.stop("AS.buildResp.PUT.Batch",
                        AtomServerPerfLogTagFormatter.getPerfLogEntryString(entryTarget));
            }

        } catch (Throwable e) {
            return handleTopLevelException(e, abdera, request);
        }
    }

    //====================================
    //  Special HTTP Error Codes/Returns 
    //====================================

    /**
     * Return a 403 FORBIDDEN error when a method has not yet been implemented
     * <p/>
     *
     * @param request The Abdera RequestContext
     * @param details Details concerning the error
     * @return response The Abdera ResponseContext, which will contain the 403 HTTP status code.
     */
    private ResponseContext notImplemented(RequestContext request, String details) {
        Abdera abdera = request.getServiceContext().getAbdera();
        String msg = "NOT IMPLEMENTED :: (" + details + ") :: " + request.getUri();
        logger.error(msg);
        return forbidden(abdera, request, msg);
    }

    /**
     * Return a 422 BAD CONTENT error
     * <p/>
     *
     * @param abdera The Abdera Instance
     * @param reason Details concerning the error
     * @return response The Abdera ResponseContext, which will contain the 422 HTTP status code.
     */
    protected ResponseContext badcontent(Abdera abdera, String reason) {
        return returnBase(createErrorDocument(abdera, 422, reason, null), 422, null);
    }

    /**
     * Return a 409 CONFLICT error when an optimistic concurrency error occurs
     * <p/>
     *
     * @param abdera  The Abdera Instance
     * @param reason  Details concerning the error
     * @param editURI The edit URI that user should use to access the Entry.
     * @return response The Abdera ResponseContext, which will contain the 409 HTTP status code.
     */
    protected ResponseContext optimisticConcurrencyError(Abdera abdera, String reason, String editURI) {
        return returnBase(createOptimisticConcurrencyErrorDocument(abdera, reason, editURI), 409, null);
    }

    /**
     * Creates the a 409 Conflict error, when an optimistic concurrency error occurs,
     * which looks something like this;
     * <pre>
     *  <error xmlns="http://incubator.apache.org/abdera">
     *    <code>409</code>
     *    <message>Optimisitic Concurrency Error:: /atomserver/v1/widgets/acme/12345.en.xml/2</message>
     *    <link xmlns="http://www.w3.org/2005/Atom" href="/atomserver/v1/widgets/acme/12345.en.xml/1" rel="edit" />
     *  </error>
     * </pre>
     * <p/>
     *
     * @param abdera  The Abdera Instance
     * @param message Details concerning the error
     * @param editURI The edit URI that user should use to access the Entry.
     * @return error The Abdera Error, as shown above.
     */
    protected Error createOptimisticConcurrencyErrorDocument(Abdera abdera, String message, String editURI) {

        Error error = Error.create(abdera, 409, ((message != null) ? message : ""));

        // create an Atom edit link, note that we must add it as an Extension
        //  so that we can access it later (in JUnits, etc)
        Factory factory = AtomServer.getFactory(abdera);
        Link link = factory.newLink();
        link.setHref(editURI);
        link.setRel("edit");

        if (logger.isDebugEnabled()) {
            logger.debug("link = " + link);
        }

        error.addExtension(link);
        return error;
    }

    /**
     * Return a 301 MOVED PERMANANTLY error
     * <p/>
     *
     * @param abdera The Abdera Instance
     * @param reason Details concerning the error
     * @param altURI The edit URI that user should use to access the Entry.
     * @return response The Abdera ResponseContext, which will contain the 301 HTTP status code.
     */
    protected ResponseContext movedPermanently(Abdera abdera, String reason, String altURI) {
        return returnBase(createMovedPermanentlyErrorDocument(abdera, reason, altURI), 301, null);
    }

    /**
     * Creates the a 301 Moved Permanently error, which looks something like this;
     * <pre>
     *  <error xmlns="http://incubator.apache.org/abdera">
     *    <code>301</code>
     *    <message>Moved Permanently Error:: /foobar/v1/tags:widgets/acme</message>
     *    <link xmlns="http://www.w3.org/2005/Atom" href="/foobar/v1/widgets/acme" rel="alternate" />
     *  </error>
     * </pre>
     *
     * @param abdera  The Abdera Instance
     * @param message Details concerning the error
     * @param altURI  The URI that user should use to access the Entry.
     * @return error The Abdera Error, as shown above.
     */
    protected Error createMovedPermanentlyErrorDocument(Abdera abdera, String message, String altURI) {
        Error error = Error.create(abdera, 301, ((message != null) ? message : ""));

        // create an Atom alternative link, note that we must add it as an Extension
        //  so that we can access it later (in JUnits, etc)
        Factory factory = AtomServer.getFactory(abdera);

        Link link = factory.newLink();
        link.setHref(altURI);
        link.setRel("alternate");

        if (logger.isDebugEnabled()) {
            logger.debug("link = " + link);
        }

        error.addExtension(link);
        return error;
    }

    protected ResponseContext servererror(Abdera abdera, RequestContext requestContext, String s,
            Throwable throwable) {
        return super.servererror(abdera, requestContext, s, throwable);
    }

    //====================================
    // Handle top-level exceptions
    //====================================

    private interface TopLevelExceptionHandler<T extends Throwable> {
        ResponseContext handle(T exception, Abdera abdera, RequestContext request);
    }

    private final Map<Class<? extends Throwable>, TopLevelExceptionHandler> exceptionHandlers = new HashMap<Class<? extends Throwable>, TopLevelExceptionHandler>();

    {
        TopLevelExceptionHandler<EntryNotFoundException> entryNotFoundHandler = new TopLevelExceptionHandler<EntryNotFoundException>() {
            public ResponseContext handle(EntryNotFoundException exception, Abdera abdera, RequestContext request) {
                String message = MessageFormat.format("Unknown Entry:: {0}\nReason:: {1}", request.getUri(),
                        exception.getMessage());
                ResponseContext response = unknown(abdera, request, message);
                if (response instanceof BaseResponseContext) {
                    ((BaseResponseContext) response).setStatusText(message);
                }
                return response;
            }
        };
        exceptionHandlers.put(EntryNotFoundException.class, entryNotFoundHandler);

        TopLevelExceptionHandler<BadRequestException> badRequestHandler = new TopLevelExceptionHandler<BadRequestException>() {
            public ResponseContext handle(BadRequestException exception, Abdera abdera, RequestContext request) {
                String message = MessageFormat.format("Bad Request:: {0}\nReason:: {1}", request.getUri(),
                        exception.getMessage());
                ResponseContext response = badrequest(abdera, request, message);
                if (response instanceof BaseResponseContext) {
                    ((BaseResponseContext) response).setStatusText(message);
                }
                return response;
            }
        };
        exceptionHandlers.put(BadRequestException.class, badRequestHandler);
        exceptionHandlers.put(MalformedURLException.class, badRequestHandler);
        exceptionHandlers.put(URISyntaxException.class, badRequestHandler);

        TopLevelExceptionHandler<BadContentException> badContentHandler = new TopLevelExceptionHandler<BadContentException>() {
            public ResponseContext handle(BadContentException exception, Abdera abdera, RequestContext request) {
                String message = MessageFormat.format("Bad Content:: {0}\nReason:: {1}", request.getUri(),
                        exception.getMessage());
                ResponseContext response = badcontent(abdera, message);
                if (response instanceof BaseResponseContext) {
                    ((BaseResponseContext) response).setStatusText(message);
                }
                return response;
            }
        };
        exceptionHandlers.put(BadContentException.class, badContentHandler);

        TopLevelExceptionHandler<MovedPermanentlyException> movedPermanentlyHandler = new TopLevelExceptionHandler<MovedPermanentlyException>() {
            public ResponseContext handle(MovedPermanentlyException exception, Abdera abdera,
                    RequestContext request) {
                String message = MessageFormat.format("Moved Permanently:: {0}\nReason:: {1}", request.getUri(),
                        exception.getMessage());
                ResponseContext response = movedPermanently(abdera, message, exception.getAlternateURI());
                if (response instanceof BaseResponseContext) {
                    ((BaseResponseContext) response).setStatusText(message);
                    ((BaseResponseContext) response).setLocation(exception.getAlternateURI());
                }
                return response;
            }
        };
        exceptionHandlers.put(MovedPermanentlyException.class, movedPermanentlyHandler);

        TopLevelExceptionHandler<OptimisticConcurrencyException> optimisticConcurrencyHandler = new TopLevelExceptionHandler<OptimisticConcurrencyException>() {
            public ResponseContext handle(OptimisticConcurrencyException exception, Abdera abdera,
                    RequestContext request) {
                String message = MessageFormat.format("Optimisitic Concurrency Error:: {0}", request.getUri());
                ResponseContext response = optimisticConcurrencyError(abdera, message, exception.getEditURI());
                if (response instanceof BaseResponseContext) {
                    ((BaseResponseContext) response).setStatusText(message);
                }
                return response;
            }
        };
        exceptionHandlers.put(OptimisticConcurrencyException.class, optimisticConcurrencyHandler);
    }

    /**
     * Handle a top-level Exception when using batched operations. This method delegates to handleTopLevelException.
     *
     * @param e       The Throwable that got you here.
     * @param abdera  The Abdera Instance
     * @param request The Abdera RequestContext
     * @param entry   The Entry that threw the Exception
     */
    protected void handleBatchItemException(Throwable e, Abdera abdera, RequestContext request, Entry entry) {
        final ResponseContext responseContext = handleTopLevelException(e, abdera, request);
        Status status = getFactory(abdera).newExtensionElement(AtomServerConstants.STATUS);
        entry.addExtension(status);
        status.setCode(String.valueOf(responseContext.getStatus()));
        status.setReason(responseContext.getStatusText());
    }

    /**
     * Handle a top-level Exception. This is the funnel point for Exception handling in the AtomServer.
     * It allows a single entry point for the AtomServer methods, which provides maximal code reuse,
     * and insures that, for example all 500 errors get logged specially.
     *
     * @param e       The Throwable that got you here.
     * @param abdera  The Abdera Instance
     * @param request The Abdera RequestContext
     */
    protected ResponseContext handleTopLevelException(Throwable e, Abdera abdera, RequestContext request) {
        TopLevelExceptionHandler exceptionHandler = exceptionHandlers.get(e.getClass());
        String message = MessageFormat.format("Unknown Error:: {0}\nReason:: {1}", request.getUri(),
                e.getMessage());

        ResponseContext response = null;

        // TODO: Refactor exception handling
        // Check for TooMuchDataException intercepted in ServletInputStream.
        if (isTooMuchDataException(e)) {

            response = tooMuchDataError(abdera, request);

        } else if (exceptionHandler == null) {
            response = servererror(abdera, request, message, e);
            if (response instanceof BaseResponseContext) {
                ((BaseResponseContext) response).setStatusText(message);
            }

            log500Error(e, abdera, request);

            // These Exceptions have probably been thrown all the way to here
            //  Thus, it has most likely NOT been logged to the "standard log" below
            logger.error(message, e);

        } else {
            if (e instanceof EntryNotFoundException) {
                logger.warn(("Error for [" + request.getUri() + "] Cause: " + e.getMessage()));
            } else {
                logger.error(("Error for [" + request.getUri() + "] Cause: " + e.getMessage()), e);
            }
            response = exceptionHandler.handle(e, abdera, request);
        }
        return response;
    }

    private boolean isTooMuchDataException(Throwable e) {
        Throwable cause = e.getCause();
        while (cause != null && !(cause instanceof TooMuchDataException)) {
            cause = cause.getCause();
        }
        return (cause != null);
    }

    private ResponseContext tooMuchDataError(Abdera abdera, RequestContext request) {
        String msg = "TOO MUCH DATA :: (Content length exceeds the maximum length allowed.) :: " + request.getUri();
        logger.error(msg);
        return returnBase(createErrorDocument(abdera, HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, msg, null),
                HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE, null);
    }

    static private final int MAX_REQ_BODY_DUMP = 500;

    private void log500Error(Throwable e, Abdera abdera, RequestContext request) {
        if (errlog != null) {
            Log http500log = errlog.getLog();
            if (http500log.isInfoEnabled()) {
                try {
                    http500log.info(
                            "%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%");
                    http500log.info("==> 500 ERROR occurred for {" + request.getUri() + "} Type:: "
                            + e.getClass().getName() + " Reason:: " + e.getMessage());
                    http500log.error("500 Error:: ", e);
                    http500log.info("METHOD:: " + request.getMethod());
                    if (request.getPrincipal() != null) {
                        http500log.info("PRINCIPAL:: " + request.getPrincipal().getName());
                    }
                    http500log.info("HEADERS:: ");
                    String[] headerNames = request.getHeaderNames();
                    if (headerNames != null) {
                        for (int ii = 0; ii < headerNames.length; ii++) {
                            http500log
                                    .info("Header(" + headerNames[ii] + ")= " + request.getHeader(headerNames[ii]));
                        }
                    }
                    http500log.info("PARAMETERS:: ");
                    String[] paramNames = request.getParameterNames();
                    if (paramNames != null) {
                        for (int ii = 0; ii < paramNames.length; ii++) {
                            http500log.info(
                                    "Parameter(" + paramNames[ii] + ")= " + request.getParameter(paramNames[ii]));
                        }
                    }
                    if (request instanceof HttpServletRequestContext) {
                        HttpServletRequestContext httpRequest = (HttpServletRequestContext) request;
                        javax.servlet.http.HttpServletRequest servletRequest = httpRequest.getRequest();
                        if (servletRequest != null) {
                            http500log.info("QUERY STRING::" + servletRequest.getQueryString());
                            http500log.info("AUTH TYPE:: " + servletRequest.getAuthType());
                            http500log.info("REMOTE USER:: " + servletRequest.getRemoteUser());
                            http500log.info("REMOTE ADDR:: " + servletRequest.getRemoteAddr());
                            http500log.info("REMOTE HOST:: " + servletRequest.getRemoteHost());
                        }
                    }
                    http500log.info("BODY:: ");
                    if (request.getDocument() != null) {
                        java.io.StringWriter stringWriter = new java.io.StringWriter();
                        request.getDocument().writeTo(abdera.getWriterFactory().getWriter("PrettyXML"),
                                stringWriter);

                        //http500log.info( "\n" + stringWriter.toString() );
                        String requestString = stringWriter.toString();
                        if (requestString != null && requestString.length() > MAX_REQ_BODY_DUMP) {
                            requestString = requestString.substring(0, (MAX_REQ_BODY_DUMP - 1));
                        }

                        http500log.info("\n" + requestString);
                    }
                } catch (Exception ee) {
                }
            }
        }
    }
}