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.server.impl.atompub;
  
  import static org.apache.chemistry.opencmis.commons.impl.CollectionsHelper.isNotEmpty;
  
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.io.PrintWriter;
  import java.math.BigInteger;
  import java.util.GregorianCalendar;
  import java.util.List;
  import java.util.Map;
  
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import javax.xml.stream.XMLStreamWriter;
  
  import org.apache.chemistry.opencmis.commons.PropertyIds;
  import org.apache.chemistry.opencmis.commons.data.AllowableActions;
  import org.apache.chemistry.opencmis.commons.data.BulkUpdateObjectIdAndChangeToken;
  import org.apache.chemistry.opencmis.commons.data.ContentStream;
  import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
  import org.apache.chemistry.opencmis.commons.data.ObjectData;
  import org.apache.chemistry.opencmis.commons.data.Properties;
  import org.apache.chemistry.opencmis.commons.data.PropertyData;
  import org.apache.chemistry.opencmis.commons.data.PropertyString;
  import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
  import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
  import org.apache.chemistry.opencmis.commons.enums.VersioningState;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
  import org.apache.chemistry.opencmis.commons.impl.Constants;
  import org.apache.chemistry.opencmis.commons.impl.DateTimeHelper;
  import org.apache.chemistry.opencmis.commons.impl.IOUtils;
  import org.apache.chemistry.opencmis.commons.impl.MimeHelper;
  import org.apache.chemistry.opencmis.commons.impl.ReturnVersion;
  import org.apache.chemistry.opencmis.commons.impl.UrlBuilder;
  import org.apache.chemistry.opencmis.commons.impl.XMLConverter;
  import org.apache.chemistry.opencmis.commons.impl.XMLUtils;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateImpl;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
  import org.apache.chemistry.opencmis.commons.server.CallContext;
  import org.apache.chemistry.opencmis.commons.server.CmisService;
  import org.apache.chemistry.opencmis.commons.server.ObjectInfo;
  import org.apache.chemistry.opencmis.commons.spi.Holder;
  import org.apache.chemistry.opencmis.server.shared.TempStoreOutputStreamFactory;
  
  /**
   * Object Service operations.
   */
  public class ObjectService {
  
      private static final int BUFFER_SIZE = 64 * 1024;
  
      /**
       * Create.
       */
      public static class Create extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String folderId = getStringParameter(request, Constants.PARAM_ID);
              String sourceFolderId = getStringParameter(request, Constants.PARAM_SOURCE_FOLDER_ID);
              VersioningState versioningState = getEnumParameter(request, Constants.PARAM_VERSIONIG_STATE,
                      VersioningState.class);
  
              TempStoreOutputStreamFactory streamFactory = (TempStoreOutputStreamFactory) context
                      .get(CallContext.STREAM_FACTORY);
              AtomEntryParser parser = new AtomEntryParser(streamFactory);
              parser.setIgnoreAtomContentSrc(true); // needed for some clients
              parser.parse(request.getInputStream());
  
              // execute
              String newObjectId = null;
              String objectId = parser.getId();
              try {
                  if (stopBeforeService(service)) {
                      return;
                  }
  
                  if (objectId == null) {
                      // create
                      ContentStream contentStream = parser.getContentStream();
                      newObjectId = service.create(repositoryId, parser.getProperties(), folderId, contentStream,
                              versioningState, parser.getPolicyIds(), null);
                  } else {
                      if (sourceFolderId == null || sourceFolderId.trim().length() == 0) {
                          // addObjectToFolder
                          service.addObjectToFolder(repositoryId, objectId, folderId, null, null);
                          newObjectId = objectId;
                      } else {
                          // move
                          Holder<String> objectIdHolder = new Holder<String>(objectId);
                          service.moveObject(repositoryId, objectIdHolder, folderId, sourceFolderId, null);
                          newObjectId = objectIdHolder.getValue();
                      }
                  }
  
                  if (stopAfterService(service)) {
                      return;
                  }
              } finally {
                  parser.release();
              }
  
              ObjectInfo objectInfo = service.getObjectInfo(repositoryId, newObjectId);
              if (objectInfo == null) {
                  throw new CmisRuntimeException("Object Info is missing!");
              }
  
              ObjectData object = objectInfo.getObject();
              if (object == null) {
                  throw new CmisRuntimeException("Object is null!");
              }
  
              // set headers
              UrlBuilder baseUrl = compileBaseUrl(request, repositoryId);
  
              response.setStatus(HttpServletResponse.SC_CREATED);
              response.setContentType(Constants.MEDIATYPE_ENTRY);
              response.setHeader("Location", compileUrl(baseUrl, RESOURCE_ENTRY, newObjectId));
  
              // write XML
              AtomEntry entry = new AtomEntry();
              entry.startDocument(response.getOutputStream(), getNamespaces(service));
              writeObjectEntry(service, entry, object, null, repositoryId, null, null, baseUrl, true,
                      context.getCmisVersion());
              entry.endDocument();
          }
      }
  
      /**
       * Create relationship.
       */
      public static class CreateRelationship extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              TempStoreOutputStreamFactory streamFactory = (TempStoreOutputStreamFactory) context
                      .get(CallContext.STREAM_FACTORY);
              AtomEntryParser parser = new AtomEntryParser(request.getInputStream(), streamFactory);
  
              // execute
              String newObjectId = null;
              try {
                  if (stopBeforeService(service)) {
                      return;
                  }
  
                  newObjectId = service.createRelationship(repositoryId, parser.getProperties(), parser.getPolicyIds(),
                          null, null, null);
  
                  if (stopAfterService(service)) {
                      return;
                  }
              } finally {
                  parser.release();
              }
  
              ObjectInfo objectInfo = service.getObjectInfo(repositoryId, newObjectId);
              if (objectInfo == null) {
                  throw new CmisRuntimeException("Object Info is missing!");
              }
  
              ObjectData object = objectInfo.getObject();
              if (object == null) {
                  throw new CmisRuntimeException("Object is null!");
              }
  
              // set headers
              UrlBuilder baseUrl = compileBaseUrl(request, repositoryId);
  
              response.setStatus(HttpServletResponse.SC_CREATED);
              response.setContentType(Constants.MEDIATYPE_ENTRY);
              response.setHeader("Location", compileUrl(baseUrl, RESOURCE_ENTRY, newObjectId));
  
              // write XML
              AtomEntry entry = new AtomEntry();
              entry.startDocument(response.getOutputStream(), getNamespaces(service));
              writeObjectEntry(service, entry, object, null, repositoryId, null, null, baseUrl, true,
                      context.getCmisVersion());
              entry.endDocument();
          }
      }
  
      /**
       * Delete object.
       */
      public static class DeleteObject extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String objectId = getStringParameter(request, Constants.PARAM_ID);
              Boolean allVersions = getBooleanParameter(request, Constants.PARAM_ALL_VERSIONS);
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              service.deleteObjectOrCancelCheckOut(repositoryId, objectId, allVersions, null);
  
              if (stopAfterService(service)) {
                  return;
              }
  
              // set headers
              response.setStatus(HttpServletResponse.SC_NO_CONTENT);
          }
      }
  
      /**
       * Delete content stream.
       */
      public static class DeleteContentStream extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String objectId = getStringParameter(request, Constants.PARAM_ID);
              String changeToken = getStringParameter(request, Constants.PARAM_CHANGE_TOKEN);
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              service.deleteContentStream(repositoryId, new Holder<String>(objectId), changeToken == null ? null
                      : new Holder<String>(changeToken), null);
  
              if (stopAfterService(service)) {
                  return;
              }
  
              // set headers
              response.setStatus(HttpServletResponse.SC_NO_CONTENT);
          }
      }
  
      /**
       * Set or append content stream.
       */
      public static class SetOrAppendContentStream extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String objectId = getStringParameter(request, Constants.PARAM_ID);
              String changeToken = getStringParameter(request, Constants.PARAM_CHANGE_TOKEN);
              Boolean appendFlag = getBooleanParameter(request, Constants.PARAM_APPEND);
              Boolean overwriteFlag = getBooleanParameter(request, Constants.PARAM_OVERWRITE_FLAG);
              Boolean isLastChunk = getBooleanParameter(request, Constants.PARAM_IS_LAST_CHUNK);
  
              ContentStreamImpl contentStream = new ContentStreamImpl();
              contentStream.setStream(request.getInputStream());
              contentStream.setMimeType(request.getHeader("Content-Type"));
              String lengthStr = request.getHeader("Content-Length");
              if (lengthStr != null) {
                  try {
                      contentStream.setLength(new BigInteger(lengthStr));
                  } catch (NumberFormatException e) {
                      // invalid content length -> ignore
                  }
              }
              String contentDisposition = request.getHeader(MimeHelper.CONTENT_DISPOSITION);
              if (contentDisposition != null) {
                  contentStream.setFileName(MimeHelper.decodeContentDispositionFilename(contentDisposition));
              }
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              Holder<String> objectIdHolder = new Holder<String>(objectId);
              if (Boolean.TRUE.equals(appendFlag)) {
                  service.appendContentStream(repositoryId, objectIdHolder, changeToken == null ? null
                          : new Holder<String>(changeToken), contentStream, (Boolean.TRUE.equals(isLastChunk) ? true
                          : false), null);
              } else {
                  service.setContentStream(repositoryId, objectIdHolder, overwriteFlag, changeToken == null ? null
                          : new Holder<String>(changeToken), contentStream, null);
              }
  
              if (stopAfterService(service)) {
                  return;
              }
  
              // set headers
              String newObjectId = objectIdHolder.getValue() == null ? objectId : objectIdHolder.getValue();
              String contentLocation = compileUrl(compileBaseUrl(request, repositoryId), RESOURCE_CONTENT, newObjectId);
              String location = compileUrl(compileBaseUrl(request, repositoryId), RESOURCE_OBJECTBYID, newObjectId);
  
              // set status
              if (newObjectId.equals(objectId)) {
                  if (Boolean.TRUE.equals(appendFlag)) {
                      // append stream: no new version -> OK
                      response.setStatus(HttpServletResponse.SC_OK);
                      response.setContentLength(0);
                  } else {
                      if (!Boolean.FALSE.equals(overwriteFlag)) {
                          // set stream: no new version and overwrite ->
                          // OK: if the document had a stream,
                          // CREATED: if the document had no stream
                          // ... but we don't know, ... OK is more likely ...
                          response.setStatus(HttpServletResponse.SC_OK);
                          response.setContentLength(0);
                      } else {
                          // set stream: no new version and not overwrite ->
                          // CREATED (the document hasn't had a stream)
                          response.setStatus(HttpServletResponse.SC_CREATED);
                      }
                  }
              } else {
                  // new version created -> CREATED
                  response.setStatus(HttpServletResponse.SC_CREATED);
              }
              response.setHeader("Content-Location", contentLocation);
              response.setHeader("Location", location);
          }
      }
  
      /**
       * Delete tree.
       */
      public static class DeleteTree extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String folderId = getStringParameter(request, Constants.PARAM_ID);
              Boolean allVersions = getBooleanParameter(request, Constants.PARAM_ALL_VERSIONS);
              UnfileObject unfileObjects = getEnumParameter(request, Constants.PARAM_UNFILE_OBJECTS, UnfileObject.class);
              Boolean continueOnFailure = getBooleanParameter(request, Constants.PARAM_CONTINUE_ON_FAILURE);
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              FailedToDeleteData ftd = service.deleteTree(repositoryId, folderId, allVersions, unfileObjects,
                      continueOnFailure, null);
  
              if (stopAfterService(service)) {
                  return;
              }
  
              if (ftd != null && isNotEmpty(ftd.getIds())) {
                  // print ids that could not be deleted
                  response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                  response.setContentType("text/plain");
  
                  PrintWriter pw = response.getWriter();
  
                  pw.println("Failed to delete the following objects:");
                  for (String id : ftd.getIds()) {
                      pw.println(id);
                  }
  
                  pw.flush();
  
                  return;
              }
  
              // set headers
              response.setStatus(HttpServletResponse.SC_NO_CONTENT);
          }
      }
  
      /**
       * getObject.
       */
      public static class GetObject extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String objectId = getStringParameter(request, Constants.PARAM_ID);
              ReturnVersion returnVersion = getEnumParameter(request, Constants.PARAM_RETURN_VERSION, ReturnVersion.class);
              String filter = getStringParameter(request, Constants.PARAM_FILTER);
              Boolean includeAllowableActions = getBooleanParameter(request, Constants.PARAM_ALLOWABLE_ACTIONS);
              IncludeRelationships includeRelationships = getEnumParameter(request, Constants.PARAM_RELATIONSHIPS,
                      IncludeRelationships.class);
              String renditionFilter = getStringParameter(request, Constants.PARAM_RENDITION_FILTER);
              Boolean includePolicyIds = getBooleanParameter(request, Constants.PARAM_POLICY_IDS);
              Boolean includeAcl = getBooleanParameter(request, Constants.PARAM_ACL);
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              ObjectData object = null;
              if (returnVersion == ReturnVersion.LATEST || returnVersion == ReturnVersion.LASTESTMAJOR) {
                  object = service.getObjectOfLatestVersion(repositoryId, objectId, null,
                          returnVersion == ReturnVersion.LASTESTMAJOR, filter, includeAllowableActions,
                          includeRelationships, renditionFilter, includePolicyIds, includeAcl, null);
              } else {
                  object = service.getObject(repositoryId, objectId, filter, includeAllowableActions,
                          includeRelationships, renditionFilter, includePolicyIds, includeAcl, null);
              }
  
              if (stopAfterService(service)) {
                  return;
              }
  
              if (object == null) {
                  throw new CmisRuntimeException("Object is null!");
              }
  
              ObjectInfo objectInfo = service.getObjectInfo(repositoryId, objectId);
              if (objectInfo == null) {
                  throw new CmisRuntimeException("Object Info is missing!");
              }
  
              // set headers
              response.setStatus(HttpServletResponse.SC_OK);
              response.setContentType(Constants.MEDIATYPE_ENTRY);
  
              // write XML
              UrlBuilder baseUrl = compileBaseUrl(request, repositoryId);
  
              AtomEntry entry = new AtomEntry();
              entry.startDocument(response.getOutputStream(), getNamespaces(service));
              writeObjectEntry(service, entry, object, null, repositoryId, null, null, baseUrl, true,
                      context.getCmisVersion());
              entry.endDocument();
          }
      }
  
      /**
       * objectByPath URI template.
       */
      public static class GetObjectByPath extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String path = getStringParameter(request, Constants.PARAM_PATH);
              String filter = getStringParameter(request, Constants.PARAM_FILTER);
              Boolean includeAllowableActions = getBooleanParameter(request, Constants.PARAM_ALLOWABLE_ACTIONS);
              IncludeRelationships includeRelationships = getEnumParameter(request, Constants.PARAM_RELATIONSHIPS,
                      IncludeRelationships.class);
              String renditionFilter = getStringParameter(request, Constants.PARAM_RENDITION_FILTER);
              Boolean includePolicyIds = getBooleanParameter(request, Constants.PARAM_POLICY_IDS);
              Boolean includeAcl = getBooleanParameter(request, Constants.PARAM_ACL);
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              ObjectData object = service.getObjectByPath(repositoryId, path, filter, includeAllowableActions,
                      includeRelationships, renditionFilter, includePolicyIds, includeAcl, null);
  
              if (stopAfterService(service)) {
                  return;
              }
  
              if (object == null) {
                  throw new CmisRuntimeException("Object is null!");
              }
  
              ObjectInfo objectInfo = service.getObjectInfo(repositoryId, object.getId());
              if (objectInfo == null) {
                  throw new CmisRuntimeException("Object Info is missing!");
              }
  
              // set headers
              response.setStatus(HttpServletResponse.SC_OK);
              response.setContentType(Constants.MEDIATYPE_ENTRY);
  
              // write XML
              UrlBuilder baseUrl = compileBaseUrl(request, repositoryId);
  
              AtomEntry entry = new AtomEntry();
              entry.startDocument(response.getOutputStream(), getNamespaces(service));
              writeObjectEntry(service, entry, object, null, repositoryId, null, null, baseUrl, true,
                      context.getCmisVersion());
              entry.endDocument();
          }
      }
  
      /**
       * Allowable Actions.
       */
      public static class GetAllowableActions extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String objectId = getStringParameter(request, Constants.PARAM_ID);
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              AllowableActions allowableActions = service.getAllowableActions(repositoryId, objectId, null);
  
              if (stopAfterService(service)) {
                  return;
              }
  
              if (allowableActions == null) {
                  throw new CmisRuntimeException("Allowable Actions is null!");
              }
  
              // set headers
              response.setStatus(HttpServletResponse.SC_OK);
              response.setContentType(Constants.MEDIATYPE_ALLOWABLEACTION);
  
              // write XML
              XMLStreamWriter writer = XMLUtils.createWriter(response.getOutputStream());
              XMLUtils.startXmlDocument(writer);
              XMLConverter.writeAllowableActions(writer, context.getCmisVersion(), true, allowableActions);
              XMLUtils.endXmlDocument(writer);
          }
      }
  
      /**
       * getContentStream.
       */
      public static class GetContentStream extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String objectId = getStringParameter(request, Constants.PARAM_ID);
              String streamId = getStringParameter(request, Constants.PARAM_STREAM_ID);
  
              BigInteger offset = context.getOffset();
              BigInteger length = context.getLength();
  
              // execute
              if (stopBeforeService(service)) {
                  return;
              }
  
              ContentStream content = service.getContentStream(repositoryId, objectId, streamId, offset, length, null);
  
              if (stopAfterService(service)) {
                  return;
              }
  
              if (content == null || content.getStream() == null) {
                  throw new CmisRuntimeException("Content stream is null!");
              }
  
              // set HTTP headers, if requested by the server implementation
              if (sendContentStreamHeaders(content, request, response)) {
                  return;
              }
  
              String contentType = content.getMimeType();
              if (contentType == null) {
                  contentType = Constants.MEDIATYPE_OCTETSTREAM;
              }
  
              // set headers
              if ((offset == null || offset.signum() == 0) && (length == null)) {
                  response.setStatus(HttpServletResponse.SC_OK);
              } else {
                  response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
  
                  if (content.getBigLength() != null && content.getBigLength().signum() == 1) {
                      BigInteger firstBytePos = (offset == null ? BigInteger.ZERO : offset);
                      BigInteger lastBytePos = firstBytePos.add(content.getBigLength().subtract(BigInteger.ONE));
  
                      response.setHeader("Content-Range",
                              "bytes " + firstBytePos.toString() + "-" + lastBytePos.toString() + "/*");
                  }
              }
              response.setContentType(contentType);
  
              if (content.getFileName() != null) {
                  response.setHeader(MimeHelper.CONTENT_DISPOSITION,
                          MimeHelper.encodeContentDisposition(MimeHelper.DISPOSITION_ATTACHMENT, content.getFileName()));
              }
  
              // send content
              InputStream in = content.getStream();
              OutputStream out = response.getOutputStream();
              try {
                  IOUtils.copy(in, out, BUFFER_SIZE);
                  out.flush();
              } finally {
                  IOUtils.closeQuietly(in);
              }
          }
      }
  
      /**
       * UpdateProperties.
       */
      public static class UpdateProperties extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              // get parameters
              String objectId = getStringParameter(request, Constants.PARAM_ID);
              Boolean checkin = getBooleanParameter(request, Constants.PARAM_CHECK_IN);
              String checkinComment = getStringParameter(request, Constants.PARAM_CHECKIN_COMMENT);
              Boolean major = getBooleanParameter(request, Constants.PARAM_MAJOR);
  
              TempStoreOutputStreamFactory streamFactory = (TempStoreOutputStreamFactory) context
                      .get(CallContext.STREAM_FACTORY);
              AtomEntryParser parser = new AtomEntryParser(request.getInputStream(), streamFactory);
  
              // execute
              Holder<String> objectIdHolder = new Holder<String>(objectId);
  
              try {
                  if (checkin != null && checkin.booleanValue()) {
                      if (stopBeforeService(service)) {
                          return;
                      }
  
                      ContentStream contentStream = parser.getContentStream();
                      service.checkIn(repositoryId, objectIdHolder, major, parser.getProperties(), contentStream,
                              checkinComment, parser.getPolicyIds(), null, null, null);
  
                      if (stopAfterService(service)) {
                          return;
                      }
                  } else {
                      Properties properties = parser.getProperties();
                      String changeToken = null;
                      if (properties != null) {
                          changeToken = extractChangeToken(properties);
                          if (changeToken != null) {
                              properties = new PropertiesImpl(properties);
                              ((PropertiesImpl) properties).removeProperty(PropertyIds.CHANGE_TOKEN);
                          }
                      }
  
                      if (changeToken == null) {
                          // not required by the CMIS specification
                          // -> keep for backwards compatibility with older
                          // OpenCMIS
                          // clients
                          changeToken = getStringParameter(request, Constants.PARAM_CHANGE_TOKEN);
                      }
  
                      if (stopBeforeService(service)) {
                          return;
                      }
  
                      service.updateProperties(repositoryId, objectIdHolder, changeToken == null ? null
                              : new Holder<String>(changeToken), properties, null);
  
                      if (stopAfterService(service)) {
                          return;
                      }
                  }
              } finally {
                  parser.release();
              }
  
              ObjectInfo objectInfo = service.getObjectInfo(repositoryId, objectIdHolder.getValue());
              if (objectInfo == null) {
                  throw new CmisRuntimeException("Object Info is missing!");
              }
  
              ObjectData object = objectInfo.getObject();
              if (object == null) {
                  throw new CmisRuntimeException("Object is null!");
              }
  
              // set headers
              UrlBuilder baseUrl = compileBaseUrl(request, repositoryId);
              String location = compileUrl(baseUrl, RESOURCE_ENTRY, objectIdHolder.getValue());
  
              response.setStatus(HttpServletResponse.SC_CREATED);
              response.setContentType(Constants.MEDIATYPE_ENTRY);
              response.setHeader("Content-Location", location);
              response.setHeader("Location", location);
  
              // write XML
              AtomEntry entry = new AtomEntry();
              entry.startDocument(response.getOutputStream(), getNamespaces(service));
              writeObjectEntry(service, entry, object, null, repositoryId, null, null, baseUrl, true,
                      context.getCmisVersion());
              entry.endDocument();
          }
  
          /**
           * Gets the change token from a property set.
           */
          private String extractChangeToken(Properties properties) {
              if (properties == null) {
                  return null;
              }
  
              Map<String, PropertyData<?>> propertiesMap = properties.getProperties();
              if (propertiesMap == null) {
                  return null;
              }
  
              PropertyData<?> changeTokenProperty = propertiesMap.get(PropertyIds.CHANGE_TOKEN);
              if (!(changeTokenProperty instanceof PropertyString)) {
                  return null;
              }
  
              return ((PropertyString) changeTokenProperty).getFirstValue();
          }
      }
  
      /**
       * BulkUpdateProperties.
       */
      public static class BulkUpdateProperties extends AbstractAtomPubServiceCall {
          @Override
          public void serve(CallContext context, CmisService service, String repositoryId, HttpServletRequest request,
                  HttpServletResponse response) throws Exception {
              assert context != null;
              assert service != null;
              assert repositoryId != null;
              assert request != null;
              assert response != null;
  
              TempStoreOutputStreamFactory streamFactory = (TempStoreOutputStreamFactory) context
                      .get(CallContext.STREAM_FACTORY);
              AtomEntryParser parser = new AtomEntryParser(streamFactory);
              parser.parse(request.getInputStream());
  
              // execute
              List<BulkUpdateObjectIdAndChangeToken> result = null;
              try {
                  BulkUpdateImpl bulkUpdate = parser.getBulkUpdate();
                  if (bulkUpdate == null) {
                      throw new CmisInvalidArgumentException("Bulk update data is missing!");
                  }
  
                  if (stopBeforeService(service)) {
                      return;
                  }
  
                  result = service.bulkUpdateProperties(repositoryId, bulkUpdate.getObjectIdAndChangeToken(),
                          bulkUpdate.getProperties(), bulkUpdate.getAddSecondaryTypeIds(),
                          bulkUpdate.getRemoveSecondaryTypeIds(), null);
  
                  if (stopAfterService(service)) {
                      return;
                  }
              } finally {
                  parser.release();
              }
  
              response.setStatus(HttpServletResponse.SC_CREATED);
              response.setContentType(Constants.MEDIATYPE_FEED);
  
              // write XML
              AtomFeed feed = new AtomFeed();
              feed.startDocument(response.getOutputStream(), getNamespaces(service));
              feed.startFeed(true);
  
              // write basic Atom feed elements
              feed.writeFeedElements(null, null, null, "Bulk Update Properties",
                      new GregorianCalendar(DateTimeHelper.GMT), null,
                      (result == null ? null : BigInteger.valueOf(result.size())));
  
              // write links
              UrlBuilder baseUrl = compileBaseUrl(request, repositoryId);
  
              feed.writeServiceLink(baseUrl.toString(), repositoryId);
  
              UrlBuilder selfLink = compileUrlBuilder(baseUrl, RESOURCE_BULK_UPDATE, null);
              feed.writeSelfLink(selfLink.toString(), null);
  
              // write entries
              if (result != null) {
                  AtomEntry entry = new AtomEntry(feed.getWriter());
                  for (BulkUpdateObjectIdAndChangeToken idAndToken : result) {
                      if (idAndToken == null || idAndToken.getId() == null) {
                          continue;
                      }
  
                      ObjectDataImpl object = new ObjectDataImpl();
                      PropertiesImpl properties = new PropertiesImpl();
                      object.setProperties(properties);
  
                      properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_ID,
                              idAndToken.getNewId() != null ? idAndToken.getNewId() : idAndToken.getId()));
  
                      if (idAndToken.getChangeToken() != null) {
                          properties.addProperty(new PropertyStringImpl(PropertyIds.CHANGE_TOKEN, idAndToken
                                  .getChangeToken()));
                      }
  
                      writeObjectEntry(service, entry, object, null, repositoryId, null, null, baseUrl, false,
                              context.getCmisVersion());
                  }
              }
  
              // we are done
              feed.endFeed();
              feed.endDocument();
          }
      }
  
  }
  