/**
   * Reserved for internal use. Execute this table operation on the specified table, using the
   * specified {@link TableRequestOptions} and {@link OperationContext}.
   *
   * <p>This method will invoke the Storage Service REST API to execute this table operation, using
   * the Table service endpoint and storage account credentials in the {@link CloudTableClient}
   * object.
   *
   * @param client A {@link CloudTableClient} instance specifying the Table service endpoint,
   *     storage account credentials, and any additional query parameters.
   * @param tableName A <code>String</code> containing the name of the table.
   * @param options A {@link TableRequestOptions} object that specifies execution options such as
   *     retry policy and timeout settings for the operation.
   * @param opContext An {@link OperationContext} object for tracking the current operation.
   * @return A {@link TableResult} containing the results of executing the operation.
   * @throws StorageException if an error occurs in the storage operation.
   */
  protected TableResult execute(
      final CloudTableClient client,
      final String tableName,
      TableRequestOptions options,
      OperationContext opContext)
      throws StorageException {
    if (opContext == null) {
      opContext = new OperationContext();
    }

    if (options == null) {
      options = new TableRequestOptions();
    }

    opContext.initialize();
    options.applyDefaults(client);
    Utility.assertNotNullOrEmpty("TableName", tableName);

    if (this.getOperationType() == TableOperationType.INSERT
        || this.getOperationType() == TableOperationType.INSERT_OR_MERGE
        || this.getOperationType() == TableOperationType.INSERT_OR_REPLACE) {
      return this.performInsert(client, tableName, options, opContext);
    } else if (this.getOperationType() == TableOperationType.DELETE) {
      return this.performDelete(client, tableName, options, opContext);
    } else if (this.getOperationType() == TableOperationType.MERGE) {
      return this.performMerge(client, tableName, options, opContext);
    } else if (this.getOperationType() == TableOperationType.REPLACE) {
      return this.performUpdate(client, tableName, options, opContext);
    } else if (this.getOperationType() == TableOperationType.RETRIEVE) {
      return ((QueryTableOperation) this).performRetrieve(client, tableName, options, opContext);
    } else {
      throw new IllegalArgumentException("Unknown table operation");
    }
  }
  /**
   * Reserved for internal use. Perform a merge operation on the specified table, using the
   * specified {@link TableRequestOptions} and {@link OperationContext}.
   *
   * <p>This method will invoke the Merge Entity REST API to execute this table operation, using the
   * Table service endpoint and storage account credentials in the {@link CloudTableClient} object.
   *
   * @param client A {@link CloudTableClient} instance specifying the Table service endpoint,
   *     storage account credentials, and any additional query parameters.
   * @param tableName A <code>String</code> containing the name of the table.
   * @param options A {@link TableRequestOptions} object that specifies execution options such as
   *     retry policy and timeout settings for the operation.
   * @param opContext An {@link OperationContext} object for tracking the current operation.
   * @return A {@link TableResult} containing the results of executing the operation.
   * @throws StorageException if an error occurs in the storage operation.
   */
  private TableResult performMerge(
      final CloudTableClient client,
      final String tableName,
      final TableRequestOptions options,
      final OperationContext opContext)
      throws StorageException {
    Utility.assertNotNullOrEmpty("Merge requires a valid ETag", this.getEntity().getEtag());
    Utility.assertNotNullOrEmpty(
        "Merge requires a valid PartitionKey", this.getEntity().getPartitionKey());
    Utility.assertNotNullOrEmpty("Merge requires a valid RowKey", this.getEntity().getRowKey());

    final StorageOperation<CloudTableClient, TableOperation, TableResult> impl =
        new StorageOperation<CloudTableClient, TableOperation, TableResult>(options) {
          @Override
          public TableResult execute(
              final CloudTableClient client,
              final TableOperation operation,
              final OperationContext opContext)
              throws Exception {

            final HttpURLConnection request =
                TableRequest.merge(
                    client.getTransformedEndPoint(opContext),
                    tableName,
                    generateRequestIdentity(false, null, false),
                    operation.getEntity().getEtag(),
                    options.getTimeoutIntervalInMs(),
                    null,
                    options,
                    opContext);

            client.getCredentials().signRequestLite(request, -1L, opContext);

            AtomPubParser.writeSingleEntityToStream(
                operation.getEntity(), false, request.getOutputStream(), opContext);

            this.setResult(ExecutionEngine.processRequest(request, opContext));

            if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND
                || this.getResult().getStatusCode() == HttpURLConnection.HTTP_CONFLICT) {
              throw TableServiceException.generateTableServiceException(
                  false, this.getResult(), operation, request.getErrorStream());
            }

            if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
              return operation.parseResponse(
                  null,
                  this.getResult().getStatusCode(),
                  request.getHeaderField(TableConstants.HeaderConstants.ETAG),
                  opContext);
            } else {
              throw TableServiceException.generateTableServiceException(
                  true, this.getResult(), operation, request.getErrorStream());
            }
          }
        };

    return ExecutionEngine.executeWithRetry(
        client, this, impl, options.getRetryPolicyFactory(), opContext);
  }
  private void doQueryEscapeTest(String data) throws StorageException {
    TableRequestOptions options = new TableRequestOptions();

    options.setTablePayloadFormat(TablePayloadFormat.JsonFullMetadata);
    doQueryEscapeTestHelper(data, options);

    options.setTablePayloadFormat(TablePayloadFormat.Json);
    doQueryEscapeTestHelper(data, options);

    options.setTablePayloadFormat(TablePayloadFormat.JsonNoMetadata);
    doQueryEscapeTestHelper(data, options);

    options.setTablePayloadFormat(TablePayloadFormat.JsonNoMetadata);
    options.setPropertyResolver(new Class1());
    doQueryEscapeTestHelper(data, options);
  }
  private void doEscapeTest(String data, boolean useBatch, boolean includeInKey)
      throws StorageException {
    TableRequestOptions options = new TableRequestOptions();

    options.setTablePayloadFormat(TablePayloadFormat.JsonFullMetadata);
    doEscapeTestHelper(data, useBatch, includeInKey, options);

    options.setTablePayloadFormat(TablePayloadFormat.Json);
    doEscapeTestHelper(data, useBatch, includeInKey, options);

    options.setTablePayloadFormat(TablePayloadFormat.JsonNoMetadata);
    doEscapeTestHelper(data, useBatch, includeInKey, options);

    options.setTablePayloadFormat(TablePayloadFormat.JsonNoMetadata);
    options.setPropertyResolver(new Class1());
    doEscapeTestHelper(data, useBatch, includeInKey, options);
  }
  /**
   * Reserved for internal use. Performs an insert operation on the specified table, using the
   * specified {@link TableRequestOptions} and {@link OperationContext}.
   *
   * <p>This method will invoke the Insert Entity REST API to execute this table operation, using
   * the Table service endpoint and storage account credentials in the {@link CloudTableClient}
   * object.
   *
   * @param client A {@link CloudTableClient} instance specifying the Table service endpoint,
   *     storage account credentials, and any additional query parameters.
   * @param tableName A <code>String</code> containing the name of the table.
   * @param options A {@link TableRequestOptions} object that specifies execution options such as
   *     retry policy and timeout settings for the operation.
   * @param opContext An {@link OperationContext} object for tracking the current operation.
   * @return A {@link TableResult} containing the results of executing the operation.
   * @throws StorageException if an error occurs in the storage operation.
   */
  private TableResult performInsert(
      final CloudTableClient client,
      final String tableName,
      final TableRequestOptions options,
      final OperationContext opContext)
      throws StorageException {
    final boolean isTableEntry = TableConstants.TABLES_SERVICE_TABLES_NAME.equals(tableName);
    final String tableIdentity =
        isTableEntry
            ? this.getEntity()
                .writeEntity(opContext)
                .get(TableConstants.TABLE_NAME)
                .getValueAsString()
            : null;

    // Upserts need row key and partition key
    if (!isTableEntry && this.opType != TableOperationType.INSERT) {
      Utility.assertNotNullOrEmpty(
          "Upserts require a valid PartitionKey", this.getEntity().getPartitionKey());
      Utility.assertNotNullOrEmpty("Upserts require a valid RowKey", this.getEntity().getRowKey());
    }

    final StorageOperation<CloudTableClient, TableOperation, TableResult> impl =
        new StorageOperation<CloudTableClient, TableOperation, TableResult>(options) {
          @Override
          public TableResult execute(
              final CloudTableClient client,
              final TableOperation operation,
              final OperationContext opContext)
              throws Exception {
            final HttpURLConnection request =
                TableRequest.insert(
                    client.getTransformedEndPoint(opContext),
                    tableName,
                    generateRequestIdentity(isTableEntry, tableIdentity, false),
                    operation.opType != TableOperationType.INSERT
                        ? operation.getEntity().getEtag()
                        : null,
                    operation.opType.getUpdateType(),
                    options.getTimeoutIntervalInMs(),
                    null,
                    options,
                    opContext);

            client.getCredentials().signRequestLite(request, -1L, opContext);

            AtomPubParser.writeSingleEntityToStream(
                operation.getEntity(), isTableEntry, request.getOutputStream(), opContext);

            this.setResult(ExecutionEngine.processRequest(request, opContext));
            if (operation.opType == TableOperationType.INSERT) {
              if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_CONFLICT) {
                throw TableServiceException.generateTableServiceException(
                    false, this.getResult(), operation, request.getErrorStream());
              }

              if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_CREATED) {
                throw TableServiceException.generateTableServiceException(
                    true, this.getResult(), operation, request.getErrorStream());
              }

              InputStream inStream = request.getInputStream();
              TableResult res = null;

              try {
                final XMLStreamReader xmlr = Utility.createXMLStreamReaderFromStream(inStream);
                res =
                    operation.parseResponse(
                        xmlr, this.getResult().getStatusCode(), null, opContext);
              } finally {
                inStream.close();
              }

              return res;
            } else {
              if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_NO_CONTENT) {
                return operation.parseResponse(
                    null,
                    this.getResult().getStatusCode(),
                    request.getHeaderField(TableConstants.HeaderConstants.ETAG),
                    opContext);
              } else {
                throw TableServiceException.generateTableServiceException(
                    true, this.getResult(), operation, request.getErrorStream());
              }
            }
          }
        };

    return ExecutionEngine.executeWithRetry(
        client, this, impl, options.getRetryPolicyFactory(), opContext);
  }
  /**
   * Reserved for internal use. Performs a delete operation on the specified table, using the
   * specified {@link TableRequestOptions} and {@link OperationContext}.
   *
   * <p>This method will invoke the <a
   * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd135727.aspx">Delete Entity</a>
   * REST API to execute this table operation, using the Table service endpoint and storage account
   * credentials in the {@link CloudTableClient} object.
   *
   * @param client A {@link CloudTableClient} instance specifying the Table service endpoint,
   *     storage account credentials, and any additional query parameters.
   * @param tableName A <code>String</code> containing the name of the table.
   * @param options A {@link TableRequestOptions} object that specifies execution options such as
   *     retry policy and timeout settings for the operation.
   * @param opContext An {@link OperationContext} object for tracking the current operation.
   * @return A {@link TableResult} containing the results of executing the operation.
   * @throws StorageException if an error occurs in the storage operation.
   */
  private TableResult performDelete(
      final CloudTableClient client,
      final String tableName,
      final TableRequestOptions options,
      final OperationContext opContext)
      throws StorageException {
    final boolean isTableEntry = TableConstants.TABLES_SERVICE_TABLES_NAME.equals(tableName);
    final String tableIdentity =
        isTableEntry
            ? this.getEntity()
                .writeEntity(opContext)
                .get(TableConstants.TABLE_NAME)
                .getValueAsString()
            : null;

    if (!isTableEntry) {
      Utility.assertNotNullOrEmpty("Delete requires a valid ETag", this.getEntity().getEtag());
      Utility.assertNotNullOrEmpty(
          "Delete requires a valid PartitionKey", this.getEntity().getPartitionKey());
      Utility.assertNotNullOrEmpty("Delete requires a valid RowKey", this.getEntity().getRowKey());
    }

    final StorageOperation<CloudTableClient, TableOperation, TableResult> impl =
        new StorageOperation<CloudTableClient, TableOperation, TableResult>(options) {
          @Override
          public TableResult execute(
              final CloudTableClient client,
              final TableOperation operation,
              final OperationContext opContext)
              throws Exception {

            final HttpURLConnection request =
                TableRequest.delete(
                    client.getTransformedEndPoint(opContext),
                    tableName,
                    generateRequestIdentity(isTableEntry, tableIdentity, false),
                    operation.getEntity().getEtag(),
                    options.getTimeoutIntervalInMs(),
                    null,
                    options,
                    opContext);

            client.getCredentials().signRequestLite(request, -1L, opContext);

            this.setResult(ExecutionEngine.processRequest(request, opContext));

            if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND
                || this.getResult().getStatusCode() == HttpURLConnection.HTTP_CONFLICT) {
              throw TableServiceException.generateTableServiceException(
                  false, this.getResult(), operation, request.getErrorStream());
            }

            if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_NO_CONTENT) {
              throw TableServiceException.generateTableServiceException(
                  true, this.getResult(), operation, request.getErrorStream());
            }

            return operation.parseResponse(null, this.getResult().getStatusCode(), null, opContext);
          }
        };

    return ExecutionEngine.executeWithRetry(
        client, this, impl, options.getRetryPolicyFactory(), opContext);
  }
  /**
   * Reserved for internal use. Parses the operation response as a collection of entities. Reads
   * entity data from the specified input stream using the specified class type and optionally
   * projects each entity result with the specified resolver into an {@link ODataPayload} containing
   * a collection of {@link TableResult} objects.
   *
   * @param inStream The <code>InputStream</code> to read the data to parse from.
   * @param clazzType The class type <code>T</code> implementing {@link TableEntity} for the
   *     entities returned. Set to <code>null</code> to ignore the returned entities and copy only
   *     response properties into the {@link TableResult} objects.
   * @param resolver An {@link EntityResolver} instance to project the entities into instances of
   *     type <code>R</code>. Set to <code>null</code> to return the entities as instances of the
   *     class type <code>T</code>.
   * @param options A {@link TableRequestOptions} object that specifies execution options such as
   *     retry policy and timeout settings for the operation.
   * @param opContext An {@link OperationContext} object used to track the execution of the
   *     operation.
   * @return An {@link ODataPayload} containing a collection of {@link TableResult} objects with the
   *     parsed operation response.
   * @throws InstantiationException if an error occurs while constructing the result.
   * @throws IllegalAccessException if an error occurs in reflection while parsing the result.
   * @throws StorageException if a storage service error occurs.
   * @throws IOException if an error occurs while accessing the stream.
   * @throws JsonParseException if an error occurs while parsing the stream.
   */
  @SuppressWarnings("unchecked")
  static <T extends TableEntity, R> ODataPayload<?> parseQueryResponse(
      final InputStream inStream,
      final TableRequestOptions options,
      final Class<T> clazzType,
      final EntityResolver<R> resolver,
      final OperationContext opContext)
      throws JsonParseException, IOException, InstantiationException, IllegalAccessException,
          StorageException {
    ODataPayload<T> corePayload = null;
    ODataPayload<R> resolvedPayload = null;
    ODataPayload<?> commonPayload = null;

    JsonParser parser = createJsonParserFromStream(inStream);

    try {

      if (resolver != null) {
        resolvedPayload = new ODataPayload<R>();
        commonPayload = resolvedPayload;
      } else {
        corePayload = new ODataPayload<T>();
        commonPayload = corePayload;
      }

      if (!parser.hasCurrentToken()) {
        parser.nextToken();
      }

      JsonUtilities.assertIsStartObjectJsonToken(parser);

      // move into data
      parser.nextToken();

      // if there is a clazz type and if JsonNoMetadata, create a classProperties dictionary to use
      // for type inference once
      // instead of querying the cache many times
      HashMap<String, PropertyPair> classProperties = null;
      if (options.getTablePayloadFormat() == TablePayloadFormat.JsonNoMetadata
          && clazzType != null) {
        classProperties = PropertyPair.generatePropertyPairs(clazzType);
      }

      while (parser.getCurrentToken() != null) {
        if (parser.getCurrentToken() == JsonToken.FIELD_NAME
            && parser.getCurrentName().equals(ODataConstants.VALUE)) {
          // move to start of array
          parser.nextToken();

          JsonUtilities.assertIsStartArrayJsonToken(parser);

          // go to properties
          parser.nextToken();

          while (parser.getCurrentToken() == JsonToken.START_OBJECT) {
            final TableResult res =
                parseJsonEntity(parser, clazzType, classProperties, resolver, options, opContext);
            if (corePayload != null) {
              corePayload.tableResults.add(res);
            }

            if (resolver != null) {
              resolvedPayload.results.add((R) res.getResult());
            } else {
              corePayload.results.add((T) res.getResult());
            }

            parser.nextToken();
          }

          JsonUtilities.assertIsEndArrayJsonToken(parser);
        }

        parser.nextToken();
      }
    } finally {
      parser.close();
    }

    return commonPayload;
  }
  /**
   * Reserved for internal use. Parses the operation response as an entity. Parses the result
   * returned in the specified stream in JSON format into a {@link TableResult} containing an entity
   * of the specified class type projected using the specified resolver.
   *
   * @param parser The <code>JsonParser</code> to read the data to parse from.
   * @param clazzType The class type <code>T</code> implementing {@link TableEntity} for the entity
   *     returned. Set to <code>null</code> to ignore the returned entity and copy only response
   *     properties into the {@link TableResult} object.
   * @param resolver An {@link EntityResolver} instance to project the entity into an instance of
   *     type <code>R</code>. Set to <code>null</code> to return the entity as an instance of the
   *     class type <code>T</code>.
   * @param options A {@link TableRequestOptions} object that specifies execution options such as
   *     retry policy and timeout settings for the operation.
   * @param opContext An {@link OperationContext} object used to track the execution of the
   *     operation.
   * @return A {@link TableResult} containing the parsed entity result of the operation.
   * @throws IOException if an error occurs while accessing the stream.
   * @throws InstantiationException if an error occurs while constructing the result.
   * @throws IllegalAccessException if an error occurs in reflection while parsing the result.
   * @throws StorageException if a storage service error occurs.
   * @throws IOException if an error occurs while accessing the stream.
   * @throws JsonParseException if an error occurs while parsing the stream.
   */
  private static <T extends TableEntity, R> TableResult parseJsonEntity(
      final JsonParser parser,
      final Class<T> clazzType,
      HashMap<String, PropertyPair> classProperties,
      final EntityResolver<R> resolver,
      final TableRequestOptions options,
      final OperationContext opContext)
      throws JsonParseException, IOException, StorageException, InstantiationException,
          IllegalAccessException {
    final TableResult res = new TableResult();

    final HashMap<String, EntityProperty> properties = new HashMap<String, EntityProperty>();

    if (!parser.hasCurrentToken()) {
      parser.nextToken();
    }

    JsonUtilities.assertIsStartObjectJsonToken(parser);

    parser.nextToken();

    // get all metadata, if present
    while (parser.getCurrentName().startsWith(ODataConstants.ODATA_PREFIX)) {
      final String name = parser.getCurrentName().substring(ODataConstants.ODATA_PREFIX.length());

      // get the value token
      parser.nextToken();

      if (name.equals(ODataConstants.ETAG)) {
        String etag = parser.getValueAsString();
        res.setEtag(etag);
      }

      // get the key token
      parser.nextToken();
    }

    if (resolver == null && clazzType == null) {
      return res;
    }

    // get object properties
    while (parser.getCurrentToken() != JsonToken.END_OBJECT) {
      String key = Constants.EMPTY_STRING;
      String val = Constants.EMPTY_STRING;
      EdmType edmType = null;

      // checks if this property is preceded by an OData property type annotation
      if (options.getTablePayloadFormat() != TablePayloadFormat.JsonNoMetadata
          && parser.getCurrentName().endsWith(ODataConstants.ODATA_TYPE_SUFFIX)) {
        parser.nextToken();
        edmType = EdmType.parse(parser.getValueAsString());

        parser.nextValue();
        key = parser.getCurrentName();
        val = parser.getValueAsString();
      } else {
        key = parser.getCurrentName();

        parser.nextToken();
        val = parser.getValueAsString();
        edmType = evaluateEdmType(parser.getCurrentToken(), parser.getValueAsString());
      }

      final EntityProperty newProp = new EntityProperty(val, edmType);
      newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
      properties.put(key, newProp);

      parser.nextToken();
    }

    String partitionKey = null;
    String rowKey = null;
    Date timestamp = null;
    String etag = null;

    // Remove core properties from map and set individually
    EntityProperty tempProp = properties.remove(TableConstants.PARTITION_KEY);
    if (tempProp != null) {
      partitionKey = tempProp.getValueAsString();
    }

    tempProp = properties.remove(TableConstants.ROW_KEY);
    if (tempProp != null) {
      rowKey = tempProp.getValueAsString();
    }

    tempProp = properties.remove(TableConstants.TIMESTAMP);
    if (tempProp != null) {
      tempProp.setDateBackwardCompatibility(false);
      timestamp = tempProp.getValueAsDate();

      if (res.getEtag() == null) {
        etag = getETagFromTimestamp(tempProp.getValueAsString());
        res.setEtag(etag);
      }
    }

    // do further processing for type if JsonNoMetdata by inferring type information via resolver or
    // clazzType
    if (options.getTablePayloadFormat() == TablePayloadFormat.JsonNoMetadata
        && (options.getPropertyResolver() != null || clazzType != null)) {
      if (options.getPropertyResolver() != null) {
        for (final Entry<String, EntityProperty> p : properties.entrySet()) {
          final String key = p.getKey();
          final String value = p.getValue().getValueAsString();
          EdmType edmType;

          // try to use the property resolver to get the type
          try {
            edmType =
                options.getPropertyResolver().propertyResolver(partitionKey, rowKey, key, value);
          } catch (Exception e) {
            throw new StorageException(
                StorageErrorCodeStrings.INTERNAL_ERROR,
                SR.CUSTOM_RESOLVER_THREW,
                Constants.HeaderConstants.HTTP_UNUSED_306,
                null,
                e);
          }

          // try to create a new entity property using the returned type
          try {
            final EntityProperty newProp = new EntityProperty(value, edmType);
            newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
            properties.put(p.getKey(), newProp);
          } catch (IllegalArgumentException e) {
            throw new StorageException(
                StorageErrorCodeStrings.INVALID_TYPE,
                String.format(SR.FAILED_TO_PARSE_PROPERTY, key, value, edmType),
                Constants.HeaderConstants.HTTP_UNUSED_306,
                null,
                e);
          }
        }
      } else if (clazzType != null) {
        if (classProperties == null) {
          classProperties = PropertyPair.generatePropertyPairs(clazzType);
        }
        for (final Entry<String, EntityProperty> p : properties.entrySet()) {
          PropertyPair propPair = classProperties.get(p.getKey());
          if (propPair != null) {
            final EntityProperty newProp =
                new EntityProperty(p.getValue().getValueAsString(), propPair.type);
            newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
            properties.put(p.getKey(), newProp);
          }
        }
      }
    }

    // set the result properties, now that they are appropriately parsed
    res.setProperties(properties);

    // use resolver if provided, else create entity based on clazz type
    if (resolver != null) {
      res.setResult(
          resolver.resolve(partitionKey, rowKey, timestamp, res.getProperties(), res.getEtag()));
    } else if (clazzType != null) {
      // Generate new entity and return
      final T entity = clazzType.newInstance();
      entity.setEtag(res.getEtag());

      entity.setPartitionKey(partitionKey);
      entity.setRowKey(rowKey);
      entity.setTimestamp(timestamp);

      entity.readEntity(res.getProperties(), opContext);

      res.setResult(entity);
    }

    return res;
  }