org.osaf.cosmo.atom.provider.ItemCollectionAdapter.java Source code

Java tutorial

Introduction

Here is the source code for org.osaf.cosmo.atom.provider.ItemCollectionAdapter.java

Source

/*
 * Copyright 2007 Open Source Applications Foundation
 * 
 * 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.osaf.cosmo.atom.provider;

import java.io.IOException;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;

import javax.activation.MimeType;

import net.fortuna.ical4j.data.ParserException;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.TimeZone;
import net.fortuna.ical4j.model.parameter.Value;
import net.fortuna.ical4j.util.Dates;

import org.apache.abdera.model.Content;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;
import org.apache.abdera.parser.ParseException;
import org.apache.abdera.protocol.server.ProviderHelper;
import org.apache.abdera.protocol.server.RequestContext;
import org.apache.abdera.protocol.server.ResponseContext;
import org.apache.abdera.protocol.server.context.AbstractResponseContext;
import org.apache.abdera.util.Constants;
import org.apache.abdera.util.EntityTag;
import org.apache.abdera.util.MimeTypeHelper;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.osaf.cosmo.atom.AtomConstants;
import org.osaf.cosmo.atom.InsufficientPrivilegesException;
import org.osaf.cosmo.atom.UidConflictException;
import org.osaf.cosmo.atom.generator.BaseItemFeedGenerator;
import org.osaf.cosmo.atom.generator.GeneratorException;
import org.osaf.cosmo.atom.generator.ItemFeedGenerator;
import org.osaf.cosmo.atom.generator.UnsupportedFormatException;
import org.osaf.cosmo.atom.generator.UnsupportedProjectionException;
import org.osaf.cosmo.atom.processor.ContentProcessor;
import org.osaf.cosmo.atom.processor.ProcessorException;
import org.osaf.cosmo.atom.processor.ProcessorFactory;
import org.osaf.cosmo.atom.processor.UnsupportedContentTypeException;
import org.osaf.cosmo.atom.processor.ValidationException;
import org.osaf.cosmo.calendar.EntityConverter;
import org.osaf.cosmo.calendar.util.CalendarUtils;
import org.osaf.cosmo.model.BaseEventStamp;
import org.osaf.cosmo.model.CalendarCollectionStamp;
import org.osaf.cosmo.model.CollectionItem;
import org.osaf.cosmo.model.CollectionLockedException;
import org.osaf.cosmo.model.ContentItem;
import org.osaf.cosmo.model.EventExceptionStamp;
import org.osaf.cosmo.model.EventStamp;
import org.osaf.cosmo.model.HomeCollectionItem;
import org.osaf.cosmo.model.IcalUidInUseException;
import org.osaf.cosmo.model.Item;
import org.osaf.cosmo.model.ItemSecurityException;
import org.osaf.cosmo.model.ModelValidationException;
import org.osaf.cosmo.model.ModificationUid;
import org.osaf.cosmo.model.NoteItem;
import org.osaf.cosmo.model.NoteOccurrence;
import org.osaf.cosmo.model.StampUtils;
import org.osaf.cosmo.model.UidInUseException;
import org.osaf.cosmo.model.User;
import org.osaf.cosmo.model.filter.EventStampFilter;
import org.osaf.cosmo.model.filter.NoteItemFilter;
import org.osaf.cosmo.model.filter.Restrictions;
import org.osaf.cosmo.model.text.XhtmlCollectionFormat;
import org.osaf.cosmo.security.CosmoSecurityException;
import org.osaf.cosmo.server.ServiceLocator;
import org.osaf.cosmo.service.ContentService;

public class ItemCollectionAdapter extends BaseCollectionAdapter implements AtomConstants {

    private static final Log log = LogFactory.getLog(ItemCollectionAdapter.class);

    private ProcessorFactory processorFactory;
    private ContentService contentService;

    // Provider methods
    private static final String[] ALLOWED_COLL_METHODS = new String[] { "GET", "HEAD", "POST", "PUT", "DELETE",
            "OPTIONS" };
    private static final String[] ALLOWED_ENTRY_METHODS = new String[] { "GET", "HEAD", "PUT", "OPTIONS" };

    public ResponseContext postEntry(RequestContext request) {
        CollectionTarget target = (CollectionTarget) request.getTarget();
        CollectionItem collection = target.getCollection();
        if (log.isDebugEnabled())
            log.debug("creating entry in collection " + collection.getUid());

        ResponseContext frc = checkEntryWritePreconditions(request, false);
        if (frc != null)
            return frc;

        try {
            String mediaType = request.getContentType().toString();
            boolean isEntry = MimeTypeHelper.isMatch(Constants.ATOM_MEDIA_TYPE, mediaType);

            Entry entry = null;
            ContentProcessor processor = null;
            NoteItem item = null;
            if (isEntry) {
                // XXX: does abdera automatically resolve external content?
                entry = (Entry) request.getDocument().getRoot();
                processor = createContentProcessor(entry);
                item = processor.processCreation(entry.getContent(), collection);
            } else {
                try {
                    processor = createContentProcessor(mediaType);
                } catch (UnsupportedContentTypeException e) {
                    return ProviderHelper.notsupported(request, "Content-type must be one of "
                            + StringUtils.join(processorFactory.getSupportedContentTypes(), ", "));
                }
                item = processor.processCreation(request.getReader(), collection);
            }

            item = (NoteItem) contentService.createContent(collection, item);

            ServiceLocator locator = createServiceLocator(request);

            if (isEntry) {
                ItemTarget itemTarget = new ItemTarget(request, item, target.getProjection(), target.getFormat());
                ItemFeedGenerator generator = createItemFeedGenerator(itemTarget, locator);
                entry = generator.generateEntry(item);

                return created(request, entry, item, locator);
            } else {
                return created(item, locator);
            }
        } catch (IOException e) {
            String reason = "Unable to read request content: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (UnsupportedContentTypeException e) {
            return ProviderHelper.badrequest(request, "Entry content type must be one of "
                    + StringUtils.join(processorFactory.getSupportedContentTypes(), ", "));
        } catch (ParseException e) {
            String reason = "Unparseable content: ";
            if (e.getCause() != null)
                reason = reason + e.getCause().getMessage();
            else
                reason = reason + e.getMessage();
            return ProviderHelper.badrequest(request, reason);
        } catch (ValidationException e) {
            String reason = "Invalid content: ";
            if (e.getCause() != null)
                reason = reason + e.getCause().getMessage();
            else
                reason = reason + e.getMessage();
            return ProviderHelper.badrequest(request, reason);
        } catch (UidInUseException e) {
            return ProviderHelper.conflict(request, "Uid already in use");
        } catch (IcalUidInUseException e) {
            return conflict(request, new UidConflictException(e));
        } catch (ProcessorException e) {
            String reason = "Unknown content processing error: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (GeneratorException e) {
            String reason = "Unknown entry generation error: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        }
    }

    public ResponseContext deleteEntry(RequestContext request) {
        ItemTarget target = (ItemTarget) request.getTarget();
        NoteItem item = target.getItem();

        // handle case where item is an occurrence, return unknown
        if (item instanceof NoteOccurrence)
            return ProviderHelper.notfound(request, "Item not found");

        if (log.isDebugEnabled())
            log.debug("deleting entry for item " + item.getUid());

        try {
            String uuid = request.getParameter("uuid");
            if (!StringUtils.isBlank(uuid)) {
                Item collection = contentService.findItemByUid(uuid);
                if (collection == null)
                    return ProviderHelper.conflict(request, "Collection not found");
                if (!(collection instanceof CollectionItem))
                    return ProviderHelper.conflict(request, "Uuid does not specify a collection");
                contentService.removeItemFromCollection(item, (CollectionItem) collection);
            } else {
                contentService.removeItem(item);
            }

            return deleted();
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        }
    }

    public ResponseContext putEntry(RequestContext request) {
        ItemTarget target = (ItemTarget) request.getTarget();
        NoteItem item = target.getItem();
        if (log.isDebugEnabled())
            log.debug("updating entry for item " + item.getUid());

        ResponseContext frc = checkEntryWritePreconditions(request);
        if (frc != null)
            return frc;

        try {
            // XXX: does abdera automatically resolve external content?
            Entry entry = (Entry) request.getDocument().getRoot();
            ContentProcessor processor = createContentProcessor(entry);
            item = processEntryUpdate(processor, entry, item);

            target = new ItemTarget(request, item, target.getProjection(), target.getFormat());
            ServiceLocator locator = createServiceLocator(request);
            ItemFeedGenerator generator = createItemFeedGenerator(target, locator);
            entry = generator.generateEntry(item);

            return updated(request, entry, item, locator, false);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        } catch (IOException e) {
            String reason = "Unable to read request content: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (UnsupportedContentTypeException e) {
            return ProviderHelper.badrequest(request, "Entry content type must be one of "
                    + StringUtils.join(processorFactory.getSupportedContentTypes(), ", "));
        } catch (ParseException e) {
            String reason = "Unparseable content: ";
            if (e.getCause() != null)
                reason = reason + e.getCause().getMessage();
            else
                reason = reason + e.getMessage();
            return ProviderHelper.badrequest(request, reason);
        } catch (ValidationException e) {
            String reason = "Invalid content: ";
            if (e.getCause() != null)
                reason = reason + e.getCause().getMessage();
            else
                reason = reason + e.getMessage();
            return ProviderHelper.badrequest(request, reason);
        } catch (IcalUidInUseException e) {
            return conflict(request, new UidConflictException(e));
        } catch (ProcessorException e) {
            String reason = "Unknown content processing error: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (GeneratorException e) {
            String reason = "Unknown entry generation error: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        }
    }

    @Override
    public ResponseContext putMedia(RequestContext request) {
        ItemTarget target = (ItemTarget) request.getTarget();
        NoteItem item = target.getItem();
        if (log.isDebugEnabled())
            log.debug("updating media for item " + item.getUid());

        ResponseContext frc = checkMediaWritePreconditions(request);
        if (frc != null)
            return frc;

        try {
            ContentProcessor processor = createContentProcessor(request.getContentType().toString());
            processor.processContent(request.getReader(), item);
            item = (NoteItem) contentService.updateContent((ContentItem) item);

            return updated(item);
        } catch (IOException e) {
            String reason = "Unable to read request content: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (UnsupportedContentTypeException e) {
            return ProviderHelper.notsupported(request, "Unsupported media type " + e.getContentType());
        } catch (ValidationException e) {
            String reason = "Invalid content: ";
            if (e.getCause() != null)
                reason = reason + e.getCause().getMessage();
            else
                reason = reason + e.getMessage();
            return ProviderHelper.badrequest(request, reason);
        } catch (ProcessorException e) {
            String reason = "Unknown content processing error: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        }
    }

    @Override
    public ResponseContext postMedia(RequestContext request) {
        if (isAddItemToCollectionRequest(request))
            return addItemToCollection(request);

        return super.postMedia(request);
    }

    public ResponseContext getFeed(RequestContext request) {
        CollectionTarget target = (CollectionTarget) request.getTarget();
        CollectionItem collection = target.getCollection();
        if (log.isDebugEnabled())
            log.debug("getting feed for collection " + collection.getUid());

        try {
            ServiceLocator locator = createServiceLocator(request);

            // check if it is a search
            Feed feed;
            String searchType = getNonEmptyParameter(request, "searchType");
            if (searchType == null) { // not a search, continue as per usual
                ItemFeedGenerator generator = createItemFeedGenerator(target, locator);
                generator.setFilter(createQueryFilter(request));
                feed = generator.generateFeed(collection);
            } else {// it's a search
                feed = getSearchFeed(searchType, request, target, locator, collection);
            }

            return ok(request, feed, collection);
        } catch (InvalidQueryException e) {
            return ProviderHelper.badrequest(request, e.getMessage());
        } catch (UnsupportedProjectionException e) {
            String reason = "Projection " + target.getProjection() + " not supported";
            return ProviderHelper.badrequest(request, reason);
        } catch (UnsupportedFormatException e) {
            String reason = "Format " + target.getFormat() + " not supported";
            return ProviderHelper.badrequest(request, reason);
        } catch (GeneratorException e) {
            String reason = "Unknown feed generation error: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        }
    }

    public Feed getSearchFeed(String searchType, RequestContext request, CollectionTarget target,
            ServiceLocator locator, CollectionItem collection) throws InvalidQueryException,
            UnsupportedProjectionException, UnsupportedFormatException, GeneratorException {
        Feed feed;

        if (searchType.equals("basicSearch")) {// coming from the quick entry
                                               // bar, searches on title and
                                               // body of notes only
            if (log.isDebugEnabled())
                log.debug("In basicSearch.");
            String query = getNonEmptyParameter(request, "query");

            if (query == null) {
                log.warn("query param missing");
                return null;
            }

            BaseItemFeedGenerator searchGenerator = (BaseItemFeedGenerator) createItemFeedGenerator(target,
                    locator);
            // should I all this in createrQueryFilter?
            // maybe I should add a createSearchQuery()

            String[] queryStrings = query.split("\\s");
            int phraseLoc, nullCount = 0;
            String temp;
            if (log.isDebugEnabled())
                log.debug("length = " + queryStrings.length);
            for (int i = 0; i < queryStrings.length; i++) {
                if (log.isDebugEnabled())
                    log.debug(queryStrings[i]);
                // if it starts with a quote (but doesn't end with one), its a
                // phrase that needs to be matched
                if (queryStrings[i].charAt(0) == '"'
                        && queryStrings[i].charAt(queryStrings[i].length() - 1) != '"') {
                    queryStrings[i] = queryStrings[i].substring(1); // get rid
                                                                    // of quote
                    phraseLoc = i;
                    temp = queryStrings[++i];
                    while (true) {
                        if (log.isDebugEnabled())
                            log.debug("Phrase start");
                        if (temp.charAt(temp.length() - 1) == '"') {// ends the
                                                                    // phrase
                                                                    // add string to phrase w/o quote, null current
                                                                    // location, break
                            queryStrings[phraseLoc] = queryStrings[phraseLoc] + " "
                                    + temp.substring(0, temp.length() - 1);
                            queryStrings[i] = null;
                            nullCount++;
                            if (log.isDebugEnabled())
                                log.debug("Phrase end");
                            break;
                        } else {// still in the phrase -- add next string to
                                // phrase, null current location
                            queryStrings[phraseLoc] = queryStrings[phraseLoc] + " " + temp;
                            queryStrings[i] = null;
                            temp = queryStrings[++i];
                            nullCount++;
                        }
                    }
                }
            }

            int queryCount = queryStrings.length - nullCount;
            if (log.isDebugEnabled())
                log.debug("nullCount = " + nullCount + ", queryCount = " + queryCount);
            NoteItemFilter[] bodyFilters = new NoteItemFilter[queryCount];
            NoteItemFilter[] titleFilters = new NoteItemFilter[queryCount];
            for (int j = 0; j < queryStrings.length; j++) {
                if (queryStrings[j] != null) {
                    bodyFilters[j] = new NoteItemFilter();
                    titleFilters[j] = new NoteItemFilter();
                    bodyFilters[j].setBody(Restrictions.ilike(queryStrings[j]));
                    titleFilters[j].setDisplayName(Restrictions.ilike(queryStrings[j]));
                }
            }
            feed = searchGenerator.generateSearchFeed(collection, bodyFilters, titleFilters);
        } else { // coming from the the advanced search widget
            feed = null;// placeholder
            log.warn("Error -- invalid searchType");
        }

        return feed;
    }

    public ResponseContext getEntry(RequestContext request) {
        ItemTarget target = (ItemTarget) request.getTarget();
        NoteItem item = target.getItem();
        if (log.isDebugEnabled())
            log.debug("getting entry for item " + item.getUid());

        try {
            ServiceLocator locator = createServiceLocator(request);
            ItemFeedGenerator generator = createItemFeedGenerator(target, locator);
            Entry entry = generator.generateEntry(item);

            return ok(request, entry, item);
        } catch (UnsupportedProjectionException e) {
            String reason = "Projection " + target.getProjection() + " not supported";
            return ProviderHelper.badrequest(request, reason);
        } catch (UnsupportedFormatException e) {
            String reason = "Format " + target.getFormat() + " not supported";
            return ProviderHelper.badrequest(request, reason);
        } catch (GeneratorException e) {
            String reason = "Unknown entry generation error: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        }
    }

    // ExtendedCollectionAdapter methods

    public ResponseContext postCollection(RequestContext request) {
        // This adapter supports POSTing an XHTML representation (
        // of a collection (no child items), POSTing an icalendar
        // representation of a collection (where child items will be
        // imported as well), or POSTing a URL that points to
        // an icalendar representation of a collection

        MimeType ct = request.getContentType();
        if (ct == null)
            return ProviderHelper.notsupported(request, "Content-Type is required");

        String mimeType = ct.toString();

        if (MimeTypeHelper.isMatch(MEDIA_TYPE_XHTML, mimeType))
            return createCollectionXHTML(request);
        else if (MimeTypeHelper.isMatch(MEDIA_TYPE_CALENDAR, mimeType))
            return createCollectionICSFromData(request);
        else if (MimeTypeHelper.isMatch(MEDIA_TYPE_URLENCODED, mimeType))
            return createCollectionICSFromURL(request);
        else
            return ProviderHelper.notsupported(request, "unsupported Content-Type");
    }

    protected ResponseContext createCollectionICSFromURL(RequestContext request) {

        try {
            String url = getNonEmptyParameter(request, "url");
            if (url == null)
                return ProviderHelper.badrequest(request, "url must be provided");
            URL calUrl = new URL(url);

            Calendar calendar = CalendarUtils.parseCalendar(calUrl.openConnection().getInputStream());
            if (calendar == null)
                return ProviderHelper.badrequest(request, "invalid icalendar");

            calendar.validate(true);

            return createCollectionICSCommon(request, calendar);

        } catch (MalformedURLException e) {
            return ProviderHelper.badrequest(request, "invalid URL");
        } catch (UnsupportedEncodingException e) {
            return ProviderHelper.badrequest(request, "displayName and icalendar must be provided");
        } catch (IOException e) {
            String reason = "Unable to read URL content: " + e.getMessage();
            return ProviderHelper.badrequest(request, reason);
        } catch (ParserException e) {
            return ProviderHelper.badrequest(request, "invalid icalendar: " + e.getMessage());
        } catch (net.fortuna.ical4j.model.ValidationException e) {
            return ProviderHelper.badrequest(request, "invalid icalendar: " + e.getMessage());
        }
    }

    protected ResponseContext createCollectionICSFromData(RequestContext request) {

        try {
            // parse and validate icalendar
            Calendar calendar = CalendarUtils.parseCalendar(request.getReader());
            if (calendar == null)
                return ProviderHelper.badrequest(request, "invalid icalendar");

            calendar.validate(true);
            return createCollectionICSCommon(request, calendar);

        } catch (UnsupportedEncodingException e) {
            return ProviderHelper.badrequest(request, "displayName and icalendar must be provided");
        } catch (IOException e) {
            String reason = "Unable to read request content: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (ParserException e) {
            return ProviderHelper.badrequest(request, "invalid icalendar: " + e.getMessage());
        } catch (net.fortuna.ical4j.model.ValidationException e) {
            return ProviderHelper.badrequest(request, "invalid icalendar: " + e.getMessage());
        }
    }

    protected ResponseContext createCollectionICSCommon(RequestContext request, Calendar calendar) {

        NewCollectionTarget target = (NewCollectionTarget) request.getTarget();
        User user = target.getUser();
        HomeCollectionItem home = target.getHomeCollection();

        if (log.isDebugEnabled())
            log.debug("creating collection from icalendar in home collection of user '" + user.getUsername() + "'");

        try {
            String displayName = target.getDisplayName();

            if (displayName == null)
                return ProviderHelper.badrequest(request, "displayName must be provided");

            // create new collection
            CollectionItem collection = getEntityFactory().createCollection();
            collection.setDisplayName(displayName);
            collection.setOwner(user);
            CalendarCollectionStamp stamp = getEntityFactory().createCalendarCollectionStamp(collection);

            stamp.setDescription(collection.getDisplayName());
            // XXX set the calendar language from Content-Language
            collection.addStamp(stamp);

            // add child items by converting icalendar calendar
            Set<Item> children = new HashSet<Item>();

            for (ContentItem child : new EntityConverter(getEntityFactory()).convertCalendar(calendar)) {
                child.setOwner(user);
                child.setLastModifiedBy(user.getEmail());
                children.add(child);
            }

            // use api that creates collection and children
            collection = getContentService().createCollection(home, collection, children);

            ServiceLocator locator = createServiceLocator(request);

            return created(collection, locator);

        } catch (ModelValidationException e) {
            return ProviderHelper.badrequest(request, "invalid icalendar: " + e.getMessage());
        }
    }

    protected ResponseContext createCollectionXHTML(RequestContext request) {
        NewCollectionTarget target = (NewCollectionTarget) request.getTarget();
        User user = target.getUser();
        HomeCollectionItem home = target.getHomeCollection();

        if (log.isDebugEnabled())
            log.debug("creating collection in home collection of user '" + user.getUsername() + "'");

        ResponseContext frc = checkCollectionWritePreconditions(request);
        if (frc != null)
            return frc;

        try {
            CollectionItem content = readCollection(request);

            CollectionItem collection = getEntityFactory().createCollection();
            collection.setUid(content.getUid());
            collection.setName(collection.getUid());
            collection.setDisplayName(content.getDisplayName());
            collection.setOwner(user);

            CalendarCollectionStamp stamp = getEntityFactory().createCalendarCollectionStamp(collection);

            stamp.setDescription(collection.getDisplayName());
            // XXX set the calendar language from Content-Language
            collection.addStamp(stamp);

            collection = contentService.createCollection(home, collection);

            ServiceLocator locator = createServiceLocator(request);

            return created(collection, locator);
        } catch (IOException e) {
            String reason = "Unable to read request content: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (ValidationException e) {
            String msg = "Invalid request content: " + e.getMessage();
            if (e.getCause() != null)
                msg += ": " + e.getCause().getMessage();
            return ProviderHelper.badrequest(request, msg);
        } catch (UidInUseException e) {
            return ProviderHelper.conflict(request, "Uid already in use");
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        }
    }

    @Override
    public ResponseContext extensionRequest(RequestContext request) {
        // POST to a NewCollectionTarget
        if (isCreateCollectionRequest(request))
            return postCollection(request);
        // POST to a CollectionTarget with media type application/x-www-form-urlencoded
        else if (isAddItemToCollectionRequest(request))
            return addItemToCollection(request);
        else
            return super.extensionRequest(request);
    }

    public ResponseContext putCollection(RequestContext request) {
        CollectionTarget target = (CollectionTarget) request.getTarget();
        CollectionItem collection = target.getCollection();

        if (collection instanceof HomeCollectionItem)
            return ProviderHelper.unauthorized(request, "Home collection is not modifiable");

        if (log.isDebugEnabled())
            log.debug("updating details for collection " + collection.getUid());

        ResponseContext frc = checkCollectionWritePreconditions(request);
        if (frc != null)
            return frc;

        try {
            CollectionItem content = readCollection(request);

            if (!content.getDisplayName().equals(collection.getDisplayName())) {
                if (log.isDebugEnabled())
                    log.debug("updating collection " + collection.getUid());
                collection.setDisplayName(content.getDisplayName());
                collection = contentService.updateCollection(collection);
            }

            return updated(collection);
        } catch (IOException e) {
            String reason = "Unable to read request content: " + e.getMessage();
            log.error(reason, e);
            return ProviderHelper.servererror(request, reason, e);
        } catch (ValidationException e) {
            String msg = "Invalid request content: " + e.getMessage();
            if (e.getCause() != null)
                msg += ": " + e.getCause().getMessage();
            return ProviderHelper.badrequest(request, msg);
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        }
    }

    public ResponseContext deleteCollection(RequestContext request) {
        CollectionTarget target = (CollectionTarget) request.getTarget();
        CollectionItem collection = target.getCollection();

        if (collection instanceof HomeCollectionItem)
            return ProviderHelper.unauthorized(request, "Home collection is not deleteable");

        if (log.isDebugEnabled())
            log.debug("deleting collection " + collection.getUid());

        try {
            contentService.removeCollection(collection);

            return deleted();
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        }
    }

    // our methods

    protected ResponseContext addItemToCollection(RequestContext request) {
        CollectionTarget target = (CollectionTarget) request.getTarget();
        CollectionItem collection = target.getCollection();
        if (log.isDebugEnabled())
            log.debug("adding item to collection " + collection.getUid());

        ResponseContext frc = checkAddItemToCollectionPreconditions(request);
        if (frc != null)
            return frc;

        try {
            String uuid = getNonEmptyParameter(request, "uuid");
            if (uuid == null)
                return ProviderHelper.badrequest(request, "Uuid must be provided");

            Item item = contentService.findItemByUid(uuid);
            if (item == null)
                return ProviderHelper.badrequest(request, "Item with uuid " + uuid + " not found");
            if (!(item instanceof NoteItem))
                return ProviderHelper.badrequest(request, "Item with uuid " + uuid + " is not a note");

            //
            // FIXME
            // for now can only check if item owner is collection owner

            // if item owner is collection owner, then item will be updateable,
            // otherwise it will be read-only for now, until we support
            // multiple tickets vs single ticket/single principal
            if (item.getOwner().equals(collection.getOwner()))
                contentService.addItemToCollection(item, collection);
            else {
                // return forbidden
                return ProviderHelper.forbidden(request, "unauthorized for item " + item.getUid());
            }

            return createResponseContext(204);
        } catch (CollectionLockedException e) {
            return locked(request);
        } catch (CosmoSecurityException e) {
            if (e instanceof ItemSecurityException)
                return insufficientPrivileges(request,
                        new InsufficientPrivilegesException((ItemSecurityException) e));
            else
                return ProviderHelper.forbidden(request, e.getMessage());
        }
    }

    public ProcessorFactory getProcessorFactory() {
        return processorFactory;
    }

    public void setProcessorFactory(ProcessorFactory factory) {
        processorFactory = factory;
    }

    public ContentService getContentService() {
        return contentService;
    }

    public void setContentService(ContentService contentService) {
        this.contentService = contentService;
    }

    public void init() {
        super.init();
        if (processorFactory == null)
            throw new IllegalStateException("processorFactory is required");
        if (contentService == null)
            throw new IllegalStateException("contentService is required");
    }

    protected ItemFeedGenerator createItemFeedGenerator(BaseItemTarget target, ServiceLocator locator)
            throws UnsupportedProjectionException, UnsupportedFormatException {
        return getGeneratorFactory().createItemFeedGenerator(target.getProjection(), target.getFormat(), locator);
    }

    protected ContentProcessor createContentProcessor(Entry entry) throws UnsupportedContentTypeException {
        String mediaType = null;

        // if the entry is text, HTML, XHTML or XML, we can use the
        // content type itself to find a processor.
        if (entry.getContentType() != null && !entry.getContentType().equals(Content.Type.MEDIA))
            mediaType = entry.getContentType().toString();

        // if it's media, then we we want to use the content's mime
        // type directly.
        if (mediaType == null && entry.getContentMimeType() != null)
            mediaType = entry.getContentMimeType().toString();

        if (mediaType != null)
            return createContentProcessor(mediaType);

        throw new UnsupportedContentTypeException("indeterminate media type");
    }

    protected ContentProcessor createContentProcessor(String mediaType) throws UnsupportedContentTypeException {
        return processorFactory.createProcessor(mediaType);
    }

    protected NoteItemFilter createQueryFilter(RequestContext request) throws InvalidQueryException {
        boolean requiresFilter = false;
        EventStampFilter eventFilter = new EventStampFilter();

        try {
            java.util.Date start = getDateParameter(request, "start");
            java.util.Date end = getDateParameter(request, "end");

            if ((start == null && end != null) || (start != null && end == null))
                throw new InvalidQueryException(
                        "Both start and end parameters must be provided for a time-range query");

            if (start != null && end != null) {
                requiresFilter = true;
                eventFilter.setTimeRange(start, end);
                eventFilter.setExpandRecurringEvents(true);
            }
        } catch (java.text.ParseException e) {
            throw new InvalidQueryException("Error parsing time-range parameter: " + e.getMessage(), e);
        }

        try {
            TimeZone tz = getTimeZoneParameter(request, "tz");
            if (tz != null) {
                requiresFilter = true;
                eventFilter.setTimezone(tz);
            }
        } catch (IllegalArgumentException e) {
            log.warn("Unrecognized time zone " + request.getParameter("tz")
                    + " ... falling back to system default time zone");
        }

        if (!requiresFilter)
            return null;

        NoteItemFilter itemFilter = new NoteItemFilter();
        itemFilter.getStampFilters().add(eventFilter);

        return itemFilter;
    }

    private boolean isCreateCollectionRequest(RequestContext request) {
        if (!(request.getTarget() instanceof NewCollectionTarget))
            return false;

        return request.getMethod().equalsIgnoreCase("POST");
    }

    private boolean isAddItemToCollectionRequest(RequestContext request) {
        if (!(request.getTarget() instanceof CollectionTarget))
            return false;

        MimeType ct = request.getContentType();
        if (ct == null)
            return false;
        return MimeTypeHelper.isMatch(MEDIA_TYPE_URLENCODED, ct.toString());
    }

    private ResponseContext checkAddItemToCollectionPreconditions(RequestContext request) {
        int contentLength = Integer.valueOf(request.getProperty(RequestContext.Property.CONTENTLENGTH).toString());
        if (contentLength <= 0)
            return lengthrequired(request);

        return null;
    }

    private NoteItem processEntryUpdate(ContentProcessor processor, Entry entry, NoteItem item)
            throws ValidationException, ProcessorException {
        EventStamp es = StampUtils.getEventStamp(item);
        Date oldstart = es != null && es.isRecurring() ? es.getStartDate() : null;

        processor.processContent(entry.getContent(), item);

        // oldStart will have a value if the item has an EventStamp
        // and the EventStamp is recurring
        if (oldstart != null) {
            es = StampUtils.getEventStamp(item);
            // Case 1: EventStamp was removed from recurring event, so we
            // have to remove all modifications (a modification doesn't make
            // sense if there is no recurring event)
            if (es == null) {
                LinkedHashSet<ContentItem> updates = new LinkedHashSet<ContentItem>();
                for (NoteItem mod : item.getModifications()) {
                    mod.setIsActive(false);
                    updates.add(mod);
                }
                updates.add(item);

                // Update item and remove modifications in one atomic service call
                contentService.updateContentItems(item.getParents(), updates);
            }
            // Case 2: Start date may have changed on master event.
            else {
                Date newstart = es.getStartDate();

                // If changed, we have to update all the recurrenceIds
                // for any modifications
                if (newstart != null && !newstart.equals(oldstart)) {
                    long delta = newstart.getTime() - oldstart.getTime();
                    if (log.isDebugEnabled())
                        log.debug("master event start date changed; " + "adjusting modifications by " + delta
                                + " milliseconds");

                    LinkedHashSet<ContentItem> updates = new LinkedHashSet<ContentItem>();
                    HashSet<NoteItem> copies = new HashSet<NoteItem>();
                    HashSet<NoteItem> removals = new HashSet<NoteItem>();

                    // copy each modification and update the copy's uid
                    // with the new start date
                    Iterator<NoteItem> mi = item.getModifications().iterator();
                    while (mi.hasNext()) {
                        NoteItem mod = mi.next();

                        // ignore modifications without event stamp
                        if (StampUtils.getEventExceptionStamp(mod) == null)
                            continue;

                        mod.setIsActive(false);
                        removals.add(mod);

                        NoteItem copy = (NoteItem) mod.copy();
                        copy.setModifies(item);

                        EventExceptionStamp ees = StampUtils.getEventExceptionStamp(copy);

                        DateTime oldRid = (DateTime) ees.getRecurrenceId();
                        java.util.Date newRidTime = new java.util.Date(oldRid.getTime() + delta);
                        DateTime newRid = (DateTime) Dates.getInstance(newRidTime, Value.DATE_TIME);
                        if (oldRid.isUtc())
                            newRid.setUtc(true);
                        else
                            newRid.setTimeZone(oldRid.getTimeZone());

                        copy.setUid(new ModificationUid(item, newRid).toString());
                        ees.setRecurrenceId(newRid);

                        // If the modification's dtstart is missing, then
                        // we have to adjust dtstart to be equal to the
                        // recurrenceId.
                        if (isDtStartMissing(StampUtils.getBaseEventStamp(mod))) {
                            ees.setStartDate(ees.getRecurrenceId());
                        }

                        copies.add(copy);
                    }

                    // add removals first
                    updates.addAll(removals);
                    // then additions
                    updates.addAll(copies);
                    // then updates
                    updates.add(item);

                    // Update everything in one atomic service call
                    contentService.updateContentItems(item.getParents(), updates);
                } else {
                    // otherwise use simple update
                    item = (NoteItem) contentService.updateContent((ContentItem) item);
                }
            }
        } else {
            // use simple update
            item = (NoteItem) contentService.updateContent((ContentItem) item);
        }

        return item;
    }

    private CollectionItem readCollection(RequestContext request) throws IOException, ValidationException {
        try {
            Reader in = request.getReader();
            if (in == null)
                throw new ValidationException("An entity-body must be provided");

            XhtmlCollectionFormat formatter = new XhtmlCollectionFormat();
            CollectionItem collection = formatter.parse(IOUtils.toString(in), getEntityFactory());

            if (collection.getDisplayName() == null)
                throw new ValidationException("Display name is required");

            return collection;
        } catch (java.text.ParseException e) {
            throw new ValidationException("Error parsing XHTML content", e);
        }
    }

    /**
     * Determine if startDate is missing.  The startDate is missing
     * if the startDate is equal to the reucurreceId and the anyTime
     * parameter is inherited.
     * @param stamp BaseEventStamp to test
     * @return
     */
    private boolean isDtStartMissing(BaseEventStamp stamp) {

        if (stamp.getStartDate() == null || stamp.getRecurrenceId() == null)
            return false;

        // "missing" startDate is represented as startDate==recurrenceId
        if (!stamp.getStartDate().equals(stamp.getRecurrenceId()))
            return false;

        // "missing" anyTime is represented as null
        if (stamp.isAnyTime() != null)
            return false;

        return true;
    }

    private ResponseContext created(NoteItem item, ServiceLocator locator) {
        org.apache.abdera.protocol.server.context.AbstractResponseContext rc = createResponseContext(201,
                "Created");

        rc.setEntityTag(new EntityTag(item.getEntityTag()));
        rc.setLastModified(item.getModifiedDate());

        rc.setLocation(locator.getAtomItemUrl(item.getUid(), true));
        // don't set Content-Location since no content is included in the
        // response

        return rc;
    }

    private ResponseContext created(CollectionItem collection, ServiceLocator locator) {
        AbstractResponseContext rc = createResponseContext(201, "Created");

        rc.setEntityTag(new EntityTag(collection.getEntityTag()));
        rc.setLastModified(collection.getModifiedDate());

        String location = locator.getAtomCollectionUrl(collection.getUid(), true);
        rc.setLocation(location);
        // don't set Content-Location since no content is included in the
        // response

        return rc;
    }
}