com.microsoft.azure.storage.table.TableDeserializer Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of azure-storage Show documentation
Show all versions of azure-storage Show documentation
SDK for Microsoft Azure Storage Clients
/**
* Copyright Microsoft Corporation
*
* Licensed 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 com.microsoft.azure.storage.table;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.security.Key;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.microsoft.azure.storage.Constants;
import com.microsoft.azure.storage.OperationContext;
import com.microsoft.azure.storage.StorageErrorCodeStrings;
import com.microsoft.azure.storage.StorageException;
import com.microsoft.azure.storage.core.EncryptionData;
import com.microsoft.azure.storage.core.JsonUtilities;
import com.microsoft.azure.storage.core.SR;
import com.microsoft.azure.storage.core.Utility;
/**
* Reserved for internal use. A class used to read Table entities.
*/
final class TableDeserializer {
/**
* 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 InputStream
to read the data to parse from.
* @param clazzType
* The class type T
implementing {@link TableEntity} for the entities returned. Set to
* null
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 R
. Set
* to null
to return the entities as instances of the class type T
.
* @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 ODataPayload> parseQueryResponse(final InputStream inStream,
final TableRequestOptions options, final Class clazzType, final EntityResolver resolver,
final OperationContext opContext) throws JsonParseException, IOException, InstantiationException,
IllegalAccessException, StorageException {
ODataPayload corePayload = null;
ODataPayload resolvedPayload = null;
ODataPayload> commonPayload = null;
JsonParser parser = Utility.getJsonParser(inStream);
try {
if (resolver != null) {
resolvedPayload = new ODataPayload();
commonPayload = resolvedPayload;
}
else {
corePayload = new ODataPayload();
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 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. Reads entity data from the specified
* JsonParser
using the specified class type and optionally projects the entity result with the
* specified resolver into a {@link TableResult} object.
*
* @param parser
* The JsonParser
to read the data to parse from.
* @param httpStatusCode
* The HTTP status code returned with the operation response.
* @param clazzType
* The class type T
implementing {@link TableEntity} for the entity returned. Set to
* null
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 R
. Set
* to null
to return the entitys as instance of the class type T
.
* @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} object 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.
*/
static TableResult parseSingleOpResponse(final InputStream inStream,
final TableRequestOptions options, final int httpStatusCode, final Class clazzType,
final EntityResolver resolver, final OperationContext opContext) throws JsonParseException, IOException,
InstantiationException, IllegalAccessException, StorageException {
JsonParser parser = Utility.getJsonParser(inStream);
try {
final TableResult res = parseJsonEntity(parser, clazzType,
null /*HashMap classProperties*/, resolver, options, opContext);
res.setHttpStatusCode(httpStatusCode);
return res;
}
finally {
parser.close();
}
}
/**
* 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 JsonParser
to read the data to parse from.
* @param clazzType
* The class type T
implementing {@link TableEntity} for the entity returned. Set to
* null
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 R
. Set
* to null
to return the entity as an instance of the class type T
.
* @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 TableResult parseJsonEntity(final JsonParser parser,
final Class clazzType, HashMap classProperties, final EntityResolver resolver,
final TableRequestOptions options, final OperationContext opContext) throws JsonParseException,
IOException, StorageException, InstantiationException, IllegalAccessException {
final TableResult res = new TableResult();
HashMap properties = new HashMap();
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);
}
}
// Deserialize the metadata property value to get the names of encrypted properties so that they can be parsed correctly below.
Key cek = null;
EncryptionData encryptionData = new EncryptionData();
HashSet encryptedPropertyDetailsSet = null;
if (options.getEncryptionPolicy() != null) {
EntityProperty propertyDetailsProperty = properties
.get(Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS);
EntityProperty keyProperty = properties.get(Constants.EncryptionConstants.TABLE_ENCRYPTION_KEY_DETAILS);
if (propertyDetailsProperty != null && !propertyDetailsProperty.getIsNull() &&
keyProperty != null && !keyProperty.getIsNull()) {
// Decrypt the metadata property value to get the names of encrypted properties.
cek = options.getEncryptionPolicy().decryptMetadataAndReturnCEK(partitionKey, rowKey, keyProperty,
propertyDetailsProperty, encryptionData);
properties.put(Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS, propertyDetailsProperty);
encryptedPropertyDetailsSet = parsePropertyDetails(propertyDetailsProperty);
}
else {
if (options.requireEncryption() != null && options.requireEncryption()) {
throw new StorageException(StorageErrorCodeStrings.DECRYPTION_ERROR,
SR.ENCRYPTION_DATA_NOT_PRESENT_ERROR, null);
}
}
}
// 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)) {
for (final Entry property : properties.entrySet()) {
if (Constants.EncryptionConstants.TABLE_ENCRYPTION_KEY_DETAILS.equals(property.getKey()))
{
// This and the following check are required because in JSON no-metadata, the type information for
// the properties are not returned and users are not expected to provide a type for them. So based
// on how the user defined property resolvers treat unknown properties, we might get unexpected results.
final EntityProperty newProp = new EntityProperty(property.getValue().getValueAsString(), EdmType.STRING);
properties.put(property.getKey(), newProp);
}
else if (Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS.equals(property.getKey()))
{
if (options.getEncryptionPolicy() == null) {
final EntityProperty newProp = new EntityProperty(property.getValue().getValueAsString(),
EdmType.BINARY);
properties.put(property.getKey(), newProp);
}
}
else if (options.getPropertyResolver() != null) {
final String key = property.getKey();
final String value = property.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,
isEncrypted(encryptedPropertyDetailsSet, key) ? EdmType.BINARY : edmType);
newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
properties.put(property.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);
}
PropertyPair propPair = classProperties.get(property.getKey());
if (propPair != null) {
EntityProperty newProp;
if (isEncrypted(encryptedPropertyDetailsSet, property.getKey())) {
newProp = new EntityProperty(property.getValue().getValueAsString(), EdmType.BINARY);
}
else {
newProp = new EntityProperty(property.getValue().getValueAsString(), propPair.type);
}
newProp.setDateBackwardCompatibility(options.getDateBackwardCompatibility());
properties.put(property.getKey(), newProp);
}
}
}
}
// set the result properties, now that they are appropriately parsed
if (options.getEncryptionPolicy() != null && cek != null) {
// decrypt properties, if necessary
properties = decryptProperties(properties, options, partitionKey, rowKey, cek, encryptionData);
}
res.setProperties(properties);
// use resolver if provided, else create entity based on clazz type
if (resolver != null) {
res.setResult(resolver.resolve(partitionKey, rowKey, timestamp, properties, 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(properties, opContext);
res.setResult(entity);
}
return res;
}
private static String getETagFromTimestamp(String timestampString) throws UnsupportedEncodingException {
timestampString = URLEncoder.encode(timestampString, Constants.UTF8_CHARSET);
return "W/\"datetime'" + timestampString + "'\"";
}
private static EdmType evaluateEdmType(JsonToken token, String value) {
EdmType edmType = null;
if (token == JsonToken.VALUE_FALSE || token == JsonToken.VALUE_TRUE) {
edmType = EdmType.BOOLEAN;
}
else if (token == JsonToken.VALUE_NUMBER_FLOAT) {
edmType = EdmType.DOUBLE;
}
else if (token == JsonToken.VALUE_NUMBER_INT) {
edmType = EdmType.INT32;
}
else {
edmType = EdmType.STRING;
}
return edmType;
}
private static boolean isEncrypted(HashSet encryptedPropertyDetailsSet, String key)
{
// Handle the case where the property is encrypted.
return encryptedPropertyDetailsSet != null && encryptedPropertyDetailsSet.contains(key);
}
private static HashMap decryptProperties(HashMap properties,
TableRequestOptions options, String partitionKey, String rowKey, Key contentEncryptionKey,
EncryptionData encryptionData) throws IOException, StorageException {
// Deserialize the metadata property value to get the names of encrypted properties.
EntityProperty propertyDetailsProperty = properties.get(Constants.EncryptionConstants.TABLE_ENCRYPTION_PROPERTY_DETAILS);
HashSet encryptedPropertyDetailsSet = parsePropertyDetails(propertyDetailsProperty);
return options.getEncryptionPolicy().decryptEntity(properties, encryptedPropertyDetailsSet, partitionKey,
rowKey, contentEncryptionKey, encryptionData);
}
private static HashSet parsePropertyDetails(EntityProperty propertyDetailsProperty) {
HashSet encryptedPropertyDetailsSet = null;
if (propertyDetailsProperty != null && !propertyDetailsProperty.getIsNull()) {
byte[] binaryVal = propertyDetailsProperty.getValueAsByteArray();
String stringProperty = new String(binaryVal, 0, binaryVal.length).replaceAll(" ", "");
encryptedPropertyDetailsSet = new HashSet(
Arrays.asList(stringProperty.substring(1, stringProperty.length() - 1).split(",")));
}
return encryptedPropertyDetailsSet;
}
}