/**
 * 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.wssoap.impl;

import org.xcmis.core.CmisAccessControlListType;
import org.xcmis.core.CmisAllowableActionsType;
import org.xcmis.core.CmisObjectType;
import org.xcmis.core.CmisPropertiesType;
import org.xcmis.core.CmisRenditionType;
import org.xcmis.core.EnumIncludeRelationships;
import org.xcmis.core.EnumUnfileObject;
import org.xcmis.core.EnumVersioningState;
import org.xcmis.messaging.CmisContentStreamType;
import org.xcmis.messaging.CmisExtensionType;
import org.xcmis.messaging.DeleteTreeResponse;
import org.xcmis.soap.CmisException;
import org.xcmis.soap.ObjectServicePort;
import org.xcmis.spi.BaseContentStream;
import org.xcmis.spi.ChangeTokenHolder;
import org.xcmis.spi.CmisConstants;
import org.xcmis.spi.CmisRegistry;
import org.xcmis.spi.Connection;
import org.xcmis.spi.ConstraintException;
import org.xcmis.spi.ContentStream;
import org.xcmis.spi.model.IncludeRelationships;
import org.xcmis.spi.model.UnfileObject;
import org.xcmis.spi.model.VersioningState;
import org.xcmis.spi.utils.Logger;
import org.xcmis.spi.utils.MimeType;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.util.List;

import javax.activation.DataHandler;
import javax.activation.DataSource;

/**
 * @author <a href="mailto:max.shaposhnik@exoplatform.com">Max Shaposhnik</a>
 * @version $Id: ObjectServicePortImpl.java 2 2010-02-04 17:21:49Z andrew00x $
 */
@javax.jws.WebService(// name = "ObjectServicePort",
serviceName = "ObjectService", //
portName = "ObjectServicePort", //
targetNamespace = "http://docs.oasis-open.org/ns/cmis/ws/200908/", //
wsdlLocation = "/wsdl/CMISWS-Service.wsdl" //,
//      endpointInterface = "org.xcmis.soap.ObjectServicePort"
)
public class ObjectServicePortImpl implements ObjectServicePort
{

   /** Logger. */
   private static final Logger LOG = Logger.getLogger(ObjectServicePortImpl.class);

   /**
    * Constructs instance of <code>ObjectServicePortImpl</code> .
    *
    */
   public ObjectServicePortImpl()
   {
   }

   /**
    * {@inheritDoc}
    */
   public void createDocument(String repositoryId, //
      CmisPropertiesType properties, //
      String folderId, //
      CmisContentStreamType contentStream, //
      EnumVersioningState versioningState, //
      List<String> policies, //
      CmisAccessControlListType addACEs, //
      CmisAccessControlListType removeACEs, //
      javax.xml.ws.Holder<CmisExtensionType> extension, //
      javax.xml.ws.Holder<String> objectId) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation createDocument");
      }
      ContentStream cs = null;
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         if (contentStream != null)
         {
            cs =
               new BaseContentStream(contentStream.getStream().getInputStream(), contentStream.getFilename(), MimeType
                  .fromString(contentStream.getMimeType()));
         }
         objectId.value =
            conn.createDocument(folderId, //
               TypeConverter.getPropertyMap(properties), //
               cs, //
               TypeConverter.getListAccessControlEntry(addACEs), //
               TypeConverter.getListAccessControlEntry(removeACEs), //
               policies, versioningState == null ? VersioningState.MAJOR : VersioningState.fromValue(versioningState
                  .value()) //
               );
      }
      catch (Exception e)
      {
         LOG.error("Create document error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void createDocumentFromSource(String repositoryId, //
      String sourceId, //
      CmisPropertiesType properties, //
      String folderId, //
      EnumVersioningState versioningState, //
      List<String> policies, //
      CmisAccessControlListType addACEs, //
      CmisAccessControlListType removeACEs, //
      javax.xml.ws.Holder<CmisExtensionType> extension, //
      javax.xml.ws.Holder<String> objectId) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation createDocumentFromSource");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         objectId.value =
            conn.createDocumentFromSource(sourceId, //
               folderId, //
               TypeConverter.getPropertyMap(properties), //
               TypeConverter.getListAccessControlEntry(addACEs), //
               TypeConverter.getListAccessControlEntry(removeACEs), //
               policies, versioningState == null ? VersioningState.MAJOR : VersioningState.fromValue(versioningState
                  .value()) //
               );
      }
      catch (Exception e)
      {
         LOG.error("Create document from source error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }

   }

   /**
    * {@inheritDoc}
    */
   public void createFolder(String repositoryId, //
      CmisPropertiesType properties, //
      String folderId, //
      List<String> policies, //
      CmisAccessControlListType addACEs, //
      CmisAccessControlListType removeACEs, //
      javax.xml.ws.Holder<CmisExtensionType> extension, //
      javax.xml.ws.Holder<String> objectId) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation createFolder");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         objectId.value =
            conn.createFolder(folderId, TypeConverter.getPropertyMap(properties), TypeConverter
               .getListAccessControlEntry(addACEs), //
               TypeConverter.getListAccessControlEntry(removeACEs), //
               policies);
      }
      catch (Exception e)
      {
         LOG.error("Create folder error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void createPolicy(String repositoryId, //
      CmisPropertiesType properties, //
      String folderId, //
      List<String> policies, //
      CmisAccessControlListType addACEs, //
      CmisAccessControlListType removeACEs, //
      javax.xml.ws.Holder<CmisExtensionType> extension, //
      javax.xml.ws.Holder<String> objectId) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation createPolicy");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         objectId.value =
            conn.createPolicy(folderId, TypeConverter.getPropertyMap(properties), TypeConverter
               .getListAccessControlEntry(addACEs), //
               TypeConverter.getListAccessControlEntry(removeACEs), policies);
      }
      catch (Exception e)
      {
         LOG.error("Create policy error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void createRelationship(String repositoryId, //
      CmisPropertiesType properties, //
      List<String> policies, //
      CmisAccessControlListType addACEs, //
      CmisAccessControlListType removeACEs, //
      javax.xml.ws.Holder<CmisExtensionType> extension, //
      javax.xml.ws.Holder<String> objectId) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation createRelationship");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         objectId.value =
            conn.createRelationship(TypeConverter.getPropertyMap(properties), TypeConverter
               .getListAccessControlEntry(addACEs), //
               TypeConverter.getListAccessControlEntry(removeACEs), policies);
      }
      catch (Exception e)
      {
         LOG.error("Create relationship error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void deleteContentStream(String repositoryId, javax.xml.ws.Holder<String> documentId,
      javax.xml.ws.Holder<String> changeToken, javax.xml.ws.Holder<CmisExtensionType> extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation deleteContentStream");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         ChangeTokenHolder hold = new ChangeTokenHolder();
         if (changeToken != null)
         {
            hold.setValue(changeToken.value);
         }
         documentId.value = conn.deleteContentStream(documentId.value, //
            changeToken != null ? hold : null);
      }
      catch (Exception e)
      {
         LOG.error("Delete document's content error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public CmisExtensionType deleteObject(String repositoryId, String objectId, Boolean allVersions,
      CmisExtensionType extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation deleteObject");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         conn.deleteObject(objectId, allVersions);
      }
      catch (Exception e)
      {
         LOG.error("Delete object error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
      return new CmisExtensionType();
   }

   /**
    * {@inheritDoc}
    */
   public DeleteTreeResponse.FailedToDelete deleteTree(String repositoryId, //
      String folderId, //
      Boolean allVersions, //
      EnumUnfileObject unfileObject, //
      Boolean continueOnFailure, //
      CmisExtensionType extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation deleteTree");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         DeleteTreeResponse.FailedToDelete failed = new DeleteTreeResponse.FailedToDelete();
         failed.getObjectIds().addAll(conn.deleteTree(folderId, //
            allVersions, //
            unfileObject == null ? UnfileObject.DELETE : UnfileObject.fromValue(unfileObject.value()), //
            continueOnFailure == null ? false : continueOnFailure));
         return failed;
      }
      catch (Exception e)
      {
         LOG.error("Delete folder tree error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public CmisAllowableActionsType getAllowableActions(String repositoryId, String objectId, CmisExtensionType extension)
      throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation getAllowableActions");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         return TypeConverter.getCmisAllowableActionsType(conn.getAllowableActions(objectId));
      }
      catch (Exception e)
      {
         LOG.error("Get allowable actions error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public CmisContentStreamType getContentStream(String repositoryId, //
      String objectId, //
      String streamId, //
      java.math.BigInteger offset, //
      java.math.BigInteger length, //
      CmisExtensionType extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation getContentStream");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);
         ContentStream cs = conn.getContentStream(objectId, streamId);
         String type = cs.getMediaType() == null ? "application/octet-stream" : cs.getMediaType().toString();
         CmisContentStreamType result = new CmisContentStreamType();
         result.setFilename(cs.getFileName());
         result.setMimeType(type);
         if (cs.length() > 0)
         {
            // Not clear need to set length of full content or part of it for range requests.
            result.setLength(BigInteger.valueOf(cs.length()));
         }
         InputStream in = cs.getStream();
         if (offset == null && length == null)
         {
            result.setStream(new DataHandler(new InputStreamDataSource(in, type)));
         }
         else
         {
            result.setStream(new DataHandler(new InputStreamDataSource(
               new ContentRange(in, offset == null ? 0 : offset.longValue(), length == null ? -1 : length.longValue()),
               type)));
         }
         return result;
      }
      catch (Exception e)
      {
         LOG.error("Get content stream error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   private static class InputStreamDataSource implements DataSource
   {
      private final InputStream in;
      private final String type;

      private InputStreamDataSource(InputStream in, String type)
      {
         this.in = in;
         this.type = type;
      }

      @Override
      public InputStream getInputStream() throws IOException
      {
         return in;
      }

      @Override
      public OutputStream getOutputStream() throws IOException
      {
         return null;
      }

      @Override
      public String getContentType()
      {
         return type;
      }

      @Override
      public String getName()
      {
         return null;
      }
   }

   private static class ContentRange extends FilterInputStream
   {
      private long remaining;
      private final boolean needTruncate;

      private ContentRange(InputStream in, long offset, long length) throws IOException, ConstraintException
      {
         super(in);
         if (offset > 0)
         {
            if (in.skip(offset) < offset)
            {
               throw new ConstraintException("offset value is greater than the size of the content. ");
            }
         }
         needTruncate = length >= 0;
         remaining = length;
      }

      @Override
      public int read() throws IOException
      {
         if (needTruncate)
         {
            if (remaining > 0)
            {
               int b = super.read();
               remaining -= 1;
               return b;
            }
            return -1;
         }
         return super.read();
      }

      @Override
      public int read(byte[] b, int off, int len) throws IOException
      {
         if (needTruncate)
         {
            if (remaining > 0)
            {
               int n = super.read(b, off, (int)Math.min(len, remaining));
               remaining -= n;
               return n;
            }
            return -1;
         }
         return super.read(b, off, len);
      }
   }

   /**
    * {@inheritDoc}
    */
   public CmisObjectType getObject(String repositoryId, //
      String objectId, //
      String propertyFilter, //
      Boolean includeAllowableActions, //
      EnumIncludeRelationships includeRelationships, //
      String renditionFilter, //
      Boolean includePolicyIds, //
      Boolean includeACL, //
      CmisExtensionType extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation getObject");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         return TypeConverter.getCmisObjectType(conn.getObject(objectId, //
            includeAllowableActions == null ? false : includeAllowableActions, //
            includeRelationships == null ? IncludeRelationships.NONE : IncludeRelationships
               .fromValue(includeRelationships.value()), //
            includePolicyIds == null ? false : includePolicyIds, //
            includeACL == null ? false : includeACL, //
            false, propertyFilter, //
            renditionFilter));
      }
      catch (Exception e)
      {
         LOG.error("Get object error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }

   }

   /**
    * {@inheritDoc}
    */
   public CmisObjectType getObjectByPath(String repositoryId, //
      String path, //
      String propertyFilter, //
      Boolean includeAllowableActions, //
      EnumIncludeRelationships includeRelationships, //
      String renditionFilter, //
      Boolean includePolicyIds, //
      Boolean includeACL, //
      CmisExtensionType extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation getObjectByPath");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         return TypeConverter.getCmisObjectType(conn.getObjectByPath(path, //
            includeAllowableActions == null ? false : includeAllowableActions, //
            includeRelationships == null ? IncludeRelationships.NONE : IncludeRelationships
               .fromValue(includeRelationships.value()), //
            includePolicyIds == null ? false : includePolicyIds, //
            includeACL == null ? false : includeACL, //
            false, propertyFilter, //
            renditionFilter));
      }
      catch (Exception e)
      {
         LOG.error("Get object by path error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }

   }

   /**
    * {@inheritDoc}
    */
   public CmisPropertiesType getProperties(String repositoryId, String objectId, String propertyFilter,
      CmisExtensionType extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation getProperties");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         return TypeConverter.getCmisPropertiesType(conn.getProperties(objectId, false, propertyFilter));
      }
      catch (Exception e)
      {
         LOG.error("Get properties error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public List<CmisRenditionType> getRenditions(String repositoryId, String objectId, String renditionFilter,
      BigInteger maxItems, BigInteger skipCount, CmisExtensionType extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation getRenditions");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         return TypeConverter.getCmisRenditionTypeList(conn.getRenditions(objectId, //
            renditionFilter, //
            maxItems == null ? CmisConstants.MAX_ITEMS : maxItems.intValue(), //
            skipCount == null ? 0 : skipCount.intValue()));
      }
      catch (Exception e)
      {
         LOG.error("Get renditions error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void moveObject(String repositoryId, javax.xml.ws.Holder<String> objectId, String targetFolderId,
      String sourceFolderId, javax.xml.ws.Holder<CmisExtensionType> extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation moveObject");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         objectId.value = conn.moveObject(objectId.value, targetFolderId, sourceFolderId);
      }
      catch (Exception e)
      {
         LOG.error("Move object error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void setContentStream(String repositoryId, //
      javax.xml.ws.Holder<String> documentId, //
      Boolean overwriteFlag, //
      javax.xml.ws.Holder<String> changeToken, //
      CmisContentStreamType contentStream, //
      javax.xml.ws.Holder<CmisExtensionType> extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation setContentStream");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         ContentStream cs = null;
         if (contentStream != null)
         {
            cs =
               new BaseContentStream(contentStream.getStream().getInputStream(), contentStream.getFilename(), MimeType
                  .fromString(contentStream.getMimeType()));
         }

         ChangeTokenHolder hold = new ChangeTokenHolder();
         if (changeToken != null)
         {
            hold.setValue(changeToken.value);
         }

         documentId.value = conn.setContentStream(documentId.value, //
            cs, //
            changeToken == null ? null : hold, //
            overwriteFlag == null ? true : overwriteFlag);
      }
      catch (Exception e)
      {
         LOG.error("Set content stream error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

   /**
    * {@inheritDoc}
    */
   public void updateProperties(String repositoryId, //
      javax.xml.ws.Holder<String> objectId, //
      javax.xml.ws.Holder<String> changeToken, //
      CmisPropertiesType properties, //
      javax.xml.ws.Holder<CmisExtensionType> extension) throws CmisException
   {
      if (LOG.isDebugEnabled())
      {
         LOG.debug("Executing operation updateProperties");
      }
      Connection conn = null;
      try
      {
         conn = CmisRegistry.getInstance().getConnection(repositoryId);

         ChangeTokenHolder hold = new ChangeTokenHolder();
         if (changeToken != null)
         {
            hold.setValue(changeToken.value);
         }

         objectId.value = conn.updateProperties(objectId.value, //
            changeToken.value == null ? null : hold, //
            TypeConverter.getPropertyMap(properties));
      }
      catch (Exception e)
      {
         LOG.error("Update properties error: " + e.getMessage(), e);
         throw ExceptionFactory.generateException(e);
      }
      finally
      {
         if (conn != null)
         {
            conn.close();
         }
      }
   }

}
