/*
 * 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.olingo.server.core.requests;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.WritableByteChannel;
import java.util.LinkedList;
import java.util.List;

import org.apache.olingo.commons.api.data.ContextURL;
import org.apache.olingo.commons.api.data.ContextURL.Suffix;
import org.apache.olingo.commons.api.data.Entity;
import org.apache.olingo.commons.api.data.Property;
import org.apache.olingo.commons.api.data.ValueType;
import org.apache.olingo.commons.api.edm.EdmBindingTarget;
import org.apache.olingo.commons.api.edm.EdmComplexType;
import org.apache.olingo.commons.api.edm.EdmEntitySet;
import org.apache.olingo.commons.api.edm.EdmEntityType;
import org.apache.olingo.commons.api.edm.EdmNavigationProperty;
import org.apache.olingo.commons.api.edm.EdmNavigationPropertyBinding;
import org.apache.olingo.commons.api.edm.EdmPrimitiveTypeKind;
import org.apache.olingo.commons.api.edm.EdmProperty;
import org.apache.olingo.commons.api.ex.ODataRuntimeException;
import org.apache.olingo.commons.api.format.ContentType;
import org.apache.olingo.commons.api.http.HttpHeader;
import org.apache.olingo.commons.api.http.HttpMethod;
import org.apache.olingo.server.api.OData;
import org.apache.olingo.server.api.ODataApplicationException;
import org.apache.olingo.server.api.ODataLibraryException;
import org.apache.olingo.server.api.ODataRequest;
import org.apache.olingo.server.api.ODataResponse;
import org.apache.olingo.server.api.ServiceMetadata;
import org.apache.olingo.server.api.deserializer.DeserializerException;
import org.apache.olingo.server.api.deserializer.DeserializerException.MessageKeys;
import org.apache.olingo.server.api.deserializer.ODataDeserializer;
import org.apache.olingo.server.api.serializer.PrimitiveSerializerOptions;
import org.apache.olingo.server.api.serializer.RepresentationType;
import org.apache.olingo.server.api.serializer.SerializerException;
import org.apache.olingo.server.api.uri.UriHelper;
import org.apache.olingo.server.api.uri.UriInfo;
import org.apache.olingo.server.api.uri.UriInfoCrossjoin;
import org.apache.olingo.server.api.uri.UriInfoResource;
import org.apache.olingo.server.api.uri.UriParameter;
import org.apache.olingo.server.api.uri.UriResource;
import org.apache.olingo.server.api.uri.UriResourceComplexProperty;
import org.apache.olingo.server.api.uri.UriResourceEntitySet;
import org.apache.olingo.server.api.uri.UriResourceNavigation;
import org.apache.olingo.server.api.uri.UriResourcePrimitiveProperty;
import org.apache.olingo.server.api.uri.UriResourceProperty;
import org.apache.olingo.server.api.uri.UriResourceSingleton;
import org.apache.olingo.server.api.uri.queryoption.ApplyOption;
import org.apache.olingo.server.core.ContentNegotiator;
import org.apache.olingo.server.core.ContentNegotiatorException;
import org.apache.olingo.server.core.ODataHandlerException;
import org.apache.olingo.server.core.ReturnRepresentation;
import org.apache.olingo.server.core.ServiceHandler;
import org.apache.olingo.server.core.ServiceRequest;
import org.apache.olingo.server.core.responses.CountResponse;
import org.apache.olingo.server.core.responses.EntityResponse;
import org.apache.olingo.server.core.responses.EntitySetResponse;
import org.apache.olingo.server.core.responses.NoContentResponse;
import org.apache.olingo.server.core.responses.PrimitiveValueResponse;
import org.apache.olingo.server.core.responses.PropertyResponse;
import org.apache.olingo.server.core.responses.StreamResponse;
import org.apache.olingo.server.core.uri.parser.UriParserSemanticException;

public class DataRequest extends ServiceRequest {
  public static final int DEFAULT_BUFFER_SIZE = 8192;
  protected UriResourceEntitySet uriResourceEntitySet;
  private boolean countRequest;
  private UriResourceProperty uriResourceProperty;
  private boolean valueRequest;
  private final LinkedList<UriResourceNavigation> uriNavigations = new LinkedList<UriResourceNavigation>();
  private boolean references;

  private RequestType type;
  private UriResourceSingleton uriResourceSingleton;

  /**
   * This sub-categorizes the request so that code can be simplified
   */
  interface RequestType {
    public HttpMethod[] allowedMethods();
    
    public boolean assertHttpMethod(ODataResponse response) throws ODataHandlerException;
    
    public ContentType getResponseContentType() throws ContentNegotiatorException;

    public ContextURL getContextURL(OData odata) throws SerializerException;

    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException;
  }

  public DataRequest(OData odata, ServiceMetadata serviceMetadata) {
    super(odata, serviceMetadata);
  }

  public UriResourceEntitySet getUriResourceEntitySet() {
    return uriResourceEntitySet;
  }

  public void setUriResourceEntitySet(UriResourceEntitySet uriResourceEntitySet) {
    this.uriResourceEntitySet = uriResourceEntitySet;
    this.type = new EntityRequest();
  }

  public void setCrossJoin(UriInfoCrossjoin info) {
    this.type = new CrossJoinRequest(info.getEntitySetNames());
  }

  public boolean isSingleton() {
    return this.uriResourceSingleton != null;
  }

  public boolean isCollection() {
    if (!this.uriNavigations.isEmpty()) {
      return this.uriNavigations.getLast().isCollection();
    }
    return this.uriResourceEntitySet != null && this.uriResourceEntitySet.isCollection();
  }

  public EdmEntitySet getEntitySet() {
    return this.uriResourceEntitySet.getEntitySet();
  }

  public boolean isCountRequest() {
    return countRequest;
  }

  public void setCountRequest(boolean countRequest) {
    this.countRequest = countRequest;
    this.type = new CountRequest();
  }

  public boolean isPropertyRequest() {
    return this.uriResourceProperty != null;
  }

  public boolean isPropertyComplex() {
    return (this.uriResourceProperty instanceof UriResourceComplexProperty);
  }

  public boolean isPropertyStream() {
    if (isPropertyComplex()) {
      return false;
    }
    EdmProperty property = ((UriResourcePrimitiveProperty)this.uriResourceProperty).getProperty();
    return property.getType() == odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Stream);
  }

  public UriResourceProperty getUriResourceProperty() {
    return uriResourceProperty;
  }

  public void setUriResourceProperty(UriResourceProperty uriResourceProperty) {
    this.uriResourceProperty = uriResourceProperty;
    this.type = new PropertyRequest();
  }

  public LinkedList<UriResourceNavigation> getNavigations() {
    return this.uriNavigations;
  }

  public void addUriResourceNavigation(UriResourceNavigation uriResourceNavigation) {
    this.uriNavigations.add(uriResourceNavigation);
  }

  public UriResourceSingleton getUriResourceSingleton() {
    return this.uriResourceSingleton;
  }

  public void setUriResourceSingleton(UriResourceSingleton info) {
    this.uriResourceSingleton = info;
    this.type = new SingletonRequest();
  }

  public List<UriParameter> getKeyPredicates() {
    if (this.uriResourceEntitySet != null) {
      return this.uriResourceEntitySet.getKeyPredicates();
    }
    return null;
  }

  public boolean isReferenceRequest() {
    return this.references;
  }

  public void setReferenceRequest(boolean ref) {
    this.references = ref;
    this.type = new ReferenceRequest();
  }

  public boolean isValueRequest() {
    return valueRequest;
  }

  public void setValueRequest(boolean valueRequest) {
    this.valueRequest = valueRequest;
    this.type = new ValueRequest();
  }

  public boolean assertHttpMethod(ODataResponse response)
      throws ODataHandlerException {
    return this.type.assertHttpMethod(response);
  }
  
  @Override
  public HttpMethod[] allowedMethods() {
    return this.type.allowedMethods();
  }

  public ContextURL getContextURL(OData odata) throws SerializerException {
    return type.getContextURL(odata);
  }

  @Override
  public void execute(ServiceHandler handler, ODataResponse response)
      throws ODataLibraryException, ODataApplicationException {

    // check for valid HTTP Verb
    assertHttpMethod(response);

    this.type.execute(handler, response);
  }

  @SuppressWarnings("unchecked")
  @Override
  public <T> T getSerializerOptions(Class<T> serilizerOptions, ContextURL contextUrl, boolean references)
      throws ContentNegotiatorException {   
    if (serilizerOptions.isAssignableFrom(PrimitiveSerializerOptions.class)) {
      
      String xmlReplacement = null;
      if (getResponseContentType().isCompatible(ContentType.APPLICATION_XML)
          || getResponseContentType().isCompatible(ContentType.APPLICATION_ATOM_XML)) {
        xmlReplacement = xml10IncompatibleCharReplacement();
      }      
      
      return (T) PrimitiveSerializerOptions.with().contextURL(contextUrl)
          .facetsFrom(getUriResourceProperty().getProperty())
          .xml10InvalidCharReplacement(xmlReplacement)
          .build();
    }
    return super.getSerializerOptions(serilizerOptions, contextUrl, references);
  }

  @Override
  public ContentType getResponseContentType() throws ContentNegotiatorException {
    return type.getResponseContentType();
  }

  class EntityRequest implements RequestType {

    @Override
    public boolean assertHttpMethod(ODataResponse response) throws ODataHandlerException {
      // the create/update/delete to navigation property is done through references
      // see # 11.4.6
      if (!getNavigations().isEmpty()) {
        if (isPOST()) {
          UriResourceNavigation last = getNavigations().getLast();
          if (!(getEntitySet().getRelatedBindingTarget(last.getProperty().getName()) 
              instanceof EdmEntitySet)) {
            return methodNotAllowed(response, httpMethod(), 
                "navigation updates must be to an entity contained in an entity set", 
                allowedMethods());
          }
        } else if (!isGET()) {
          return methodNotAllowed(response, httpMethod(), 
              "update/delete to navigation property is done through references", 
              allowedMethods());
        }
      }
      
      if ((isGET() || isDELETE()) && getReturnRepresentation() != ReturnRepresentation.NONE) {
        return methodNotAllowed(response, httpMethod(), 
            "Invalid prefer header used", 
            allowedMethods());
      }
      
      // in update, delete entity cases, predicate must be there
      if ((isPATCH() || isPUT() || isDELETE()) 
          && (getKeyPredicates() == null || getKeyPredicates().isEmpty())) {
        return methodNotAllowed(response, httpMethod(),
            "No key predicates provided",allowedMethods());
      }
      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }

    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] { HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT,
          HttpMethod.PATCH, HttpMethod.DELETE };
    }     
    
    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(),
          getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_ENTITY
              : RepresentationType.ENTITY);
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {

      ContextURL contextURL = getContextURL(odata);
      EntityResponse entityResponse = EntityResponse.getInstance(DataRequest.this,
          contextURL, false, response);

      if (isGET()) {
        if (isCollection()) {
          handler.read(DataRequest.this,
              EntitySetResponse.getInstance(DataRequest.this, contextURL, false, response));
        } else {
          handler.read(DataRequest.this,entityResponse);
        }
      } else if (isPUT() || isPATCH()) {
        // RFC 2616: he result of a request having both an If-Match header field and either
        // an If-None-Match or an If-Modified-Since header fields is undefined
        // by this specification.
        boolean ifMatch = getHeader(HttpHeader.IF_MATCH) != null;
        boolean ifNoneMatch = (getHeader(HttpHeader.IF_NONE_MATCH)!= null 
            && "*".equals(getHeader(HttpHeader.IF_NONE_MATCH)));
        if(ifMatch) {
          handler.updateEntity(DataRequest.this, getEntityFromClient(), isPATCH(), getETag(),
              entityResponse);
        } else if (ifNoneMatch) {
          // 11.4.4
          entityResponse = EntityResponse.getInstance(DataRequest.this,
              contextURL, false, response, getReturnRepresentation());
          handler.createEntity(DataRequest.this, getEntityFromClient(), entityResponse);
        } else {
          handler.upsertEntity(DataRequest.this, getEntityFromClient(), isPATCH(), getETag(),
              entityResponse);
        }
      } else if (isPOST()) {
        if (!getNavigations().isEmpty()) {
          entityResponse = EntityResponse.getInstance(DataRequest.this,
              contextURL, false, response, getReturnRepresentation());
          UriResourceNavigation last = getNavigations().getLast();
          EdmEntityType navigationType = last.getProperty().getType();
          Entity entity = getEntityFromClient(navigationType);
          handler.createEntity(DataRequest.this, entity,entityResponse);
        } else {
          entityResponse = EntityResponse.getInstance(DataRequest.this,
              contextURL, false, response, getReturnRepresentation());
          handler.createEntity(DataRequest.this, getEntityFromClient(),entityResponse);
        }
      } else if (isDELETE()) {
        handler.deleteEntity(DataRequest.this, getETag(), entityResponse);
      }
    }

    private Entity getEntityFromClient() throws DeserializerException {
      return getEntityFromClient(getEntitySet().getEntityType());
    }
    
    private Entity getEntityFromClient(EdmEntityType entityType) throws DeserializerException {
      ODataDeserializer deserializer = odata.createDeserializer(getRequestContentType(), getServiceMetaData());
      return deserializer.entity(getODataRequest().getBody(), entityType).getEntity();
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      // EntitySet based return
      final UriHelper helper = odata.createUriHelper();
      ContextURL.Builder builder = buildEntitySetContextURL(helper, getEntitySet(),
          getKeyPredicates(), getUriInfo(), getNavigations(), isCollection(), false, getODataRequest());
      return builder.build();
    }
  }

  class CountRequest implements RequestType {

    @Override
    public boolean assertHttpMethod(ODataResponse response) throws ODataHandlerException {
      if (getReturnRepresentation() != ReturnRepresentation.NONE) {
        return methodNotAllowed(response, httpMethod(), "Invalid Prefer header used", 
            allowedMethods());
      }
      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }

    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] { HttpMethod.GET};
    }     
    
    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      return ContentType.TEXT_PLAIN;
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {
      handler.read(DataRequest.this, CountResponse.getInstance(DataRequest.this, response));
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      return null;
    }
  }

  /**
   * Is NavigationProperty Reference.
   */
  class ReferenceRequest implements RequestType {

    @Override
    public boolean assertHttpMethod(ODataResponse response) throws ODataHandlerException {
      if ((isGET() || isDELETE()) && getReturnRepresentation() != ReturnRepresentation.NONE) {
        return methodNotAllowed(response, httpMethod(), "Invalid Prefer header used", 
            allowedMethods());
      }
      
      // references are only allowed on the navigation properties
      if (getNavigations().isEmpty()) {
        return methodNotAllowed(response, httpMethod(),
            "References can be only used on navigation properties",
            allowedMethods());
      }

      // 11.4.6.1 - post allowed on only collection valued navigation
      if (isPOST() && !getNavigations().getLast().isCollection()) {
        return methodNotAllowed(response, httpMethod(),
            "POST only allowed on collection valued navigation",
            allowedMethods());
      }

      // 11.4.6.3 - PUT allowed on single valued navigation
      if (isPUT() && getNavigations().getLast().isCollection()) {
        return methodNotAllowed(response, httpMethod(),
            "PUT only allowed on single valued navigation", allowedMethods());
      }
      //PATCH is not defined in spec
      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }

    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] { HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT,
          HttpMethod.DELETE };
    }
    
    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(),
          getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_REFERENCE
              : RepresentationType.REFERENCE);
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {
      if (isGET()) {
        if (isCollection()) {
          handler.read(DataRequest.this,
              EntitySetResponse.getInstance(DataRequest.this, getContextURL(odata), true, response));
        } else {
          handler.read(DataRequest.this,
              EntityResponse.getInstance(DataRequest.this, getContextURL(odata), true, response));
        }
      } else if (isDELETE()) {
        // if this against the collection, user need to look at $id param for entity ref #11.4.6.2
        String id = getQueryParameter("$id");
        if (id == null && isCollection()) {
          throw new UriParserSemanticException("$id not specified for delete of reference", 
              UriParserSemanticException.MessageKeys.RESOURCE_NOT_FOUND);
        }
        if (id == null) {
          handler.deleteReference(DataRequest.this, null, getETag(), new NoContentResponse(
              getServiceMetaData(), response));
        } else {
          try {
            handler.deleteReference(DataRequest.this, new URI(id), getETag(), new NoContentResponse(
                getServiceMetaData(), response));
          } catch (URISyntaxException e) {
            throw new DeserializerException("failed to read $id", e, MessageKeys.UNKNOWN_CONTENT);
          }
        }
      } else if (isPUT()) {
        // note this is always against single reference
        handler.updateReference(DataRequest.this, getETag(), getPayload().get(0), new NoContentResponse(
            getServiceMetaData(), response));
      } else if (isPOST()) {
        // this needs to be against collection of references
        handler.addReference(DataRequest.this, getETag(), getPayload().get(0), new NoContentResponse(
            getServiceMetaData(), response));
      }
    }

    // http://docs.oasis-open.org/odata/odata-json-format/v4.0/errata02/os
    // /odata-json-format-v4.0-errata02-os-complete.html#_Toc403940643
    // The below code reads as property and converts to an URI
    private List<URI> getPayload() throws DeserializerException {
      ODataDeserializer deserializer = odata.createDeserializer(getRequestContentType(), getServiceMetaData());
      return deserializer.entityReferences(getODataRequest().getBody()).getEntityReferences();
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      ContextURL.Builder builder = ContextURL.with().suffix(Suffix.REFERENCE);
      if (isCollection()) {
        builder.asCollection();
      }
      setServiceRoot(builder, getODataRequest());
      return builder.build();
    }
  }

  class PropertyRequest implements RequestType {

    @Override
    public boolean assertHttpMethod(ODataResponse response) throws ODataHandlerException {
      if ((isGET() || isDELETE() || isPropertyStream())
          && getReturnRepresentation() != ReturnRepresentation.NONE) {
        return methodNotAllowed(response, httpMethod(), "Invalid Prefer header used", 
            allowedMethods());
      }
      
      // create of properties is not allowed,
      // only read, update, delete. Note that delete is
      // same as update with null

      // 11.4.9.4, collection properties are not supported with merge
      if (isPATCH() && (isCollection() || isPropertyStream())) {
        return methodNotAllowed(response, httpMethod(),
            "collection properties are not supported for merge",
            allowedMethods());
      }
      
      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }

    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] { HttpMethod.GET, HttpMethod.PUT,
          HttpMethod.PATCH, HttpMethod.DELETE };
    }
    
    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      if (isPropertyComplex()) {
        return ContentNegotiator.doContentNegotiation(getUriInfo().getFormatOption(),
            getODataRequest(), getCustomContentTypeSupport(),
            isCollection() ? RepresentationType.COLLECTION_COMPLEX : RepresentationType.COMPLEX);
      } else if (isPropertyStream()) {
        return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), request,
            getCustomContentTypeSupport(), RepresentationType.BINARY);
      }
      return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(),
          getCustomContentTypeSupport(), isCollection() ? RepresentationType.COLLECTION_PRIMITIVE
              : RepresentationType.PRIMITIVE);
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {

      EdmProperty edmProperty = getUriResourceProperty().getProperty();

      if (isGET()) {
        if (isPropertyStream()) {
          handler.read(DataRequest.this, new StreamResponse(getServiceMetaData(), response));
        } else {
          handler.read(DataRequest.this, buildResponse(response, edmProperty));
        }
      } else if (isPATCH()) {
        handler.updateProperty(DataRequest.this, getPropertyValueFromClient(edmProperty), false, true,
            getETag(), buildResponse(response, edmProperty));
      } else if (isPUT()) {
        if (isPropertyStream()) {
          handler.upsertStreamProperty(DataRequest.this, getETag(), request.getBody(),
              new NoContentResponse(getServiceMetaData(), response));
        } else {
          handler.updateProperty(DataRequest.this, getPropertyValueFromClient(edmProperty), false, false,
              getETag(), buildResponse(response, edmProperty));
        }
      } else if (isDELETE()) {
        if (isPropertyStream()) {
          handler.upsertStreamProperty(DataRequest.this, getETag(), null,
              new NoContentResponse(getServiceMetaData(), response));
        } else {
          Property property = new Property(edmProperty.getType().getFullQualifiedName()
              .getFullQualifiedNameAsString(), edmProperty.getName());
          handler.updateProperty(DataRequest.this, property, false, false, getETag(),
              buildResponse(response, edmProperty));
        }
      }
    }

    private PropertyResponse buildResponse(ODataResponse response, EdmProperty edmProperty)
        throws ContentNegotiatorException, SerializerException {
      PropertyResponse propertyResponse = PropertyResponse.getInstance(DataRequest.this, response,
          edmProperty.getType(), getContextURL(odata), edmProperty.isCollection());
      return propertyResponse;
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      final UriHelper helper = odata.createUriHelper();
      EdmProperty edmProperty = getUriResourceProperty().getProperty();

      ContextURL.Builder builder =
          ContextURL.with().entitySetOrSingletonOrType(getTargetEntitySet(getEntitySet(), getNavigations()));
      builder.keyPath(helper.buildContextURLKeyPredicate(getUriResourceEntitySet()
          .getKeyPredicates()));
      String navPath = buildNavPath(helper, getEntitySet().getEntityType(), getNavigations(), true);
      if (navPath != null && !navPath.isEmpty()) {
        builder.navOrPropertyPath(navPath+"/"+edmProperty.getName());
      } else {
        builder.navOrPropertyPath(edmProperty.getName());
      }
      setServiceRoot(builder, getODataRequest());      
      if (isPropertyComplex()) {
        EdmComplexType complexType = ((UriResourceComplexProperty) uriResourceProperty).getComplexType();
        String select = helper.buildContextURLSelectList(complexType, getUriInfo().getExpandOption(),
            getUriInfo().getSelectOption());
        builder.selectList(select);
      }
      return builder.build();
    }
  }

  class ValueRequest extends PropertyRequest {

    @Override
    public boolean assertHttpMethod(ODataResponse response)
        throws ODataHandlerException {
      //part2-url-conventions # 4.7
      // Properties of type Edm.Stream already return the raw value of 
      // the media stream and do not support appending the $value segment.
      if (isPropertyStream() && isGET()) {
        return methodNotAllowed(response, httpMethod(), "Edm.Stream properties do not support $value", 
            allowedMethods());
      }

      if ((isGET() || isDELETE() || isPropertyStream())
          && getReturnRepresentation() != ReturnRepresentation.NONE) {
        return methodNotAllowed(response, httpMethod(), "Invalid Prefer header used", 
            allowedMethods());
      }

      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }

    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] { HttpMethod.GET, HttpMethod.PUT, HttpMethod.DELETE };
    }
    
    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      RepresentationType valueRepresentationType =
          uriResourceProperty.getType() == odata.createPrimitiveTypeInstance(EdmPrimitiveTypeKind.Binary) ?
              RepresentationType.BINARY :
              RepresentationType.VALUE;
      return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), request,
          getCustomContentTypeSupport(), valueRepresentationType);
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {
      EdmProperty edmProperty = getUriResourceProperty().getProperty();
      if (isGET()) {
        handler.read(DataRequest.this, PrimitiveValueResponse.getInstance(DataRequest.this,
            response, isCollection(), getUriResourceProperty().getProperty()));
      } else if (isDELETE()) {
        Property property = new Property(
            edmProperty.getType().getFullQualifiedName().getFullQualifiedNameAsString(),
            edmProperty.getName());
        PropertyResponse propertyResponse = PropertyResponse.getInstance(DataRequest.this, response,
            edmProperty.getType(), getContextURL(odata), edmProperty.isCollection());
        handler.updateProperty(DataRequest.this, property, true, false, getETag(), propertyResponse);
      } else if (isPUT()) {
        PropertyResponse propertyResponse = PropertyResponse.getInstance(DataRequest.this, response,
            edmProperty.getType(), getContextURL(odata), edmProperty.isCollection());
        Property property = new Property(
            edmProperty.getType().getFullQualifiedName().getFullQualifiedNameAsString(),
            edmProperty.getName());
        property.setValue(ValueType.PRIMITIVE, getRawValueFromClient());
        handler.updateProperty(DataRequest.this, property, true, false,
            getETag(), propertyResponse);
      }
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      return null;
    }
  }

  class SingletonRequest implements RequestType {

    @Override
    public boolean assertHttpMethod(ODataResponse response)
        throws ODataHandlerException {
      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }
    
    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] {HttpMethod.GET};
    }
    
    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      return ContentNegotiator.doContentNegotiation(uriInfo.getFormatOption(), getODataRequest(),
          getCustomContentTypeSupport(), RepresentationType.ENTITY);
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      final UriHelper helper = odata.createUriHelper();
      ContextURL.Builder builder = buildEntitySetContextURL(helper,
          uriResourceSingleton.getSingleton(), null, getUriInfo(), getNavigations(), isCollection(), true, 
          getODataRequest());
      return builder.build();
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {
      handler.read(DataRequest.this,
          EntityResponse.getInstance(DataRequest.this, getContextURL(odata), false, response));
    }
  }

  class CrossJoinRequest implements RequestType {
    private final List<String> entitySetNames;

    public CrossJoinRequest(List<String> entitySetNames) {
      this.entitySetNames = entitySetNames;
    }

    @Override
    public boolean assertHttpMethod(ODataResponse response)
        throws ODataHandlerException {
      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }
    
    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] {HttpMethod.GET};
    }

    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      return ContentNegotiator.doContentNegotiation(getUriInfo().getFormatOption(),
          getODataRequest(), getCustomContentTypeSupport(), RepresentationType.COLLECTION_COMPLEX);
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {
      handler.crossJoin(DataRequest.this, this.entitySetNames, response);
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      ContextURL.Builder builder = ContextURL.with().asCollection();
      return builder.build();
    }
  }
  
  class ApplyRequest implements RequestType {

    @Override
    public boolean assertHttpMethod(ODataResponse response)
        throws ODataHandlerException {
      return ServiceRequest.assertHttpMethod(httpMethod(), allowedMethods(), response);
    }
    
    @Override
    public HttpMethod[] allowedMethods() {
      return new HttpMethod[] {HttpMethod.GET};
    }

    @Override
    public ContentType getResponseContentType() throws ContentNegotiatorException {
      return ContentNegotiator.doContentNegotiation(getUriInfo().getFormatOption(),
          getODataRequest(), getCustomContentTypeSupport(), RepresentationType.COLLECTION_COMPLEX);
    }

    @Override
    public void execute(ServiceHandler handler, ODataResponse response)
        throws ODataLibraryException, ODataApplicationException {
      handler.apply(DataRequest.this, response);
    }

    @Override
    public ContextURL getContextURL(OData odata) throws SerializerException {
      ContextURL.Builder builder = ContextURL.with().asCollection();
      return builder.build();
    }
  }
  
  private org.apache.olingo.commons.api.data.Property getPropertyValueFromClient(
      EdmProperty edmProperty) throws DeserializerException {
    ODataDeserializer deserializer = odata.createDeserializer(getRequestContentType(), getServiceMetaData());
    return deserializer.property(getODataRequest().getBody(), edmProperty).getProperty();
  }
  
  private Object getRawValueFromClient() throws DeserializerException {
    InputStream input = getODataRequest().getBody();
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    if (input != null) {
      try {
        ByteBuffer inBuffer = ByteBuffer.allocate(DEFAULT_BUFFER_SIZE);
        ReadableByteChannel ic = Channels.newChannel(input);
        WritableByteChannel oc = Channels.newChannel(buffer);
        while (ic.read(inBuffer) > 0) {
          inBuffer.flip();
          oc.write(inBuffer);
          inBuffer.rewind();
        }
        return buffer.toByteArray();
      } catch (IOException e) {
        throw new ODataRuntimeException("Error on reading content");
      }
    }
    return null;
  }

  static ContextURL.Builder buildEntitySetContextURL(UriHelper helper,
      EdmBindingTarget edmEntitySet, List<UriParameter> keyPredicates, UriInfo uriInfo,
      LinkedList<UriResourceNavigation> navigations, boolean collectionReturn, boolean singleton, ODataRequest request)
      throws SerializerException {

    ContextURL.Builder builder =
        ContextURL.with().entitySetOrSingletonOrType(getTargetEntitySet(edmEntitySet, navigations));
    String select = helper.buildContextURLSelectList(edmEntitySet.getEntityType(),
        uriInfo.getExpandOption(), uriInfo.getSelectOption());
    if (!singleton) {
      builder.suffix(collectionReturn ? null : Suffix.ENTITY);
    }

    setServiceRoot(builder, request);    
    builder.selectList(select);

    final UriInfoResource resource = uriInfo.asUriInfoResource();
    final List<UriResource> resourceParts = resource.getUriResourceParts();
    final List<String> path = getPropertyPath(resourceParts);
    String propertyPath = buildPropertyPath(path);
    final String navPath = buildNavPath(helper, edmEntitySet.getEntityType(), navigations, collectionReturn);
    if (navPath != null && !navPath.isEmpty()) {
      if (keyPredicates != null) {
        builder.keyPath(helper.buildContextURLKeyPredicate(keyPredicates));
      }
      if (propertyPath != null) {
        propertyPath = navPath+"/"+propertyPath;
      } else {
        propertyPath = navPath;
      }
    }
    builder.navOrPropertyPath(propertyPath);
    return builder;
  }

  private static void setServiceRoot(ContextURL.Builder builder, ODataRequest request) {
    String serviceRoot = request.getRawBaseUri();
    if (serviceRoot != null) {
      try {
        if (!serviceRoot.endsWith("/")) {
          serviceRoot = serviceRoot + "/";
        }
        builder.serviceRoot(URI.create(serviceRoot));
      } catch (IllegalArgumentException e) {
        // ignore
      }
    }
  }

  private static List<String> getPropertyPath(final List<UriResource> path) {
    List<String> result = new LinkedList<String>();
    int index = 1;
    while (index < path.size() && path.get(index) instanceof UriResourceProperty) {
      result.add(((UriResourceProperty) path.get(index)).getProperty().getName());
      index++;
    }
    return result;
  }

  private static String buildPropertyPath(final List<String> path) {
    StringBuilder result = new StringBuilder();
    for (final String segment : path) {
      result.append(result.length() == 0 ? "" : '/').append(segment); //$NON-NLS-1$
    }
    return result.length() == 0?null:result.toString();
  }

  static String getTargetEntitySet(EdmBindingTarget root, LinkedList<UriResourceNavigation> navigations) {
    EdmEntityType type = root.getEntityType();
    EdmBindingTarget targetEntitySet = root;
    String targetEntitySetName = root.getName();
    String name = null;
    for (UriResourceNavigation nav:navigations) {
      name = nav.getProperty().getName();
      EdmNavigationProperty property = type.getNavigationProperty(name);
      if (property.containsTarget()) {
        return root.getName();
      }
      type = nav.getProperty().getType();
      
      for(EdmNavigationPropertyBinding enb:targetEntitySet.getNavigationPropertyBindings()) {
        if (enb.getPath().equals(name)) {
          targetEntitySetName = enb.getTarget();
        } else if (enb.getPath().endsWith("/"+name)) {
          targetEntitySetName = enb.getTarget();
        }
      }
    }
    return targetEntitySetName;
  }
  
  static String buildNavPath(UriHelper helper, EdmEntityType rootType,
      LinkedList<UriResourceNavigation> navigations, boolean includeLastPredicates)
      throws SerializerException {
    if (navigations.isEmpty()) {
      return null;
    }
    StringBuilder sb = new StringBuilder();
    boolean containsTarget = false;
    EdmEntityType type = rootType;
    for (UriResourceNavigation nav:navigations) {
      String name = nav.getProperty().getName();
      EdmNavigationProperty property = type.getNavigationProperty(name);
      if (property.containsTarget()) {
        containsTarget = true;
      }
      type = nav.getProperty().getType();
    }

    if (containsTarget) {
      for (int i = 0; i < navigations.size(); i++) {
        UriResourceNavigation nav = navigations.get(i);
        if (i > 0) {
          sb.append("/");
        }
        sb.append(nav.getProperty().getName());

        boolean skipKeys = false;
        if (navigations.size() == i+1 && !includeLastPredicates ) {
          skipKeys = true;
        }

        if (!skipKeys && !nav.getKeyPredicates().isEmpty()) {
          sb.append("(");
          sb.append(helper.buildContextURLKeyPredicate(nav.getKeyPredicates()));
          sb.append(")");
        }

        if (nav.getTypeFilterOnCollection() != null) {
          sb.append("/")
            .append(nav.getTypeFilterOnCollection().getFullQualifiedName().getFullQualifiedNameAsString());
        } else if (nav.getTypeFilterOnEntry() != null) {
          sb.append("/")
            .append(nav.getTypeFilterOnEntry().getFullQualifiedName().getFullQualifiedNameAsString());
        }
      }
    }
    return sb.toString();
  }

  public void setApply(ApplyOption apply) {
    this.type = new ApplyRequest();
  }
}
