com.microsoft.windowsazure.storage.table.TableParser Maven / Gradle / Ivy
/**
* 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.windowsazure.storage.table;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map.Entry;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import org.apache.commons.lang3.StringEscapeUtils;
import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.microsoft.windowsazure.storage.Constants;
import com.microsoft.windowsazure.storage.OperationContext;
import com.microsoft.windowsazure.storage.StorageErrorCodeStrings;
import com.microsoft.windowsazure.storage.StorageException;
import com.microsoft.windowsazure.storage.core.SR;
import com.microsoft.windowsazure.storage.core.Utility;
/**
* Reserved for internal use. A class used to read and write Table entities in OData AtomPub format requests and
* responses.
*
* For more information about OData, see the Open Data Protocol website. For more
* information about the AtomPub format used in OData, see OData Protocol Atom Format.
*/
class TableParser {
/**
* Used to create Json parsers and generators.
*/
private static JsonFactory jsonFactory = new JsonFactory();
/**
* 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 format
* The {@link TablePayloadFormat} to use for parsing.
* @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 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 XMLStreamException
* if an error occurs while accessing the stream with AtomPub.
* @throws ParseException
* if an error occurs while parsing the stream.
* @throws StorageException
* if a storage service error occurs.
* @throws IOException
* if an error occurs while accessing the stream with Json.
* @throws JsonParseException
* if an error occurs while parsing the stream.
*/
protected static ODataPayload> parseQueryResponse(final InputStream inStream,
final TableRequestOptions options, final Class clazzType, final EntityResolver resolver,
final OperationContext opContext) throws InstantiationException, IllegalAccessException,
XMLStreamException, ParseException, StorageException, JsonParseException, IOException {
ODataPayload> payload;
if (options.getTablePayloadFormat() == TablePayloadFormat.AtomPub) {
payload = parseAtomQueryResponse(inStream, clazzType, resolver, opContext);
}
else {
payload = parseJsonQueryResponse(inStream, clazzType, resolver, options, opContext);
}
return payload;
}
/**
* 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 format
* The {@link TablePayloadFormat} to use for parsing.
* @param httpStatusCode
* The HTTP status code returned with the operation response.
* @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 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 XMLStreamException
* if an error occurs while accessing the stream with AtomPub.
* @throws ParseException
* if an error occurs while parsing the stream.
* @throws StorageException
* if a storage service error occurs.
* @throws IOException
* if an error occurs while accessing the stream with Json.
* @throws JsonParseException
* if an error occurs while parsing the stream.
*/
protected static TableResult parseSingleOpResponse(final InputStream inStream,
final TableRequestOptions options, final int httpStatusCode, final Class clazzType,
final EntityResolver resolver, final OperationContext opContext) throws InstantiationException,
IllegalAccessException, XMLStreamException, ParseException, StorageException, IOException,
JsonParseException {
TableResult res;
if (options.getTablePayloadFormat() == TablePayloadFormat.AtomPub) {
res = parseSingleOpAtomResponse(inStream, httpStatusCode, clazzType, resolver, opContext);
}
else {
res = parseSingleOpJsonResponse(inStream, httpStatusCode, clazzType, resolver, options, opContext);
}
return res;
}
/**
* Reserved for internal use. Writes an entity to the stream, leaving the stream open for additional writing.
*
* @param outStream
* The OutputStream
to write the entity to.
* @param format
* The {@link TablePayloadFormat} to use for parsing.
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
* @throws XMLStreamException
* if an error occurs while accessing the stream with AtomPub.
* @throws StorageException
* if a Storage service error occurs.
* @throws IOException
* if an error occurs while accessing the stream with Json.
*/
protected static void writeSingleEntityToStream(final OutputStream outStream, final TablePayloadFormat format,
final TableEntity entity, final boolean isTableEntry, final OperationContext opContext)
throws XMLStreamException, StorageException, IOException {
if (format == TablePayloadFormat.AtomPub) {
writeSingleAtomEntity(outStream, entity, isTableEntry, opContext);
}
else {
writeSingleJsonEntity(outStream, format, entity, isTableEntry, opContext);
}
}
/**
* Reserved for internal use. Writes an entity to the stream, leaving the stream open for additional writing.
*
* @param strWriter
* The StringWriter
to write the entity to.
* @param format
* The {@link TablePayloadFormat} to use for parsing.
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
* @throws XMLStreamException
* if an error occurs while accessing the stream with AtomPub.
* @throws StorageException
* if a Storage service error occurs.
* @throws IOException
* if an error occurs while accessing the stream with Json.
*/
protected static void writeSingleEntityToString(final StringWriter strWriter, final TablePayloadFormat format,
final TableEntity entity, final boolean isTableEntry, final OperationContext opContext)
throws XMLStreamException, StorageException, IOException {
if (format == TablePayloadFormat.AtomPub) {
writeSingleAtomEntity(strWriter, entity, isTableEntry, opContext);
}
else {
writeSingleJsonEntity(strWriter, format, entity, isTableEntry, opContext);
}
}
/**
* 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 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 ParseException
* if an error occurs while parsing 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, ParseException, StorageException, InstantiationException, IllegalAccessException {
final TableResult res = new TableResult();
final HashMap properties = new HashMap();
if (!parser.hasCurrentToken()) {
parser.nextToken();
}
ODataUtilities.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);
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) {
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 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);
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 p : properties.entrySet()) {
PropertyPair propPair = classProperties.get(p.getKey());
if (propPair != null) {
final EntityProperty newProp = new EntityProperty(p.getValue().getValueAsString(),
propPair.type);
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;
}
/**
* 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 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 ParseException
* if an error occurs while parsing 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.
*/
@SuppressWarnings("unchecked")
private static ODataPayload> parseJsonQueryResponse(final InputStream inStream,
final Class clazzType, final EntityResolver resolver, final TableRequestOptions options,
final OperationContext opContext) throws ParseException, InstantiationException, IllegalAccessException,
StorageException, JsonParseException, IOException {
ODataPayload corePayload = null;
ODataPayload resolvedPayload = null;
ODataPayload> commonPayload = null;
JsonParser parser = createJsonParserFromStream(inStream);
try {
if (resolver != null) {
resolvedPayload = new ODataPayload();
commonPayload = resolvedPayload;
}
else {
corePayload = new ODataPayload();
commonPayload = corePayload;
}
if (!parser.hasCurrentToken()) {
parser.nextToken();
}
ODataUtilities.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();
ODataUtilities.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();
}
ODataUtilities.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 opContext
* An {@link OperationContext} object used to track the execution of the operation.
* @return
* A {@link TableResult} object with the parsed operation response.
* @throws ParseException
* if an error occurs while parsing 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 parseSingleOpJsonResponse(final InputStream inStream,
final int httpStatusCode, final Class clazzType, final EntityResolver resolver,
final TableRequestOptions options, final OperationContext opContext) throws ParseException,
InstantiationException, IllegalAccessException, StorageException, JsonParseException, IOException {
JsonParser parser = createJsonParserFromStream(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. Writes an entity to the specified JsonGenerator
as an JSON resource
*
* @param generator
* The JsonGenerator
to write the entity to.
* @param format
* The {@link TablePayloadFormat} to use for parsing.
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
*
* @throws StorageException
* if a Storage service error occurs.
* @throws IOException
* if an error occurs while accessing the stream.
*/
private static void writeJsonEntity(final JsonGenerator generator, TablePayloadFormat format,
final TableEntity entity, final boolean isTableEntry, final OperationContext opContext)
throws StorageException, IOException {
HashMap properties = entity.writeEntity(opContext);
if (properties == null) {
properties = new HashMap();
}
// start object
generator.writeStartObject();
if (!isTableEntry) {
Utility.assertNotNull(TableConstants.PARTITION_KEY, entity.getPartitionKey());
Utility.assertNotNull(TableConstants.ROW_KEY, entity.getRowKey());
Utility.assertNotNull(TableConstants.TIMESTAMP, entity.getTimestamp());
// PartitionKey
generator.writeStringField(TableConstants.PARTITION_KEY, entity.getPartitionKey());
// RowKey
generator.writeStringField(TableConstants.ROW_KEY, entity.getRowKey());
// Timestamp
generator.writeStringField(TableConstants.TIMESTAMP, Utility.getTimeByZoneAndFormat(entity.getTimestamp(),
Utility.UTC_ZONE, Utility.ISO8061_LONG_PATTERN));
}
for (final Entry ent : properties.entrySet()) {
if (ent.getKey().equals(TableConstants.PARTITION_KEY) || ent.getKey().equals(TableConstants.ROW_KEY)
|| ent.getKey().equals(TableConstants.TIMESTAMP) || ent.getKey().equals("Etag")) {
continue;
}
EntityProperty currProp = ent.getValue();
if (currProp.getEdmType().mustAnnotateType()) {
final String edmTypeString = currProp.getEdmType().toString();
// property type
generator.writeStringField(ent.getKey() + ODataConstants.ODATA_TYPE_SUFFIX, edmTypeString);
// property key and value
generator.writeStringField(ent.getKey(), ent.getValue().getValueAsString());
}
else if (currProp.getEdmType() == EdmType.DOUBLE && currProp.getIsNull() == false) {
final String edmTypeString = currProp.getEdmType().toString();
final Double value = currProp.getValueAsDouble();
// property type, if needed
if (value.equals(Double.POSITIVE_INFINITY) || value.equals(Double.NEGATIVE_INFINITY)
|| value.equals(Double.NaN)) {
generator.writeStringField(ent.getKey() + ODataConstants.ODATA_TYPE_SUFFIX, edmTypeString);
// property key and value
generator.writeStringField(ent.getKey(), ent.getValue().getValueAsString());
}
else {
writeJsonProperty(generator, ent);
}
}
else {
writeJsonProperty(generator, ent);
}
}
// end object
generator.writeEndObject();
}
/**
* Reserved for internal use. Writes an entity to the stream as an JSON resource, leaving the stream open
* for additional writing.
*
* @param outStream
* The OutputStream
to write the entity to.
* @param format
* The {@link TablePayloadFormat} to use for parsing.
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
*
* @throws StorageException
* if a Storage service error occurs.
* @throws IOException
* if an error occurs while accessing the stream.
*/
private static void writeSingleJsonEntity(final OutputStream outStream, TablePayloadFormat format,
final TableEntity entity, final boolean isTableEntry, final OperationContext opContext)
throws StorageException, IOException {
JsonGenerator generator = jsonFactory.createGenerator(outStream);
try {
// write to stream
writeJsonEntity(generator, format, entity, isTableEntry, opContext);
}
finally {
generator.close();
}
}
/**
* Reserved for internal use. Writes an entity to the stream as an JSON resource, leaving the stream open
* for additional writing.
*
* @param strWriter
* The StringWriter
to write the entity to.
* @param format
* The {@link TablePayloadFormat} to use for parsing.
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
*
* @throws StorageException
* if a Storage service error occurs.
* @throws IOException
* if an error occurs while accessing the stream.
*/
private static void writeSingleJsonEntity(final StringWriter strWriter, TablePayloadFormat format,
final TableEntity entity, final boolean isTableEntry, final OperationContext opContext)
throws StorageException, IOException {
JsonGenerator generator = jsonFactory.createGenerator(strWriter);
try {
// write to stream
writeJsonEntity(generator, format, entity, isTableEntry, opContext);
}
finally {
generator.close();
}
}
/**
* Reserved for internal use. Parses the operation response as an entity. Parses the result returned in the
* specified stream in AtomPub format into a {@link TableResult} containing an entity of the specified class type
* projected using the specified resolver.
*
* @param xmlr
* An XMLStreamReader
on the input stream.
* @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 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 XMLStreamException
* if an error occurs while accessing the stream.
* @throws ParseException
* if an error occurs while parsing 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.
*/
private static TableResult parseAtomEntity(final XMLStreamReader xmlr,
final Class clazzType, final EntityResolver resolver, final OperationContext opContext)
throws XMLStreamException, ParseException, InstantiationException, IllegalAccessException, StorageException {
int eventType = xmlr.getEventType();
final TableResult res = new TableResult();
xmlr.require(XMLStreamConstants.START_ELEMENT, null, ODataConstants.ENTRY);
String etag = StringEscapeUtils.unescapeHtml4(xmlr.getAttributeValue(ODataConstants.DATA_SERVICES_METADATA_NS,
ODataConstants.ETAG));
res.setEtag(etag);
while (xmlr.hasNext()) {
eventType = xmlr.next();
if (eventType == XMLStreamConstants.CHARACTERS) {
xmlr.getText();
continue;
}
final String name = xmlr.getName().toString();
if (eventType == XMLStreamConstants.START_ELEMENT) {
if (name.equals(ODataConstants.BRACKETED_ATOM_NS + ODataConstants.ID)) {
Utility.readElementFromXMLReader(xmlr, ODataConstants.ID);
}
else if (name.equals(ODataConstants.BRACKETED_DATA_SERVICES_METADATA_NS + ODataConstants.PROPERTIES)) {
// Do read properties
if (resolver == null && clazzType == null) {
return res;
}
else {
res.setProperties(readAtomProperties(xmlr, opContext));
break;
}
}
}
}
// Move to end Content
eventType = xmlr.next();
if (eventType == XMLStreamConstants.CHARACTERS) {
eventType = xmlr.next();
}
xmlr.require(XMLStreamConstants.END_ELEMENT, null, ODataConstants.CONTENT);
eventType = xmlr.next();
if (eventType == XMLStreamConstants.CHARACTERS) {
eventType = xmlr.next();
}
xmlr.require(XMLStreamConstants.END_ELEMENT, null, ODataConstants.ENTRY);
String rowKey = null;
String partitionKey = null;
Date timestamp = null;
// Remove core properties from map and set individually
EntityProperty tempProp = res.getProperties().remove(TableConstants.PARTITION_KEY);
if (tempProp != null) {
partitionKey = tempProp.getValueAsString();
}
tempProp = res.getProperties().remove(TableConstants.ROW_KEY);
if (tempProp != null) {
rowKey = tempProp.getValueAsString();
}
tempProp = res.getProperties().remove(TableConstants.TIMESTAMP);
if (tempProp != null) {
timestamp = tempProp.getValueAsDate();
}
if (resolver != null) {
// Call resolver
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;
}
/**
* 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 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 XMLStreamException
* if an error occurs while accessing the stream.
* @throws ParseException
* if an error occurs while parsing 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.
*/
@SuppressWarnings("unchecked")
private static ODataPayload> parseAtomQueryResponse(final InputStream inStream,
final Class clazzType, final EntityResolver resolver, final OperationContext opContext)
throws XMLStreamException, ParseException, InstantiationException, IllegalAccessException, StorageException {
ODataPayload corePayload = null;
ODataPayload resolvedPayload = null;
ODataPayload> commonPayload = null;
if (resolver != null) {
resolvedPayload = new ODataPayload();
commonPayload = resolvedPayload;
}
else {
corePayload = new ODataPayload();
commonPayload = corePayload;
}
final XMLStreamReader xmlr = Utility.createXMLStreamReaderFromStream(inStream);
int eventType = xmlr.getEventType();
xmlr.require(XMLStreamConstants.START_DOCUMENT, null, null);
eventType = xmlr.next();
xmlr.require(XMLStreamConstants.START_ELEMENT, null, ODataConstants.FEED);
// skip feed chars
eventType = xmlr.next();
while (xmlr.hasNext()) {
eventType = xmlr.next();
if (eventType == XMLStreamConstants.CHARACTERS) {
xmlr.getText();
continue;
}
final String name = xmlr.getName().toString();
if (eventType == XMLStreamConstants.START_ELEMENT) {
if (name.equals(ODataConstants.BRACKETED_ATOM_NS + ODataConstants.ENTRY)) {
final TableResult res = parseAtomEntity(xmlr, clazzType, resolver, opContext);
if (corePayload != null) {
corePayload.tableResults.add(res);
}
if (resolver != null) {
resolvedPayload.results.add((R) res.getResult());
}
else {
corePayload.results.add((T) res.getResult());
}
}
}
else if (eventType == XMLStreamConstants.END_ELEMENT
&& name.equals(ODataConstants.BRACKETED_ATOM_NS + ODataConstants.FEED)) {
break;
}
}
xmlr.require(XMLStreamConstants.END_ELEMENT, null, ODataConstants.FEED);
return commonPayload;
}
/**
* Reserved for internal use. Parses the operation response as an entity. Reads entity data from the specified
* XMLStreamReader
using the specified class type and optionally projects the entity result with the
* specified resolver into a {@link TableResult} object.
*
* @param xmlr
* The XMLStreamReader
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 opContext
* An {@link OperationContext} object used to track the execution of the operation.
* @return
* A {@link TableResult} object with the parsed operation response.
*
* @throws XMLStreamException
* if an error occurs while accessing the stream.
* @throws ParseException
* if an error occurs while parsing 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.
*/
private static TableResult parseSingleOpAtomResponse(final InputStream inStream,
final int httpStatusCode, final Class clazzType, final EntityResolver resolver,
final OperationContext opContext) throws XMLStreamException, ParseException, InstantiationException,
IllegalAccessException, StorageException {
XMLStreamReader xmlr = Utility.createXMLStreamReaderFromStream(inStream);
try {
xmlr.require(XMLStreamConstants.START_DOCUMENT, null, null);
xmlr.next();
final TableResult res = parseAtomEntity(xmlr, clazzType, resolver, opContext);
res.setHttpStatusCode(httpStatusCode);
return res;
}
finally {
xmlr.close();
}
}
/**
* Reserved for internal use. Reads the properties of an entity from the stream into a map of property names to
* typed values. Reads the entity data as an AtomPub Entry Resource from the specified {@link XMLStreamReader} into
* a map of String
property names to {@link EntityProperty} data typed values.
*
* @param xmlr
* The XMLStreamReader
to read the data from.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
*
* @return
* A java.util.HashMap
containing a map of String
property names to
* {@link EntityProperty} data typed values found in the entity data.
* @throws XMLStreamException
* if an error occurs accessing the stream.
* @throws ParseException
* if an error occurs converting the input to a particular data type.
*/
private static HashMap readAtomProperties(final XMLStreamReader xmlr,
final OperationContext opContext) throws XMLStreamException, ParseException {
int eventType = xmlr.getEventType();
xmlr.require(XMLStreamConstants.START_ELEMENT, null, ODataConstants.PROPERTIES);
final HashMap properties = new HashMap();
while (xmlr.hasNext()) {
eventType = xmlr.next();
if (eventType == XMLStreamConstants.CHARACTERS) {
xmlr.getText();
continue;
}
if (eventType == XMLStreamConstants.START_ELEMENT
&& xmlr.getNamespaceURI().equals(ODataConstants.DATA_SERVICES_NS)) {
final String key = xmlr.getLocalName();
String val = Constants.EMPTY_STRING;
String edmType = null;
if (xmlr.getAttributeCount() > 0) {
edmType = xmlr.getAttributeValue(ODataConstants.DATA_SERVICES_METADATA_NS, ODataConstants.TYPE);
}
// move to chars
eventType = xmlr.next();
if (eventType == XMLStreamConstants.CHARACTERS) {
val = xmlr.getText();
// end element
eventType = xmlr.next();
}
xmlr.require(XMLStreamConstants.END_ELEMENT, null, key);
final EntityProperty newProp = new EntityProperty(val, EdmType.parse(edmType));
properties.put(key, newProp);
}
else if (eventType == XMLStreamConstants.END_ELEMENT
&& xmlr.getName().toString()
.equals(ODataConstants.BRACKETED_DATA_SERVICES_METADATA_NS + ODataConstants.PROPERTIES)) {
// End read properties
break;
}
}
xmlr.require(XMLStreamConstants.END_ELEMENT, null, ODataConstants.PROPERTIES);
return properties;
}
/**
* Reserved for internal use. Writes an entity to the stream as an AtomPub Entry Resource, leaving the stream open
* for additional writing.
*
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param xmlw
* The XMLStreamWriter
to write the entity to.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
*
* @throws XMLStreamException
* if an error occurs accessing the stream.
* @throws StorageException
* if a Storage service error occurs.
*/
private static void writeAtomEntity(final TableEntity entity, final boolean isTableEntry,
final XMLStreamWriter xmlw, final OperationContext opContext) throws XMLStreamException, StorageException {
HashMap properties = entity.writeEntity(opContext);
if (properties == null) {
properties = new HashMap();
}
if (!isTableEntry) {
Utility.assertNotNull(TableConstants.PARTITION_KEY, entity.getPartitionKey());
Utility.assertNotNull(TableConstants.ROW_KEY, entity.getRowKey());
Utility.assertNotNull(TableConstants.TIMESTAMP, entity.getTimestamp());
}
// Begin entry
xmlw.writeStartElement("entry");
xmlw.writeNamespace("d", ODataConstants.DATA_SERVICES_NS);
xmlw.writeNamespace("m", ODataConstants.DATA_SERVICES_METADATA_NS);
// default namespace
xmlw.writeNamespace(null, ODataConstants.ATOM_NS);
// Content
xmlw.writeStartElement(ODataConstants.CONTENT);
xmlw.writeAttribute(ODataConstants.TYPE, ODataConstants.ODATA_CONTENT_TYPE);
// m:properties
xmlw.writeStartElement("m", ODataConstants.PROPERTIES, ODataConstants.DATA_SERVICES_METADATA_NS);
if (!isTableEntry) {
// d:PartitionKey
xmlw.writeStartElement("d", TableConstants.PARTITION_KEY, ODataConstants.DATA_SERVICES_NS);
xmlw.writeAttribute("xml", "xml", "space", "preserve");
xmlw.writeCharacters(entity.getPartitionKey());
xmlw.writeEndElement();
// d:RowKey
xmlw.writeStartElement("d", TableConstants.ROW_KEY, ODataConstants.DATA_SERVICES_NS);
xmlw.writeAttribute("xml", "xml", "space", "preserve");
xmlw.writeCharacters(entity.getRowKey());
xmlw.writeEndElement();
// d:Timestamp
if (entity.getTimestamp() == null) {
entity.setTimestamp(new Date());
}
xmlw.writeStartElement("d", TableConstants.TIMESTAMP, ODataConstants.DATA_SERVICES_NS);
xmlw.writeAttribute("m", ODataConstants.DATA_SERVICES_METADATA_NS, ODataConstants.TYPE,
EdmType.DATE_TIME.toString());
xmlw.writeCharacters(Utility.getTimeByZoneAndFormat(entity.getTimestamp(), Utility.UTC_ZONE,
Utility.ISO8061_LONG_PATTERN));
xmlw.writeEndElement();
}
for (final Entry ent : properties.entrySet()) {
if (ent.getKey().equals(TableConstants.PARTITION_KEY) || ent.getKey().equals(TableConstants.ROW_KEY)
|| ent.getKey().equals(TableConstants.TIMESTAMP) || ent.getKey().equals("Etag")) {
continue;
}
EntityProperty currProp = ent.getValue();
// d:PropName
xmlw.writeStartElement("d", ent.getKey(), ODataConstants.DATA_SERVICES_NS);
if (currProp.getEdmType() == EdmType.STRING) {
xmlw.writeAttribute("xml", "xml", "space", "preserve");
}
else if (currProp.getEdmType().toString().length() != 0) {
String edmTypeString = currProp.getEdmType().toString();
if (edmTypeString.length() != 0) {
xmlw.writeAttribute("m", ODataConstants.DATA_SERVICES_METADATA_NS, ODataConstants.TYPE,
edmTypeString);
}
}
if (currProp.getIsNull()) {
xmlw.writeAttribute("m", ODataConstants.DATA_SERVICES_METADATA_NS, ODataConstants.NULL, Constants.TRUE);
}
// Write Value
xmlw.writeCharacters(currProp.getValueAsString());
// End d:PropName
xmlw.writeEndElement();
}
// End m:properties
xmlw.writeEndElement();
// End content
xmlw.writeEndElement();
// End entry
xmlw.writeEndElement();
}
/**
* Reserved for internal use. Writes a single entity to the specified OutputStream
as a complete XML
* document.
*
* @param outStream
* The OutputStream
to write the entity to.
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
* @throws XMLStreamException
* if an error occurs creating or accessing the stream.
* @throws StorageException
* if a Storage service error occurs.
*/
private static void writeSingleAtomEntity(final OutputStream outStream, final TableEntity entity,
final boolean isTableEntry, final OperationContext opContext) throws XMLStreamException, StorageException {
final XMLOutputFactory xmlOutFactoryInst = XMLOutputFactory.newInstance();
XMLStreamWriter xmlw = xmlOutFactoryInst.createXMLStreamWriter(outStream, Constants.UTF8_CHARSET);
// default is UTF8
xmlw.writeStartDocument(Constants.UTF8_CHARSET, "1.0");
writeAtomEntity(entity, isTableEntry, xmlw, opContext);
// end doc
xmlw.writeEndDocument();
xmlw.flush();
}
/**
* Reserved for internal use. Writes a single entity to the specified OutputStream
as a complete XML
* document.
*
* @param entity
* The instance implementing {@link TableEntity} to write to the output stream.
* @param isTableEntry
* A flag indicating the entity is a reference to a table at the top level of the storage service when
* true and a reference to an entity within a table when false
.
* @param opContext
* An {@link OperationContext} object used to track the execution of the operation.
* @param outStream
* The OutputStream
to write the entity to.
*
* @throws XMLStreamException
* if an error occurs creating or accessing the stream.
* @throws StorageException
* if a Storage service error occurs.
*/
private static void writeSingleAtomEntity(final StringWriter strWriter, final TableEntity entity,
final boolean isTableEntry, final OperationContext opContext) throws XMLStreamException, StorageException {
final XMLOutputFactory xmlOutFactoryInst = XMLOutputFactory.newInstance();
XMLStreamWriter xmlw = xmlOutFactoryInst.createXMLStreamWriter(strWriter);
// default is UTF8
xmlw.writeStartDocument(Constants.UTF8_CHARSET, "1.0");
writeAtomEntity(entity, isTableEntry, xmlw, opContext);
// end doc
xmlw.writeEndDocument();
xmlw.flush();
}
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) throws JsonParseException, IOException {
EdmType edmType = null;
if (token == JsonToken.VALUE_NULL) {
edmType = EdmType.NULL;
}
else 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 void writeJsonProperty(JsonGenerator generator, Entry prop)
throws JsonGenerationException, IOException {
EdmType edmType = prop.getValue().getEdmType();
if (prop.getValue().getIsNull()) {
generator.writeNullField(prop.getKey());
}
else if (edmType == EdmType.BOOLEAN) {
generator.writeBooleanField(prop.getKey(), prop.getValue().getValueAsBoolean());
}
else if (edmType == EdmType.DOUBLE) {
generator.writeNumberField(prop.getKey(), prop.getValue().getValueAsDouble());
}
else if (edmType == EdmType.INT32) {
generator.writeNumberField(prop.getKey(), prop.getValue().getValueAsInteger());
}
else {
generator.writeStringField(prop.getKey(), prop.getValue().getValueAsString());
}
}
private static JsonParser createJsonParserFromStream(final InputStream streamRef) throws JsonParseException,
IOException {
JsonParser parser = jsonFactory.createParser(streamRef);
// allows handling of infinity, -infinity, and NaN for Doubles
return parser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);
}
}