content: /*
   * Licensed to the Apache Software Foundation (ASF) under one
   * or more contributor license agreements.  See the NOTICE file
   * distributed with this work for additional information
   * regarding copyright ownership.  The ASF licenses this file
   * to you under the Apache License, Version 2.0 (the
   * "License"); you may not use this file except in compliance
   * with the License.  You may obtain a copy of the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing,
   * software distributed under the License is distributed on an
   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   * KIND, either express or implied.  See the License for the
   * specific language governing permissions and limitations
   * under the License.
   */
  package org.apache.chemistry.opencmis.client.runtime;
  
  import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNullOrEmpty;
  
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.math.BigInteger;
  import java.util.ArrayList;
  import java.util.EnumSet;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Set;
  
  import org.apache.chemistry.opencmis.client.api.CmisObject;
  import org.apache.chemistry.opencmis.client.api.Document;
  import org.apache.chemistry.opencmis.client.api.DocumentType;
  import org.apache.chemistry.opencmis.client.api.ObjectFactory;
  import org.apache.chemistry.opencmis.client.api.ObjectId;
  import org.apache.chemistry.opencmis.client.api.ObjectType;
  import org.apache.chemistry.opencmis.client.api.OperationContext;
  import org.apache.chemistry.opencmis.client.api.Policy;
  import org.apache.chemistry.opencmis.client.api.Property;
  import org.apache.chemistry.opencmis.client.bindings.spi.LinkAccess;
  import org.apache.chemistry.opencmis.client.runtime.util.AppendOutputStream;
  import org.apache.chemistry.opencmis.commons.PropertyIds;
  import org.apache.chemistry.opencmis.commons.data.Ace;
  import org.apache.chemistry.opencmis.commons.data.ContentStream;
  import org.apache.chemistry.opencmis.commons.data.ContentStreamHash;
  import org.apache.chemistry.opencmis.commons.data.ObjectData;
  import org.apache.chemistry.opencmis.commons.data.PartialContentStream;
  import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
  import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
  import org.apache.chemistry.opencmis.commons.enums.Updatability;
  import org.apache.chemistry.opencmis.commons.enums.VersioningState;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamHashImpl;
  import org.apache.chemistry.opencmis.commons.spi.Holder;
  
  public class DocumentImpl extends AbstractFilableCmisObject implements Document {
  
      private static final long serialVersionUID = 1L;
  
      /**
       * Constructor.
       */
      public DocumentImpl(SessionImpl session, ObjectType objectType, ObjectData objectData, OperationContext context) {
          initialize(session, objectType, objectData, context);
      }
  
      @Override
      public DocumentType getDocumentType() {
          ObjectType objectType = super.getType();
          if (objectType instanceof DocumentType) {
              return (DocumentType) objectType;
          } else {
              throw new ClassCastException("Object type is not a document type.");
          }
      }
  
      @Override
      public boolean isVersionable() {
          return Boolean.TRUE.equals(getDocumentType().isVersionable());
      }
  
      @Override
      public Boolean isVersionSeriesPrivateWorkingCopy() {
          if (Boolean.FALSE.equals(getDocumentType().isVersionable())) {
              return false;
          }
  
          Boolean isCheckedOut = isVersionSeriesCheckedOut();
          if (Boolean.FALSE.equals(isCheckedOut)) {
              return false;
          }
  
          Boolean isPWC = isPrivateWorkingCopy();
          if (isPWC != null) {
              return isPWC;
          }
  
          String vsCoId = getVersionSeriesCheckedOutId();
          if (vsCoId == null) {
              // we don't know ...
              return null;
          }
  
          return vsCoId.equals(getId());
      }
  
      // properties
  
      @Override
      public String getCheckinComment() {
          return getPropertyValue(PropertyIds.CHECKIN_COMMENT);
      }
  
      @Override
      public String getVersionLabel() {
          return getPropertyValue(PropertyIds.VERSION_LABEL);
      }
  
      @Override
      public String getVersionSeriesId() {
          return getPropertyValue(PropertyIds.VERSION_SERIES_ID);
      }
  
      @Override
      public String getVersionSeriesCheckedOutId() {
          return getPropertyValue(PropertyIds.VERSION_SERIES_CHECKED_OUT_ID);
      }
  
      @Override
      public String getVersionSeriesCheckedOutBy() {
          return getPropertyValue(PropertyIds.VERSION_SERIES_CHECKED_OUT_BY);
      }
  
      @Override
      public Boolean isImmutable() {
          return getPropertyValue(PropertyIds.IS_IMMUTABLE);
      }
  
      @Override
      public Boolean isLatestMajorVersion() {
          return getPropertyValue(PropertyIds.IS_LATEST_MAJOR_VERSION);
      }
  
      @Override
      public Boolean isLatestVersion() {
          return getPropertyValue(PropertyIds.IS_LATEST_VERSION);
      }
  
      @Override
      public Boolean isMajorVersion() {
          return getPropertyValue(PropertyIds.IS_MAJOR_VERSION);
      }
  
      @Override
      public Boolean isPrivateWorkingCopy() {
          return getPropertyValue(PropertyIds.IS_PRIVATE_WORKING_COPY);
      }
  
      @Override
      public Boolean isVersionSeriesCheckedOut() {
          return getPropertyValue(PropertyIds.IS_VERSION_SERIES_CHECKED_OUT);
      }
  
      @Override
      public long getContentStreamLength() {
          BigInteger bigInt = getPropertyValue(PropertyIds.CONTENT_STREAM_LENGTH);
          return bigInt == null ? (long) -1 : bigInt.longValue();
      }
  
      @Override
      public String getContentStreamMimeType() {
          return getPropertyValue(PropertyIds.CONTENT_STREAM_MIME_TYPE);
      }
  
      @Override
      public String getContentStreamFileName() {
          return getPropertyValue(PropertyIds.CONTENT_STREAM_FILE_NAME);
      }
  
      @Override
      public String getContentStreamId() {
          return getPropertyValue(PropertyIds.CONTENT_STREAM_ID);
      }
  
      @Override
      public List<ContentStreamHash> getContentStreamHashes() {
          List<String> hashes = getPropertyValue(PropertyIds.CONTENT_STREAM_HASH);
          if (isNullOrEmpty(hashes)) {
              return null;
          }
  
          List<ContentStreamHash> result = new ArrayList<ContentStreamHash>(hashes.size());
          for (String hash : hashes) {
              result.add(new ContentStreamHashImpl(hash));
          }
  
          return result;
      }
  
      @Override
      public String getLatestAccessibleStateId() {
          return getPropertyValue(PropertyIds.LATEST_ACCESSIBLE_STATE_ID);
      }
  
      // operations
  
      @Override
      public Document copy(ObjectId targetFolderId, Map<String, ?> properties, VersioningState versioningState,
              List<Policy> policies, List<Ace> addAces, List<Ace> removeAces, OperationContext context) {
  
          ObjectId newId = null;
          try {
              newId = getSession().createDocumentFromSource(this, properties, targetFolderId, versioningState, policies,
                      addAces, removeAces);
          } catch (CmisNotSupportedException nse) {
              newId = copyViaClient(targetFolderId, properties, versioningState, policies, addAces, removeAces);
          }
  
          // if no context is provided the object will not be fetched
          if (context == null || newId == null) {
              return null;
          }
          // get the new object
          CmisObject object = getSession().getObject(newId, context);
          if (!(object instanceof Document)) {
              throw new CmisRuntimeException("Newly created object is not a document! New id: " + newId);
          }
  
          return (Document) object;
      }
  
      @Override
      public Document copy(ObjectId targetFolderId) {
          return copy(targetFolderId, null, null, null, null, null, getSession().getDefaultContext());
      }
  
      /**
       * Copies the document manually. The content is streamed from the repository
       * and back.
       */
      protected ObjectId copyViaClient(ObjectId targetFolderId, Map<String, ?> properties,
              VersioningState versioningState, List<Policy> policies, List<Ace> addAces, List<Ace> removeAces) {
          Map<String, Object> newProperties = new HashMap<String, Object>();
  
          OperationContext allPropsContext = getSession().createOperationContext();
          allPropsContext.setFilterString("*");
          allPropsContext.setIncludeAcls(false);
          allPropsContext.setIncludeAllowableActions(false);
          allPropsContext.setIncludePathSegments(false);
          allPropsContext.setIncludePolicies(false);
          allPropsContext.setIncludeRelationships(IncludeRelationships.NONE);
          allPropsContext.setRenditionFilterString("cmis:none");
  
          Document allPropsDoc = (Document) getSession().getObject(this, allPropsContext);
  
          for (Property<?> prop : allPropsDoc.getProperties()) {
              if (prop.getDefinition().getUpdatability() == Updatability.READWRITE
                      || prop.getDefinition().getUpdatability() == Updatability.ONCREATE) {
                  newProperties.put(prop.getId(), prop.getValue());
              }
          }
  
          if (properties != null) {
              newProperties.putAll(properties);
          }
  
          ContentStream contentStream = allPropsDoc.getContentStream();
          try {
              return getSession().createDocument(newProperties, targetFolderId, contentStream, versioningState, policies,
                      addAces, removeAces);
          } finally {
              if (contentStream != null) {
                  InputStream stream = contentStream.getStream();
                  if (stream != null) {
                      try {
                          stream.close();
                      } catch (IOException ioe) {
                          throw new CmisRuntimeException("Cannot close source stream!", ioe);
                      }
                  }
              }
          }
      }
  
      @Override
      public void deleteAllVersions() {
          delete(true);
      }
  
      // versioning
  
      @Override
      public ObjectId checkOut() {
          String newObjectId = null;
  
          readLock();
          try {
              String objectId = getObjectId();
              Holder<String> objectIdHolder = new Holder<String>(objectId);
  
              getBinding().getVersioningService().checkOut(getRepositoryId(), objectIdHolder, null, null);
              newObjectId = objectIdHolder.getValue();
          } finally {
              readUnlock();
          }
  
          // remove original version from cache, the path and a few versioning
          // properties are not valid anymore
          getSession().removeObjectFromCache(this);
  
          if (newObjectId == null) {
              return null;
          }
  
          return getSession().createObjectId(newObjectId);
      }
  
      @Override
      public void cancelCheckOut() {
          String objectId = getObjectId();
  
          getBinding().getVersioningService().cancelCheckOut(getRepositoryId(), objectId, null);
  
          // remove PWC from cache, it doesn't exist anymore
          getSession().removeObjectFromCache(this);
      }
  
      @Override
      public ObjectId checkIn(boolean major, Map<String, ?> properties, ContentStream contentStream,
              String checkinComment, List<Policy> policies, List<Ace> addAces, List<Ace> removeAces) {
          String newObjectId = null;
  
          readLock();
          try {
              Holder<String> objectIdHolder = new Holder<String>(getObjectId());
  
              ObjectFactory of = getObjectFactory();
  
              Set<Updatability> updatebility = EnumSet.noneOf(Updatability.class);
              updatebility.add(Updatability.READWRITE);
              updatebility.add(Updatability.WHENCHECKEDOUT);
  
              getBinding().getVersioningService().checkIn(getRepositoryId(), objectIdHolder, major,
                      of.convertProperties(properties, getType(), getSecondaryTypes(), updatebility),
                      of.convertContentStream(contentStream), checkinComment, of.convertPolicies(policies),
                      of.convertAces(addAces), of.convertAces(removeAces), null);
  
              newObjectId = objectIdHolder.getValue();
          } finally {
              readUnlock();
          }
  
          // remove PWC from cache, it doesn't exist anymore
          getSession().removeObjectFromCache(this);
  
          if (newObjectId == null) {
              return null;
          }
  
          return getSession().createObjectId(newObjectId);
      }
  
      @Override
      public List<Document> getAllVersions() {
          return getAllVersions(getSession().getDefaultContext());
      }
  
      @Override
      public List<Document> getAllVersions(OperationContext context) {
          checkContext(context);
  
          String objectId;
          String versionSeriesId;
  
          readLock();
          try {
              objectId = getObjectId();
              versionSeriesId = getVersionSeriesId();
          } finally {
              readUnlock();
          }
  
          List<ObjectData> versions = getBinding().getVersioningService().getAllVersions(getRepositoryId(), objectId,
                  versionSeriesId, context.getFilterString(), context.isIncludeAllowableActions(), null);
  
          ObjectFactory objectFactory = getSession().getObjectFactory();
  
          List<Document> result = new ArrayList<Document>();
          if (versions != null) {
              for (ObjectData objectData : versions) {
                  CmisObject doc = objectFactory.convertObject(objectData, context);
                  if (!(doc instanceof Document)) {
                      // should not happen...
                      continue;
                  }
  
                  result.add((Document) doc);
              }
          }
  
          return result;
  
      }
  
      @Override
      public Document getObjectOfLatestVersion(boolean major) {
          return getObjectOfLatestVersion(major, getSession().getDefaultContext());
      }
  
      @Override
      public Document getObjectOfLatestVersion(boolean major, OperationContext context) {
          return getSession().getLatestDocumentVersion(this, major, context);
      }
  
      // content operations
  
      @Override
      public ContentStream getContentStream() {
          return getContentStream(null, null, null);
      }
  
      @Override
      public ContentStream getContentStream(BigInteger offset, BigInteger length) {
          return getContentStream(null, offset, length);
      }
  
      @Override
      public ContentStream getContentStream(String streamId) {
          return getContentStream(streamId, null, null);
      }
  
      @Override
      public ContentStream getContentStream(String streamId, BigInteger offset, BigInteger length) {
          // get the stream
          ContentStream contentStream = getSession().getContentStream(this, streamId, offset, length);
  
          if (contentStream == null) {
              return null;
          }
  
          // the AtomPub binding doesn't return a file name
          // -> get the file name from properties, if present
          String filename = contentStream.getFileName();
          if (filename == null) {
              filename = getContentStreamFileName();
          }
  
          long lengthLong = (contentStream.getBigLength() == null ? -1 : contentStream.getBigLength().longValue());
  
          // convert and return stream object
          return getSession().getObjectFactory().createContentStream(filename, lengthLong, contentStream.getMimeType(),
                  contentStream.getStream(), contentStream instanceof PartialContentStream);
      }
  
      @Override
      public String getContentUrl() {
          return getContentUrl(null);
      }
  
      @Override
      public String getContentUrl(String streamId) {
          if (getBinding().getObjectService() instanceof LinkAccess) {
              LinkAccess linkAccess = (LinkAccess) getBinding().getObjectService();
  
              if (streamId == null) {
                  return linkAccess.loadContentLink(getRepositoryId(), getId());
              } else {
                  return linkAccess.loadRenditionContentLink(getRepositoryId(), getId(), streamId);
              }
          }
  
          return null;
      }
  
      @Override
      public Document setContentStream(ContentStream contentStream, boolean overwrite) {
          ObjectId objectId = setContentStream(contentStream, overwrite, true);
          if (objectId == null) {
              return null;
          }
  
          if (!getObjectId().equals(objectId.getId())) {
              return (Document) getSession().getObject(objectId, getCreationContext());
          }
  
          return this;
      }
  
      @Override
      public ObjectId setContentStream(ContentStream contentStream, boolean overwrite, boolean refresh) {
          String newObjectId = null;
  
          readLock();
          try {
              Holder<String> objectIdHolder = new Holder<String>(getObjectId());
              Holder<String> changeTokenHolder = new Holder<String>((String) getPropertyValue(PropertyIds.CHANGE_TOKEN));
  
              getBinding().getObjectService().setContentStream(getRepositoryId(), objectIdHolder, overwrite,
                      changeTokenHolder, getObjectFactory().convertContentStream(contentStream), null);
  
              newObjectId = objectIdHolder.getValue();
          } finally {
              readUnlock();
          }
  
          if (refresh) {
              refresh();
          }
  
          if (newObjectId == null) {
              return null;
          }
  
          return getSession().createObjectId(newObjectId);
      }
  
      @Override
      public Document appendContentStream(ContentStream contentStream, boolean isLastChunk) {
          if (getSession().getRepositoryInfo().getCmisVersion() == CmisVersion.CMIS_1_0) {
              throw new CmisNotSupportedException("This method is not supported for CMIS 1.0 repositories.");
          }
  
          ObjectId objectId = appendContentStream(contentStream, isLastChunk, true);
          if (objectId == null) {
              return null;
          }
  
          if (!getObjectId().equals(objectId.getId())) {
              return (Document) getSession().getObject(objectId, getCreationContext());
          }
  
          return this;
      }
  
      @Override
      public ObjectId appendContentStream(ContentStream contentStream, boolean isLastChunk, boolean refresh) {
          if (getSession().getRepositoryInfo().getCmisVersion() == CmisVersion.CMIS_1_0) {
              throw new CmisNotSupportedException("This method is not supported for CMIS 1.0 repositories.");
          }
  
          String newObjectId = null;
  
          readLock();
          try {
              Holder<String> objectIdHolder = new Holder<String>(getObjectId());
              Holder<String> changeTokenHolder = new Holder<String>((String) getPropertyValue(PropertyIds.CHANGE_TOKEN));
  
              getBinding().getObjectService().appendContentStream(getRepositoryId(), objectIdHolder, changeTokenHolder,
                      getObjectFactory().convertContentStream(contentStream), isLastChunk, null);
  
              newObjectId = objectIdHolder.getValue();
          } finally {
              readUnlock();
          }
  
          if (refresh) {
              refresh();
          }
  
          if (newObjectId == null) {
              return null;
          }
  
          return getSession().createObjectId(newObjectId);
      }
  
      @Override
      public Document deleteContentStream() {
          ObjectId objectId = deleteContentStream(true);
          if (objectId == null) {
              return null;
          }
  
          if (!getObjectId().equals(objectId.getId())) {
              return (Document) getSession().getObject(objectId, getCreationContext());
          }
  
          return this;
      }
  
      @Override
      public ObjectId deleteContentStream(boolean refresh) {
          String newObjectId = null;
  
          readLock();
          try {
              Holder<String> objectIdHolder = new Holder<String>(getObjectId());
              Holder<String> changeTokenHolder = new Holder<String>((String) getPropertyValue(PropertyIds.CHANGE_TOKEN));
  
              getBinding().getObjectService().deleteContentStream(getRepositoryId(), objectIdHolder, changeTokenHolder,
                      null);
  
              newObjectId = objectIdHolder.getValue();
          } finally {
              readUnlock();
          }
  
          if (refresh) {
              refresh();
          }
  
          if (newObjectId == null) {
              return null;
          }
  
          return getSession().createObjectId(newObjectId);
      }
  
      @Override
      public OutputStream createOverwriteOutputStream(String filename, String mimeType) {
          if (getSession().getRepositoryInfo().getCmisVersion() == CmisVersion.CMIS_1_0) {
              throw new CmisNotSupportedException("This method is not supported for CMIS 1.0 repositories.");
          }
  
          return new AppendOutputStream(getSession(), this, true, filename, mimeType);
      }
  
      @Override
      public OutputStream createOverwriteOutputStream(String filename, String mimeType, int bufferSize) {
          if (getSession().getRepositoryInfo().getCmisVersion() == CmisVersion.CMIS_1_0) {
              throw new CmisNotSupportedException("This method is not supported for CMIS 1.0 repositories.");
          }
  
          return new AppendOutputStream(getSession(), this, true, filename, mimeType, bufferSize);
      }
  
      @Override
      public OutputStream createAppendOutputStream() {
          if (getSession().getRepositoryInfo().getCmisVersion() == CmisVersion.CMIS_1_0) {
              throw new CmisNotSupportedException("This method is not supported for CMIS 1.0 repositories.");
          }
  
          return new AppendOutputStream(getSession(), this, false, null, null);
      }
  
      @Override
      public OutputStream createAppendOutputStream(int bufferSize) {
          if (getSession().getRepositoryInfo().getCmisVersion() == CmisVersion.CMIS_1_0) {
              throw new CmisNotSupportedException("This method is not supported for CMIS 1.0 repositories.");
          }
  
          return new AppendOutputStream(getSession(), this, false, null, null, bufferSize);
      }
  
      @Override
      public ObjectId checkIn(boolean major, Map<String, ?> properties, ContentStream contentStream,
              String checkinComment) {
          return this.checkIn(major, properties, contentStream, checkinComment, null, null, null);
      }
  }
  