/**
 * Copyright (C) 2010 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.xcmis.restatom.collections;

import org.apache.abdera.factory.Factory;
import org.apache.abdera.i18n.iri.IRI;
import org.apache.abdera.model.Content;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;
import org.apache.abdera.model.Link;
import org.apache.abdera.model.Person;
import org.apache.abdera.model.Content.Type;
import org.apache.abdera.protocol.server.RequestContext;
import org.apache.abdera.protocol.server.ResponseContext;
import org.apache.abdera.protocol.server.TargetType;
import org.apache.abdera.protocol.server.context.EmptyResponseContext;
import org.apache.abdera.protocol.server.context.MediaResponseContext;
import org.apache.abdera.protocol.server.context.ResponseContextException;
import org.apache.commons.codec.binary.Base64;
import org.xcmis.restatom.AtomCMIS;
import org.xcmis.restatom.AtomUtils;
import org.xcmis.restatom.abdera.ContentTypeElement;
import org.xcmis.restatom.abdera.ObjectTypeElement;
import org.xcmis.restatom.types.CmisContentType;
import org.xcmis.restatom.types.EnumReturnVersion;
import org.xcmis.spi.BaseContentStream;
import org.xcmis.spi.ChangeTokenHolder;
import org.xcmis.spi.CmisConstants;
import org.xcmis.spi.Connection;
import org.xcmis.spi.ConstraintException;
import org.xcmis.spi.ContentAlreadyExistsException;
import org.xcmis.spi.ContentStream;
import org.xcmis.spi.FilterNotValidException;
import org.xcmis.spi.InvalidArgumentException;
import org.xcmis.spi.NameConstraintViolationException;
import org.xcmis.spi.ObjectNotFoundException;
import org.xcmis.spi.StorageException;
import org.xcmis.spi.StreamNotSupportedException;
import org.xcmis.spi.UpdateConflictException;
import org.xcmis.spi.model.AccessControlEntry;
import org.xcmis.spi.model.BaseType;
import org.xcmis.spi.model.CmisObject;
import org.xcmis.spi.model.IncludeRelationships;
import org.xcmis.spi.model.Property;
import org.xcmis.spi.model.Rendition;
import org.xcmis.spi.model.RepositoryCapabilities;
import org.xcmis.spi.model.RepositoryInfo;
import org.xcmis.spi.model.impl.StringProperty;
import org.xcmis.spi.utils.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLDecoder;
import java.security.Principal;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.activation.MimeType;
import javax.activation.MimeTypeParameterList;
import javax.ws.rs.core.HttpHeaders;

/**
 * @author <a href="mailto:andrey.parfonov@exoplatform.com">Andrey Parfonov</a>
 * @version $Id: CmisObjectCollection.java 218 2010-02-15 07:38:06Z andrew00x $
 */
public abstract class CmisObjectCollection extends AbstractCmisCollection<CmisObject>
{

   private class HttpConnectionStream extends InputStream
   {
      private final HttpURLConnection httpConnection;

      private InputStream inStream;

      private boolean closed;

      public HttpConnectionStream(HttpURLConnection httpConnection)
      {
         this.httpConnection = httpConnection;
      }

      public int read() throws IOException
      {
         if (inStream == null)
         {
            inStream = httpConnection.getInputStream();
         }
         int i = inStream.read();
         if (i == -1)
         {
            if (!closed)
            {
               try
               {
                  inStream.close();
                  httpConnection.disconnect();
                  closed = true;
               }
               catch (IOException e)
               {
                  LOG.error(e.getMessage(), e);
               }
            }
         }
         return i;
      }

      @Override
      protected void finalize() throws Throwable
      {
         if (!closed)
         {
            try
            {
               inStream.close();
               httpConnection.disconnect();
            }
            catch (IOException e)
            {
               LOG.error(e.getMessage(), e);
            }
         }
         super.finalize();
      }
   }

   private static final Logger LOG = Logger.getLogger(CmisObjectCollection.class);

   /** The Constant SPACES_AIR_SPECIFIC_REFERER. */
   protected static final String SPACES_AIR_SPECIFIC_REFERER = "app:/CMISSpacesAir.swf";

   public CmisObjectCollection(Connection connection)
   {
      super(connection);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void deleteEntry(String objectId, RequestContext request) throws ResponseContextException
   {
      try
      {
         Connection connection = getConnection(request);
         connection.deleteObject(objectId, getBooleanParameter(request, AtomCMIS.PARAM_ALL_VERSIONS, true));
      }
      catch (ConstraintException cve)
      {
         throw new ResponseContextException(createErrorResponse(cve, 409));
      }
      catch (StorageException re)
      {
         throw new ResponseContextException(createErrorResponse(re, 500));
      }
      catch (UpdateConflictException uce)
      {
         throw new ResponseContextException(createErrorResponse(uce, 409));
      }
      catch (ObjectNotFoundException onfe)
      {
         throw new ResponseContextException(createErrorResponse(onfe, 404));
      }
      catch (InvalidArgumentException iae)
      {
         throw new ResponseContextException(createErrorResponse(iae, 400));
      }
      catch (Throwable t)
      {
         throw new ResponseContextException(createErrorResponse(t, 500));
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public ResponseContext deleteMedia(RequestContext request)
   {
      try
      {
         Connection connection = getConnection(request);
         ChangeTokenHolder changeTokenHolder = new ChangeTokenHolder();
         // TODO : Is it correct to use 'If-Match' header ?
         changeTokenHolder.setValue(request.getHeader(HttpHeaders.IF_MATCH));
         String objectId = connection.deleteContentStream(getId(request), changeTokenHolder /*changeToken*/);
         CmisObject object = connection.getProperties(objectId, true, CmisConstants.CHANGE_TOKEN);
         @SuppressWarnings("unchecked")
         Property<String> changeToken = (Property<String>)getProperty(object, CmisConstants.CHANGE_TOKEN);
         // TODO : 204, is it correct ? It used by default when delete content of ATOM resource
         ResponseContext response = new EmptyResponseContext(204);
         if (changeToken != null && changeToken.getValues().size() > 0)
         {
            response.setEntityTag(changeToken.getValues().get(0));
         }
         return response;
      }
      catch (ConstraintException cve)
      {
         return createErrorResponse(cve, 409);
      }
      catch (StorageException re)
      {
         return createErrorResponse(re, 500);
      }
      catch (UpdateConflictException uce)
      {
         return createErrorResponse(uce, 409);
      }
      catch (ObjectNotFoundException onfe)
      {
         return createErrorResponse(onfe, 404);
      }
      catch (InvalidArgumentException iae)
      {
         return createErrorResponse(iae, 400);
      }
      catch (Throwable t)
      {
         return createErrorResponse(t, 500);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public void deleteMedia(String documentId, RequestContext request) throws ResponseContextException
   {
      try
      {
         Connection connection = getConnection(request);
         ChangeTokenHolder changeTokenHolder = new ChangeTokenHolder();
         // TODO : Is it correct to use 'If-Match' header ?
         changeTokenHolder.setValue(request.getHeader(HttpHeaders.IF_MATCH));
         connection.deleteContentStream(documentId, changeTokenHolder);
      }
      catch (ConstraintException cve)
      {
         throw new ResponseContextException(createErrorResponse(cve, 409));
      }
      catch (StorageException re)
      {
         throw new ResponseContextException(createErrorResponse(re, 500));
      }
      catch (UpdateConflictException uce)
      {
         throw new ResponseContextException(createErrorResponse(uce, 409));
      }
      catch (ObjectNotFoundException onfe)
      {
         throw new ResponseContextException(createErrorResponse(onfe, 404));
      }
      catch (InvalidArgumentException iae)
      {
         throw new ResponseContextException(createErrorResponse(iae, 400));
      }
      catch (Throwable t)
      {
         throw new ResponseContextException(createErrorResponse(t, 500));
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public String getAuthor(RequestContext request) throws ResponseContextException
   {
      Principal principal = request.getPrincipal();
      if (principal != null)
      {
         return principal.getName();
      }
      return ANONYMOUS;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public List<Person> getAuthors(CmisObject object, RequestContext request) throws ResponseContextException
   {
      String author = object.getObjectInfo().getCreatedBy();
      Person p = request.getAbdera().getFactory().newAuthor();
      if (author != null)
      {
         p.setName(author);
      }
      else
      {
         p.setName(SYSTEM);
      }
      return Collections.singletonList(p);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public String getContentType(CmisObject object)
   {
      String contentType = object.getObjectInfo().getContentStreamMimeType();
      if (contentType != null //
         && !"".equals(contentType))
      {
         return contentType;
      }
      return null;
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public CmisObject getEntry(String id, RequestContext request) throws ResponseContextException
   {
      try
      {
         boolean includeAllowableActions =
            getBooleanParameter(request, AtomCMIS.PARAM_INCLUDE_ALLOWABLE_ACTIONS, false);
         String propertyFilter = request.getParameter(AtomCMIS.PARAM_FILTER);
         boolean includePolicies = getBooleanParameter(request, AtomCMIS.PARAM_INCLUDE_POLICY_IDS, false);
         boolean includeACL = getBooleanParameter(request, AtomCMIS.PARAM_INCLUDE_ACL, false);
         String renditionFilter = request.getParameter(AtomCMIS.PARAM_RENDITION_FILTER);
         IncludeRelationships includeRelationships;
         try
         {
            includeRelationships =
               request.getParameter(AtomCMIS.PARAM_INCLUDE_RELATIONSHIPS) == null
                  || request.getParameter(AtomCMIS.PARAM_INCLUDE_RELATIONSHIPS).length() == 0
                  ? IncludeRelationships.NONE : IncludeRelationships.fromValue(request
                     .getParameter(AtomCMIS.PARAM_INCLUDE_RELATIONSHIPS));
         }
         catch (IllegalArgumentException iae)
         {
            String msg = "Invalid parameter " + request.getParameter(AtomCMIS.PARAM_INCLUDE_RELATIONSHIPS);
            throw new ResponseContextException(createErrorResponse(msg, 400));
         }

         Connection connection = getConnection(request);

         CmisObject object;
         if (id.charAt(0) != '/')
         {
            // Get by id.
            object =
               connection.getObject(id, includeAllowableActions, includeRelationships, includePolicies, includeACL,
                  true, propertyFilter, renditionFilter);
         }
         else
         {
            // Get by path.
            object =
               connection.getObjectByPath(id, includeAllowableActions, includeRelationships, includePolicies,
                  includeACL, true, propertyFilter, renditionFilter);
         }
         BaseType type = getBaseObjectType(object);
         if (type == BaseType.DOCUMENT)
         {
            String returnVersion = request.getParameter(AtomCMIS.PARAM_RETURN_VERSION);
            if (returnVersion == null || returnVersion.length() == 0)
            {
               return object;
            }
            EnumReturnVersion enumReturnVersion;
            try
            {
               enumReturnVersion = EnumReturnVersion.fromValue(returnVersion);
            }
            catch (IllegalArgumentException iae)
            {
               String msg = "Invalid parameter " + returnVersion;
               throw new ResponseContextException(createErrorResponse(msg, 400));
            }
            if (enumReturnVersion == EnumReturnVersion.THIS)
            {
               return object;
            }

            if (enumReturnVersion == EnumReturnVersion.LATEST && object.getObjectInfo().isLatestVersion())
            {
               return object;
            }

            if (enumReturnVersion == EnumReturnVersion.LATESTMAJOR && object.getObjectInfo().isLatestMajorVersion())
            {
               return object;
            }

            // Find latest in Version series.
            String versionSeriesId = object.getObjectInfo().getVersionSeriesId();
            return connection.getObjectOfLatestVersion(//
               versionSeriesId, //
               enumReturnVersion == EnumReturnVersion.LATESTMAJOR, //
               includeAllowableActions, //
               includeRelationships, //
               includePolicies, //
               includeACL, //
               true, //
               propertyFilter, //
               renditionFilter);
         }

         // Is not document.
         return object;
      }
      catch (FilterNotValidException fe)
      {
         throw new ResponseContextException(createErrorResponse(fe, 400));
      }
      catch (ObjectNotFoundException onfe)
      {
         throw new ResponseContextException(createErrorResponse(onfe, 404));
      }
      catch (InvalidArgumentException iae)
      {
         throw new ResponseContextException(createErrorResponse(iae, 400));
      }
      catch (Throwable t)
      {
         throw new ResponseContextException(createErrorResponse(t, 500));
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public String getId(CmisObject object) throws ResponseContextException
   {
      return object.getObjectInfo().getId();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public String getId(RequestContext request)
   {
      return request.getTarget().getParameter("objectid");
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public ResponseContext getMedia(RequestContext request)
   {
      try
      {
         Connection connection = getConnection(request);
         // TODO : resolve (optional) offset, length
         ContentStream content = connection.getContentStream(getId(request), getStreamId(request));
         ResponseContext response = new MediaResponseContext(content.getStream(), 200);
         response.setContentType(content.getMediaType().toString());
         response.setContentLength(content.length());
         response.setHeader(AtomCMIS.CONTENT_DISPOSITION_HEADER, //
            "attachment; filename=\"" + content.getFileName() + "\"");
         // TODO : need ETag here ?
         return response;
      }
      catch (ObjectNotFoundException onfe)
      {
         return createErrorResponse(onfe, 404);
      }
      catch (ConstraintException cve)
      {
         return createErrorResponse(cve, 409);
      }
      catch (Throwable t)
      {
         return createErrorResponse(t, 500);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public String getName(CmisObject object) throws ResponseContextException
   {
      return object.getObjectInfo().getName();
   }

   /**
    * {@inheritDoc}
    */
   public String getStreamId(RequestContext request)
   {
      return request.getTarget().getParameter("streamid");
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public String getTitle(CmisObject object) throws ResponseContextException
   {
      return object.getObjectInfo().getName();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public Date getUpdated(CmisObject object) throws ResponseContextException
   {
      return getLastModificationDate(object).getTime();
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public ResponseContext putEntry(RequestContext request)
   {
      Entry entry;
      try
      {
         entry = getEntryFromRequest(request);
      }
      catch (ResponseContextException rce)
      {
         //         rce.printStackTrace();
         return rce.getResponseContext();
      }

      try
      {
         Connection connection = getConnection(request);
         ObjectTypeElement objectElement = entry.getFirstChild(AtomCMIS.OBJECT);
         CmisObject object = objectElement != null ? object = objectElement.getObject() : new CmisObject();
         updatePropertiesFromEntry(object, entry);

         Map<String, Property<?>> properties = object.getProperties();
         Collection<String> policyIds = object.getPolicyIds();
         List<AccessControlEntry> acl = object.getACL();
         ContentStream contentStream = getContentStream(entry, request);

         boolean checkin = getBooleanParameter(request, AtomCMIS.PARAM_CHECKIN, false);
         String updatedId = null;
         if (checkin)
         {
            // If 'checkin' param is TRUE, execute checkin() service.
            boolean major = getBooleanParameter(request, AtomCMIS.PARAM_MAJOR, true);
            String checkinComment = request.getParameter(AtomCMIS.PARAM_CHECKIN_COMMENT);
            // TODO : ACEs for removing. Not clear from specification how to
            // pass (obtain) ACEs for adding and removing from one object.
            updatedId =
               connection.checkin(getId(request), major, properties, contentStream, checkinComment, acl, null,
                  policyIds);
         }
         else
         {
            // If 'checkin' param is FALSE, execute updateProperties() service.
            // TODO : is correct to use 'if-match' header?
            // Get 'if-match' header as is, according to HTTP specification :
            // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3
            // ------------------------------------------
            // Clients MAY issue simple (non-subrange) GET requests with either
            // weak validators or strong validators. Clients MUST NOT use weak
            // validators in other forms of request.
            // ------------------------------------------
            // Method is PUT - use strong comparison.
            CmisObject cmisObject = connection.getProperties(getId(request), true, CmisConstants.BASE_TYPE_ID);
            BaseType baseType = cmisObject.getObjectInfo().getBaseType();

            ChangeTokenHolder changeTokenHolder = new ChangeTokenHolder();
            changeTokenHolder.setValue(request.getHeader(HttpHeaders.IF_MATCH));
            updatedId = connection.updateProperties(getId(request), changeTokenHolder, properties);
            if (baseType == BaseType.DOCUMENT && contentStream != null)
            {
               updatedId = connection.setContentStream(getId(request), contentStream, changeTokenHolder, true);
            }

         }

         CmisObject updated = connection.getProperties(updatedId, true, CmisConstants.WILDCARD);
         entry = request.getAbdera().getFactory().newEntry();
         addEntryDetails(request, entry, request.getResolvedUri(), updated);
         return buildGetEntryResponse(request, entry);
      }
      catch (ConstraintException cve)
      {
         return createErrorResponse(cve, 409);
      }
      catch (NameConstraintViolationException nce)
      {
         return createErrorResponse(nce, 400);
      }
      catch (StorageException re)
      {
         return createErrorResponse(re, 500);
      }
      catch (UpdateConflictException uce)
      {
         return createErrorResponse(uce, 409);
      }
      catch (InvalidArgumentException iae)
      {
         return createErrorResponse(iae, 400);
      }
      catch (ResponseContextException rce)
      {
         return rce.getResponseContext();
      }
      catch (Throwable t)
      {
         return createErrorResponse(t, 500);
      }
   }

   /**
    * Put media.
    *
    * @param entryObj the entry obj
    * @param contentType the content type
    * @param slug the slug
    * @param inputStream the input stream
    * @param request the request
    *
    * @throws ResponseContextException the response context exception
    */
   @Override
   public void putMedia(CmisObject entryObj, MimeType contentType, String slug, InputStream inputStream,
      RequestContext request) throws ResponseContextException
   {
      try
      {
         Connection connection = getConnection(request);
         ContentStream content = new BaseContentStream(inputStream, null, convertMimeType(contentType));
         // TODO : is correct ?
         ChangeTokenHolder changeTokenHolder = new ChangeTokenHolder();
         changeTokenHolder.setValue(request.getHeader(HttpHeaders.IF_MATCH));
         boolean overwriteFlag = getBooleanParameter(request, AtomCMIS.PARAM_OVERWRITE_FLAG, true);
         connection.setContentStream(getId(request), content, changeTokenHolder, overwriteFlag);
      }
      catch (ConstraintException cve)
      {
         throw new ResponseContextException(createErrorResponse(cve, 409));
      }
      catch (StorageException re)
      {
         throw new ResponseContextException(createErrorResponse(re, 409));
      }
      catch (ContentAlreadyExistsException ce)
      {
         throw new ResponseContextException(createErrorResponse(ce, 409));
      }
      catch (StreamNotSupportedException se)
      {
         // XXX : specification says 403, is it correct ?
         throw new ResponseContextException(createErrorResponse(se, 400));
      }
      catch (UpdateConflictException uce)
      {
         throw new ResponseContextException(createErrorResponse(uce, 409));
      }
      catch (Throwable t)
      {
         throw new ResponseContextException(createErrorResponse(t, 500));
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   public ResponseContext putMedia(RequestContext request)
   {
      try
      {
         Connection connection = getConnection(request);
         ContentStream content =
            new BaseContentStream(request.getInputStream(), null, convertMimeType(request.getContentType()));
         ChangeTokenHolder changeTokenHolder = new ChangeTokenHolder();
         // TODO : is correct ?
         changeTokenHolder.setValue(request.getHeader(HttpHeaders.IF_MATCH));
         boolean overwriteFlag = getBooleanParameter(request, AtomCMIS.PARAM_OVERWRITE_FLAG, true);
         String updatedId = connection.setContentStream(getId(request), content, changeTokenHolder, overwriteFlag);
         CmisObject updated = connection.getProperties(updatedId, true, CmisConstants.CHANGE_TOKEN);
         ResponseContext response = new EmptyResponseContext(201);
         String contentLink = getContentLink(getId(request), request);
         response.setHeader(HttpHeaders.CONTENT_LOCATION, contentLink);
         response.setHeader(HttpHeaders.LOCATION, contentLink);
         String changeToken = updated.getObjectInfo().getChangeToken();
         if (changeToken != null)
         {
            response.setEntityTag(changeToken);
         }
         return response;
      }
      catch (IOException ioe)
      {
         return createErrorResponse(ioe, 500);
      }
      catch (ConstraintException cve)
      {
         return createErrorResponse(cve, 409);
      }
      catch (StorageException re)
      {
         return createErrorResponse(re, 409);
      }
      catch (ContentAlreadyExistsException ce)
      {
         return createErrorResponse(ce, 409);
      }
      catch (StreamNotSupportedException se)
      {
         // XXX : specification says 403, is it correct ?
         return createErrorResponse(se, 400);
      }
      catch (UpdateConflictException uce)
      {
         return createErrorResponse(uce, 409);
      }
      catch (Throwable t)
      {
         return createErrorResponse(t, 500);
      }
   }

   /**
    * Process rendition links.
    *
    * @param entry the entry
    * @param object the object
    * @param request the request
    *
    * @throws ResponseContextException the response context exception
    */
   private void processRenditionLinks(Entry entry, CmisObject object, RequestContext request)
      throws ResponseContextException
   {
      String baseRenditionHref = getBaseRenditionHref(getId(object), request);

      List<Rendition> renditionList = object.getRenditions();
      for (Rendition rendition : renditionList)
      {
         Link link = entry.addLink(//
            baseRenditionHref + "/" + rendition.getStreamId(), //
            AtomCMIS.LINK_ALTERNATE, //
            rendition.getMimeType(), //
            rendition.getTitle(), //
            null, //
            rendition.getLength());
         link.setAttributeValue(AtomCMIS.RENDITION_KIND, rendition.getKind());
         entry.addLink(link);
      }
   }

   /**
    * {@inheritDoc}
    */
   @Override
   protected String addEntryDetails(RequestContext request, Entry entry, IRI feedIri, CmisObject object)
      throws ResponseContextException
   {
      String objectId = getId(object);
      entry.setId(objectId);
      // FIXME updated and published is incorrect when pass Date.
      // Abdera uses Calendar.getInstance(TimeZone.getTimeZone("GMT"))
      // See org.apache.abdera.model.AtomDate .
      entry.setPublished(AtomUtils.getAtomDate(getCreationDate(object)));
      entry.setUpdated(AtomUtils.getAtomDate(getLastModificationDate(object)));
      entry.setSummary("");
      for (Person person : getAuthors(object, request))
      {
         entry.addAuthor(person);
      }

      entry.setTitle(getTitle(object));

      // Service link.
      String service = getServiceLink(request);
      entry.addLink(service, AtomCMIS.LINK_SERVICE, AtomCMIS.MEDIATYPE_ATOM_SERVICE, null, null, -1);

      String self = getObjectLink(objectId, request);
      // Self link.
      entry.addLink(self, AtomCMIS.LINK_SELF);
      // Edit link.
      entry.addLink(self, AtomCMIS.LINK_EDIT);

      // Alternate links.
      processRenditionLinks(entry, object, request);

      // Object type link.
      String typeId = object.getObjectInfo().getTypeId();
      entry.addLink(getObjectTypeLink(typeId, request), AtomCMIS.LINK_DESCRIBEDBY, AtomCMIS.MEDIATYPE_ATOM_ENTRY, null,
         null, -1);

      // Allowable actions link.
      entry.addLink(getAllowableActionsLink(objectId, request), AtomCMIS.LINK_CMIS_ALLOWABLEACTIONS,
         AtomCMIS.MEDIATYPE_ALLOWABLE_ACTIONS, null, null, -1);

      BaseType baseType = getBaseObjectType(object);
      if (baseType == BaseType.FOLDER)
      {
         // Relationships link.
         String relationships = getRelationshipsLink(objectId, request);
         entry.addLink(relationships, AtomCMIS.LINK_CMIS_RELATIONSHIPS, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // Policies link.
         String policies = getPoliciesLink(objectId, request);
         entry.addLink(policies, AtomCMIS.LINK_CMIS_POLICIES, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // ACL link.
         String acl = getACLLink(objectId, request);
         entry.addLink(acl, AtomCMIS.LINK_CMIS_ACL, AtomCMIS.MEDIATYPE_ACL, null, null, -1);

         // Children link.
         String children = getChildrenLink(objectId, request);
         entry.addLink(children, AtomCMIS.LINK_DOWN, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // Descendants link. Provided only if repository support descendants feature.
         String descendants = getDescendantsLink(objectId, request);
         if (descendants != null)
         {
            entry.addLink(descendants, AtomCMIS.LINK_DOWN, AtomCMIS.MEDIATYPE_CMISTREE, null, null, -1);
         }

         // Folder tree. link. Provided only if repository support folder tree feature.
         String folderTree = getFolderTreeLink(objectId, request);
         if (folderTree != null)
         {
            entry.addLink(folderTree, AtomCMIS.LINK_CMIS_FOLDERTREE, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);
         }

         // Parent link.
         String parentId = object.getObjectInfo().getParentId();
         if (parentId != null)
         {
            // Not provided for root folder.
            String parent = getObjectLink(parentId, request);
            entry.addLink(parent, AtomCMIS.LINK_UP, AtomCMIS.MEDIATYPE_ATOM_ENTRY, null, null, -1);
         }

         // Must have 'content' element to conform Atom specification.
         String name = object.getObjectInfo().getName();
         entry.setContent(name);
      }
      else if (baseType == BaseType.DOCUMENT)
      {
         // Relationship link.
         String relationships = getRelationshipsLink(objectId, request);
         entry.addLink(relationships, AtomCMIS.LINK_CMIS_RELATIONSHIPS, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // Policies link
         String policies = getPoliciesLink(objectId, request);
         entry.addLink(policies, AtomCMIS.LINK_CMIS_POLICIES, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // ACL link
         String acl = getACLLink(objectId, request);
         entry.addLink(acl, AtomCMIS.LINK_CMIS_ACL, AtomCMIS.MEDIATYPE_ACL, null, null, -1);

         // All versions
         String versionSeriesId = object.getObjectInfo().getVersionSeriesId();
         String allVersions = getAllVersionsLink(versionSeriesId, request);
         entry.addLink(allVersions, AtomCMIS.LINK_VERSION_HISTORY, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // Latest version link
         StringBuilder sb = new StringBuilder();
         sb.append(self).append('?').append(AtomCMIS.PARAM_RETURN_VERSION).append('=').append(
            EnumReturnVersion.LATEST.value());
         entry.addLink(sb.toString(), AtomCMIS.LINK_CURRENT_VERSION, AtomCMIS.MEDIATYPE_ATOM_ENTRY, null, null, -1);

         // PWC link if it exists.
         String checkedoutProperty = object.getObjectInfo().getVersionSeriesCheckedOutId();
         if (checkedoutProperty != null)
         {
            String pwcLink = getObjectLink(checkedoutProperty, request);
            entry.addLink(pwcLink, AtomCMIS.LINK_WORKING_COPY, AtomCMIS.MEDIATYPE_ATOM_ENTRY, null, null, -1);
         }

         // Parents link.
         String parent = getParentsLink(objectId, request);
         entry.addLink(parent, AtomCMIS.LINK_UP, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // Edit-media link.
         String contentLink = getContentLink(objectId, request);
         entry.addLink(contentLink, AtomCMIS.LINK_EDIT_MEDIA);

         // Content element.
         String contentType = getContentType(object);
         if (contentType != null)
         {
            entry.setContent(new IRI(contentLink), contentType);
         }
         else
         {
            Factory factory = request.getAbdera().getFactory();
            Content content = factory.newContent();
            content.setContentType(null);
            content.setSrc(contentLink);
            entry.setContentElement(content);
         }
      }
      else if (baseType == BaseType.POLICY)
      {
         // Relationships link.
         String relationships = getRelationshipsLink(objectId, request);
         entry.addLink(relationships, AtomCMIS.LINK_CMIS_RELATIONSHIPS, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // Policy link.
         String policies = getPoliciesLink(objectId, request);
         entry.addLink(policies, AtomCMIS.LINK_CMIS_POLICIES, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // ACL link.
         String acl = getACLLink(objectId, request);
         entry.addLink(acl, AtomCMIS.LINK_CMIS_ACL, AtomCMIS.MEDIATYPE_ACL, null, null, -1);

         // Parents link.
         String parent = getParentsLink(objectId, request);
         entry.addLink(parent, AtomCMIS.LINK_UP, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

         // Must have 'content' element to conform Atom specification.
         String name = object.getObjectInfo().getName();
         entry.setContent(name);
      }
      else if (baseType == BaseType.RELATIONSHIP)
      {
         // Relationship source link.
         String sourceId = object.getObjectInfo().getSourceId();
         entry.addLink(getObjectLink(sourceId, request), AtomCMIS.LINK_CMIS_SOURCE, AtomCMIS.MEDIATYPE_ATOM_ENTRY,
            null, null, -1);

         // Relationship target link.
         String targetId = object.getObjectInfo().getTargetId();
         entry.addLink(getObjectLink(targetId, request), AtomCMIS.LINK_CMIS_TARGET, AtomCMIS.MEDIATYPE_ATOM_ENTRY,
            null, null, -1);

         // Must have 'content' element to conform Atom specification.
         entry.setContent(getName(object));
      }

      ObjectTypeElement objectElement = new ObjectTypeElement(request.getAbdera().getFactory(), AtomCMIS.OBJECT);
      objectElement.build(object);
      entry.addExtension(objectElement);

      return self;
   }

   @SuppressWarnings("unchecked")
   protected org.xcmis.spi.utils.MimeType convertMimeType(MimeType abderaMimeType)
   {
      if (abderaMimeType == null)
      {
         return new org.xcmis.spi.utils.MimeType();
      }
      MimeTypeParameterList abderaParameters = abderaMimeType.getParameters();
      Map<String, String> paremeters = new HashMap<String, String>();
      for (Enumeration<String> names = abderaParameters.getNames(); names.hasMoreElements();)
      {
         String name = names.nextElement();
         paremeters.put(name, abderaParameters.get(name));
      }
      return new org.xcmis.spi.utils.MimeType(abderaMimeType.getPrimaryType(), abderaMimeType.getSubType(), paremeters);
   }

   /**
    * {@inheritDoc}
    */
   @Override
   protected Feed createFeedBase(RequestContext request) throws ResponseContextException
   {
      Factory factory = request.getAbdera().getFactory();
      Feed feed = factory.newFeed();
      feed.setId(getId(request));
      feed.setTitle(getTitle(request));
      feed.addAuthor(getAuthor(request));
      // FIXME updated is incorrect when pass Date.
      // Abdera uses Calendar.getInstance(TimeZone.getTimeZone("GMT"))
      // See org.apache.abdera.model.AtomDate .
      feed.setUpdated(AtomUtils.getAtomDate(Calendar.getInstance())); // TODO proper date

      String service = getServiceLink(request);
      feed.addLink(service, AtomCMIS.LINK_SERVICE, AtomCMIS.MEDIATYPE_ATOM_SERVICE, null, null, -1);

      String self = getSelfLink(getId(request), request);
      feed.addLink(self, AtomCMIS.LINK_SELF, AtomCMIS.MEDIATYPE_ATOM_FEED, null, null, -1);

      String via = getObjectLink(getId(request), request);
      feed.addLink(via, AtomCMIS.LINK_VIA, AtomCMIS.MEDIATYPE_ATOM_ENTRY, null, null, -1);

      return feed;
   }

   /**
    * Get link to AtomPub Document that describes object's ACL.
    *
    * @param id object id
    * @param request request context
    * @return link to allowable actions document
    */
   protected String getACLLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "objacl");
      params.put("id", id);
      String actions = request.absoluteUrlFor(TargetType.ENTRY, params);
      return actions;
   }

   /**
    * Get link to AtomPub Document that describes object's allowable actions.
    *
    * @param id object id
    * @param request request context
    * @return link to allowable actions document
    */
   protected String getAllowableActionsLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "allowableactions");
      params.put("id", id);
      String actions = request.absoluteUrlFor(TargetType.ENTRY, params);
      return actions;
   }

   /**
    * Get link to AtomPub Document that describes object's versions.
    *
    * @param id objects id
    * @param request request context
    * @return link to AtomPub Document that describes object's versions
    */
   protected String getAllVersionsLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "versions");
      params.put("id", id);
      String parents = request.absoluteUrlFor(TargetType.ENTRY, params);
      return parents;
   }

   /**
    * Get object's base type.
    *
    * @param object object
    * @return object's base type
    */
   protected BaseType getBaseObjectType(CmisObject object)
   {
      return object.getObjectInfo().getBaseType();
   }

   /**
    * Get link to express renditions.
    *
    * @param id objects id
    * @param request request context
    * @return link to AtomPub Document that describes object's parent(s)
    */
   protected String getBaseRenditionHref(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "alternate");
      params.put("id", id);
      String link = request.absoluteUrlFor(TargetType.ENTRY, params);
      return link;
   }

   /**
    * Get link to AtomPub document that describes folder's children.
    *
    * @param id folder id
    * @param request the request context
    * @return link to AtomPub document that describes folder's children
    */
   protected String getChildrenLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "children");
      params.put("id", id);
      String children = request.absoluteUrlFor(TargetType.ENTRY, params);
      return children;
   }

   /**
    * Get self link which provides the URI to retrieve this resource again.
    *
    * The atom:link with relation self MUST be generated to return the URI of
    * the feed. If paging or any other mechanism is used to filter, sort, or
    * change the representation of the feed, the URI MUST point back a resource
    * with the same representation.
    *
    * @param id the object id
    * @param request the request context
    * @return link which provides the URI to retrieve this resource again.
    */
   protected String getSelfLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", getHref().substring(1));
      params.put("id", id);
      String children = request.absoluteUrlFor(TargetType.ENTRY, params) + "?" + request.getUri().getQuery();
      return children;
   }

   /**
    * Get link to document content.
    *
    * @param id document id
    * @param request request context
    * @return link to document content
    */
   protected String getContentLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "file");
      params.put("id", id);
      String content = request.absoluteUrlFor(TargetType.ENTRY, params);
      return content;
   }

   /**
    * Get content stream from entry.
    *
    * @param entry source entry
    * @param request request context
    * @return content stream as <code>ContentStream</code> or null if there is
    *         no 'content' in entry
    * @throws IOException if any i/o error occurs
    * @throws ResponseContextException other errors
    */
   protected ContentStream getContentStream(Entry entry, RequestContext request) throws IOException,
      ResponseContextException
   {
      ContentStream contentStream = null;
      ContentTypeElement cmisContent = entry.getExtension(AtomCMIS.CONTENT);
      if (cmisContent != null)
      {
         CmisContentType content = cmisContent.getContent();
         String base64 = content.getBase64();
         byte[] data = null;
         if (base64 != null)
         {
            data = Base64.decodeBase64(base64.getBytes());
         }
         contentStream =
            new BaseContentStream(data, null, org.xcmis.spi.utils.MimeType.fromString(content.getMediatype()));
      }
      else
      {
         Content content = entry.getContentElement();
         if (content != null)
         {
            final IRI src = content.getSrc();
            if (src != null)
            {
               if (src.equals(new IRI(getContentLink(getId(request), request))))
               {
                  // If 'src' attribute provides URI is the same to current
                  // object (document). This may happen when client does 'check-in'
                  // or 'check-out' operation.
                  // TODO : need to do anything ??
               }
               else
               {
                  URL url = null;
                  try
                  {
                     url = src.toURL();
                  }
                  catch (URISyntaxException e)
                  {
                     String msg = "Invalid src attribute: " + src;
                     throw new ResponseContextException(createErrorResponse(msg, 400));
                  }
                  // HTTP only
                  HttpURLConnection httpConnection = (HttpURLConnection)url.openConnection();
                  httpConnection.setRequestMethod("GET");
                  httpConnection.setDoOutput(false);
                  httpConnection.setDoInput(true);
                  int status = httpConnection.getResponseCode();
                  if (200 == status)
                  {
                     contentStream =
                        new BaseContentStream(new HttpConnectionStream(httpConnection), null,
                           org.xcmis.spi.utils.MimeType.fromString(httpConnection.getHeaderField("Content-Type")));
                  }
                  else
                  {
                     httpConnection.disconnect();
                     String msg = "Unable get content from URI : " + src.toString() + ". Response status is " + status;
                     throw new ResponseContextException(createErrorResponse(msg, 500));
                  }
               }
            }
            else
            {
               Type contentType = content.getContentType();
               org.xcmis.spi.utils.MimeType mediaType;
               if (contentType == Type.XML || contentType == Type.XHTML || contentType == Type.HTML
                  || contentType == Type.TEXT)
               {
                  switch (contentType)
                  {
                     case XHTML :
                        mediaType = new org.xcmis.spi.utils.MimeType("application", "xhtml+xml");
                        break;
                     case HTML :
                        mediaType = new org.xcmis.spi.utils.MimeType("text", "html");
                        break;
                     case TEXT :
                        mediaType = new org.xcmis.spi.utils.MimeType("text", "plain");
                        break;
                     case XML :
                        mediaType = convertMimeType(content.getMimeType());
                        break;
                     default :
                        // Must never happen.
                        mediaType = new org.xcmis.spi.utils.MimeType();
                  }

                  byte[] data;
                  // XXX CMISSpaces sends XML content as Base64 encoded but
                  // Abdera waits for plain text.
                  // TODO Done just for research work. Find good solution to fix this.
                  if (SPACES_AIR_SPECIFIC_REFERER.equalsIgnoreCase(request.getHeader("referer")))
                  {
                     data = Base64.decodeBase64(content.getText().getBytes());
                  }
                  else
                  {
                     String charset = mediaType.getParameter(CmisConstants.CHARSET);
                     if (charset == null)
                     {
                        // workaround
                        mediaType.getParameters().put(CmisConstants.CHARSET, "UTF-8");
                     }
                     data = content.getValue().getBytes(charset == null ? "UTF-8" : charset);

                  }

                  contentStream = new BaseContentStream(data, null, mediaType);
               }
               else
               {
                  contentStream =
                     new BaseContentStream(content.getDataHandler().getInputStream(), null, convertMimeType(content
                        .getMimeType()));
               }
            }
         }
      }
      return contentStream;
   }

   /**
    * Get object creation date.
    *
    * @param object source object
    * @return creation date
    */
   protected Calendar getCreationDate(CmisObject object)
   {
      Calendar creationDate = object.getObjectInfo().getCreationDate();
      if (creationDate == null)
      {
         creationDate = Calendar.getInstance();
      }
      return creationDate;
   }

   /**
    * Get link to AtomPub document that describes folder's descendants. If
    * repository does not support capability 'getDescendants' this method will
    * return null.
    *
    * @param id folder id
    * @param repository info
    * @param request request context
    * @return link to AtomPub document that describes folder's descendants or
    *         null if capability 'getDescendants' is not supported.
    * @see RepositoryInfo
    */
   protected String getDescendantsLink(String id, /*RepositoryInfo repoInfo,*/RequestContext request)
   {
      String children = null;
      Connection connection = getConnection(request);
      RepositoryCapabilities capabilities = connection.getStorage().getRepositoryInfo().getCapabilities();
      if (capabilities.isCapabilityGetDescendants())
      {
         Map<String, String> params = new HashMap<String, String>();
         params.put("repoid", getRepositoryId(request));
         params.put("atomdoctype", "descendants");
         params.put("id", id);
         children = request.absoluteUrlFor("feed", params);
      }
      return children;
   }

   /**
    * Get link to AtomPub document that describes folder's tree. If repository
    * does not support capability 'getFolderTree' this method will return null.
    *
    * @param id folder id
    * @param request request context
    * @return link to AtomPub document that describes folder's tree or null if
    *         capability 'getFolderTree' is not supported.
    */
   protected String getFolderTreeLink(String id, RequestContext request)
   {
      String children = null;
      Connection conn = getConnection(request);
      RepositoryCapabilities capabilities = conn.getStorage().getRepositoryInfo().getCapabilities();
      if (capabilities.isCapabilityGetFolderTree())
      {
         Map<String, String> params = new HashMap<String, String>();
         params.put("repoid", getRepositoryId(request));
         params.put("atomdoctype", "foldertree");
         params.put("id", id);
         children = request.absoluteUrlFor("feed", params);
      }
      return children;
   }

   /**
    * Get object creation date.
    *
    * @param object source object
    * @return creation date
    */
   protected Calendar getLastModificationDate(CmisObject object)
   {
      Calendar lastModification = object.getObjectInfo().getLastModificationDate();
      if (lastModification == null)
      {
         lastModification = Calendar.getInstance();
      }
      return lastModification;
   }

   /**
    * Get link to AtomPub Document that describes object with <code>id</code>.
    *
    * @param id object id
    * @param request request context
    * @return link to AtomPub Document that describes object with
    *         <code>id</code>
    */
   protected String getObjectLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "object");
      params.put("id", id);
      String link = request.absoluteUrlFor(TargetType.ENTRY, params);
      return link;
   }

   /**
    * Get link to AtomPub Document that describes object's parent(s).
    *
    * @param id objects id
    * @param request request context
    * @return link to AtomPub Document that describes object's parent(s)
    */
   protected String getParentsLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "parents");
      params.put("id", id);
      String parents = request.absoluteUrlFor("feed", params);
      return parents;
   }

   /**
    * Get link to AtomPub Document that describes object's policies.
    *
    * @param id objects id
    * @param request request context
    * @return link to AtomPub Document that describes policies applied to object
    */
   protected String getPoliciesLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "policies");
      params.put("id", id);
      String parents = request.absoluteUrlFor("feed", params);
      return parents;
   }

   /**
    * Get object's property.
    *
    * @param object object
    * @param propertyName property name
    * @return property or null if property does not exist
    */
   protected Property<?> getProperty(CmisObject object, String propertyName)
   {
      Map<String, Property<?>> properties = object.getProperties();
      Property<?> property = null;
      if (properties != null && !properties.isEmpty())
      {
         //         for (Property<?> prop : properties.values())
         //         {
         //            if (propertyName.equals(prop.getId()))
         //            {
         //               return prop;
         //            }
         //         }
         property = properties.get(propertyName);
      }
      return property;
   }

   /**
    * Get link to AtomPub Document that describes object's relationships.
    *
    * @param id objects id
    * @param request request context
    * @return link to AtomPub Document that describes object's relationships
    */
   protected String getRelationshipsLink(String id, RequestContext request)
   {
      Map<String, String> params = new HashMap<String, String>();
      params.put("repoid", getRepositoryId(request));
      params.put("atomdoctype", "relationships");
      params.put("id", id);
      String parents = request.absoluteUrlFor("feed", params);
      return parents;
   }

   /**
    * Get's the name of the specific resource requested.
    *
    * @param request RequestContext
    * @return string resource name
    */
   @Override
   protected String getResourceName(RequestContext request)
   {
      // TODO : can get smarter?
      String path = request.getTarget().getParameter("path");
      if (path != null)
      {
         try
         {
            path = URLDecoder.decode(path, "UTF-8");
         }
         catch (UnsupportedEncodingException uee)
         {
         }
         return path.charAt(0) == '/' ? path : ('/' + path);
      }
      return super.getResourceName(request);
   }

   /**
    * From specification (1.0-cd06), section 3.5.2 Entries. When POSTing an Atom
    * Document, the Atom elements MUST take precedence over the corresponding
    * writable CMIS property. For example, atom:title will overwrite cmis:name.
    *
    * @param object CMIS object
    * @param entry entry that delivered CMIS object.
    */
   protected void updatePropertiesFromEntry(CmisObject object, Entry entry)
   {
      // SPEC.: 3.5.2 Entries
      // atom:title MUST be the cmis:name property
      String title = entry.getTitle();
      if (title != null && title.length() > 0)
      {
         // Should never be null, but check it to avoid overwriting existed cmis:name property.
         StringProperty prop = (StringProperty)getProperty(object, CmisConstants.NAME);
         if (prop == null)
         {
            prop = new StringProperty();
            prop.setId(CmisConstants.NAME);
            prop.setLocalName(CmisConstants.NAME);
            prop.getValues().add(title);
            object.getProperties().put(prop.getId(), prop);
         }
         else
         {
            prop.getValues().clear();
            prop.getValues().add(title);
         }
      }
      // atom:author/atom:name MUST be cmis:createdBy
      String author = entry.getAuthor() != null ? entry.getAuthor().getName() : null;
      if (author != null && author.length() > 0)
      {
         StringProperty prop = (StringProperty)getProperty(object, CmisConstants.CREATED_BY);
         if (prop == null)
         {
            prop = new StringProperty();
            prop.setId(CmisConstants.CREATED_BY);
            prop.setLocalName(CmisConstants.CREATED_BY);
            prop.getValues().add(author);
            object.getProperties().put(prop.getId(), prop);
         }
         else
         {
            prop.getValues().clear();
            prop.getValues().add(author);
         }
      }
   }

}
