diff --git a/microsoft-azure-api/pom.xml b/microsoft-azure-api/pom.xml index c02cccea05a9a..d9b0c4c4f2cf0 100644 --- a/microsoft-azure-api/pom.xml +++ b/microsoft-azure-api/pom.xml @@ -80,6 +80,16 @@ commons-logging 1.1.1 + + javax.mail + mail + 1.4 + + + org.apache.commons + commons-lang3 + 3.1 + diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/AccessCondition.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/AccessCondition.java index 970cc3850ead5..c2a741ef3ed2d 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/AccessCondition.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/AccessCondition.java @@ -40,9 +40,8 @@ public static AccessCondition generateEmptyCondition() { * Returns an access condition such that an operation will be performed only if the resource's ETag value matches * the specified ETag value. *

- * Setting this access condition modifies the request to include the HTTP If-Match conditional header. If - * this access condition is set, the operation is performed only if the ETag of the resource matches the specified - * ETag. + * Setting this access condition modifies the request to include the HTTP If-Match conditional header. If this + * access condition is set, the operation is performed only if the ETag of the resource matches the specified ETag. *

* For more information, see Specifying * Conditional Headers for Blob Service Operations. @@ -84,8 +83,8 @@ public static AccessCondition generateIfModifiedSinceCondition(final Date lastMo * Returns an access condition such that an operation will be performed only if the resource's ETag value does not * match the specified ETag value. *

- * Setting this access condition modifies the request to include the HTTP If-None-Match conditional header. - * If this access condition is set, the operation is performed only if the ETag of the resource does not match the + * Setting this access condition modifies the request to include the HTTP If-None-Match conditional header. If + * this access condition is set, the operation is performed only if the ETag of the resource does not match the * specified ETag. *

* For more information, see Specifying diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java index 8818215a944ee..8beb56163e4cc 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java @@ -24,6 +24,7 @@ import com.microsoft.windowsazure.services.blob.client.CloudBlobClient; import com.microsoft.windowsazure.services.core.storage.utils.Utility; import com.microsoft.windowsazure.services.queue.client.CloudQueueClient; +import com.microsoft.windowsazure.services.table.client.CloudTableClient; /** * Represents a Windows Azure storage account. @@ -538,6 +539,26 @@ public CloudQueueClient createCloudQueueClient() { return new CloudQueueClient(this.getQueueEndpoint(), this.getCredentials()); } + /** + * Creates a new table service client. + * + * @return A client object that uses the Table service endpoint. + */ + public CloudTableClient createCloudTableClient() { + if (this.getTableEndpoint() == null) { + throw new IllegalArgumentException("No table endpoint configured."); + } + + if (this.credentials == null) { + throw new IllegalArgumentException("No credentials provided."); + } + + if (!this.credentials.canCredentialsSignRequest()) { + throw new IllegalArgumentException("CloudTableClient requires a credential that can sign request"); + } + return new CloudTableClient(this.getTableEndpoint(), this.getCredentials()); + } + /** * Returns the endpoint for the Blob service, as configured for the storage account. This method is not supported * when using shared access signature credentials. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java index 492320796e708..7676efe058daa 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java @@ -287,7 +287,7 @@ public static class HeaderConstants { /** * Specifies the value to use for UserAgent header. */ - public static final String USER_AGENT_VERSION = "Client v0.1.1"; + public static final String USER_AGENT_VERSION = "Client v0.1.2"; } /** diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java index debb82c4c6a89..39d2c8b737068 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java @@ -39,9 +39,9 @@ public class ResultSegment { private final int pageSize; /** - * Holds the Iterable collection of results. + * Holds the ArrayList of results. */ - private final Iterable results; + private final ArrayList results; /** * Reserved for internal use. Creates an instance of the ResultSegment class. @@ -115,11 +115,11 @@ public int getRemainingPageResults() { } /** - * Returns an enumerable set of results from the blob service. + * Returns an enumerable set of results from the service. * - * @return The results retrieved from the blob service. + * @return The results retrieved from the service. */ - public Iterable getResults() { + public ArrayList getResults() { return this.results; } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java index e9d0b3f6ad373..baa4c1db04659 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java @@ -242,13 +242,13 @@ public void setRetryPolicyFactory(final RetryPolicyFactory retryPolicyFactory) { /** * Sets the timeout to use when making requests to the storage service. *

- * The server timeout interval begins at the time that the complete request has been received by the service, and - * the server begins processing the response. If the timeout interval elapses before the response is returned to the + * The server timeout interval begins at the time that the complete request has been received by the service, and the + * server begins processing the response. If the timeout interval elapses before the response is returned to the * client, the operation times out. The timeout interval resets with each retry, if the request is retried. * - * The default timeout interval for a request made via the service client is 90 seconds. You can change this value - * on the service client by setting this property, so that all subsequent requests made via the service client will - * use the new timeout interval. You can also change this value for an individual request, by setting the + * The default timeout interval for a request made via the service client is 90 seconds. You can change this value on + * the service client by setting this property, so that all subsequent requests made via the service client will use + * the new timeout interval. You can also change this value for an individual request, by setting the * {@link RequestOptions#timeoutIntervalInMs} property. * * If you are downloading a large blob, you should increase the value of the timeout beyond the default value. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java index 84d4c055976ac..84b4c194ac8fb 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java @@ -39,8 +39,8 @@ public abstract class StorageCredentials { * Either include an account name with an account key (specifying values for * {@link CloudStorageAccount#ACCOUNT_NAME_NAME} and {@link CloudStorageAccount#ACCOUNT_KEY_NAME} ), or a * shared access signature (specifying a value for - * {@link CloudStorageAccount#SHARED_ACCESS_SIGNATURE_NAME} ). If you use an account name and account - * key, do not include a shared access signature, and vice versa. + * {@link CloudStorageAccount#SHARED_ACCESS_SIGNATURE_NAME} ). If you use an account name and account key, + * do not include a shared access signature, and vice versa. * * @return A {@link StorageCredentials} object representing the storage credentials determined from the name/value * pairs. @@ -81,8 +81,8 @@ protected static StorageCredentials tryParseCredentials(final HashMapString that contains the key/value pairs that represent the storage credentials. *

- * The format for the connection string is in the pattern "keyname=value". Multiple key/value - * pairs can be separated by a semi-colon, for example, "keyname1=value1;keyname2=value2". + * The format for the connection string is in the pattern "keyname=value". Multiple key/value pairs + * can be separated by a semi-colon, for example, "keyname1=value1;keyname2=value2". * * @return A {@link StorageCredentials} object representing the storage credentials determined from the connection * string. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java index c33ce78d64590..2f3e44a380ed1 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java @@ -96,8 +96,8 @@ public String computeHmac256(final String value) { /** * Computes a signature for the specified string using the HMAC-SHA256 algorithm with the specified operation - * context. This is not a valid operation for objects of type StorageCredentialsSharedAccessSignature - * so the method merely returns null. + * context. This is not a valid operation for objects of type StorageCredentialsSharedAccessSignature so + * the method merely returns null. * * @param value * The UTF-8-encoded string to sign. @@ -130,8 +130,8 @@ public String computeHmac512(final String value) { /** * Computes a signature for the specified string using the HMAC-SHA512 algorithm with the specified operation - * context. This is not a valid operation for objects of type StorageCredentialsSharedAccessSignature - * so the method merely returns null. + * context. This is not a valid operation for objects of type StorageCredentialsSharedAccessSignature so + * the method merely returns null. * * @param value * The UTF-8-encoded string to sign. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java index 5e37420554264..624a31467311d 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java @@ -188,6 +188,11 @@ public final class StorageErrorCodeStrings { */ public static final String SERVER_BUSY = "ServerBusy"; + /** + * Table Already Exists + */ + public static final String TABLE_ALREADY_EXISTS = "TableAlreadyExists"; + /** * One or more header values are not supported. */ diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java index 02e3554479058..e30d5f1e45440 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.microsoft.windowsazure.services.core.storage.utils.implementation; import java.io.IOException; @@ -34,6 +35,7 @@ import com.microsoft.windowsazure.services.core.storage.RetryResult; import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.table.client.TableServiceException; /** * RESERVED FOR INTERNAL USE. A class that handles execution of StorageOperations and enforces retry policies. @@ -166,6 +168,17 @@ public static RESULT_TYPE executeWithRet setLastException(opContext, translatedException); throw translatedException; } + catch (final TableServiceException e) { + task.getResult().setStatusCode(e.getHttpStatusCode()); + task.getResult().setStatusMessage(e.getMessage()); + setLastException(opContext, e); + if (!e.isRetryable()) { + throw e; + } + else { + translatedException = e; + } + } catch (final StorageException e) { // Non Retryable, just throw // do not translate StorageException diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java index 049bf75757fd5..90a2713c57935 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.microsoft.windowsazure.services.core.storage.utils.implementation; import java.io.InputStream; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java new file mode 100644 index 0000000000000..4b462cb334137 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java @@ -0,0 +1,562 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.InputStream; +import java.io.OutputStream; +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.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.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 AtomPubParser { + /** + * Reserved for internal use. A static factory method to construct an XMLStreamWriter instance based on + * the specified OutputStream. + * + * @param outStream + * The OutputStream instance to create an XMLStreamWriter on. + * @return + * An XMLStreamWriter instance based on the specified OutputStream. + * @throws XMLStreamException + * if an error occurs while creating the stream. + */ + protected static XMLStreamWriter generateTableWriter(final OutputStream outStream) throws XMLStreamException { + final XMLOutputFactory xmlOutFactoryInst = XMLOutputFactory.newInstance(); + return xmlOutFactoryInst.createXMLStreamWriter(outStream, "UTF-8"); + } + + /** + * 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. + */ + protected static TableResult parseEntity(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); + + res.setEtag(StringEscapeUtils.unescapeHtml4(xmlr.getAttributeValue(ODataConstants.DATA_SERVICES_METADATA_NS, + ODataConstants.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)) { + res.setId(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(readProperties(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().get(TableConstants.PARTITION_KEY); + if (tempProp != null) { + res.getProperties().remove(TableConstants.PARTITION_KEY); + partitionKey = tempProp.getValueAsString(); + } + + tempProp = res.getProperties().get(TableConstants.ROW_KEY); + if (tempProp != null) { + res.getProperties().remove(TableConstants.ROW_KEY); + rowKey = tempProp.getValueAsString(); + } + + tempProp = res.getProperties().get(TableConstants.TIMESTAMP); + if (tempProp != null) { + res.getProperties().remove(TableConstants.TIMESTAMP); + 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") + protected static ODataPayload parseResponse(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 = parseEntity(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. + */ + protected static TableResult parseSingleOpResponse(final XMLStreamReader xmlr, + final int httpStatusCode, final Class clazzType, final EntityResolver resolver, + final OperationContext opContext) throws XMLStreamException, ParseException, InstantiationException, + IllegalAccessException, StorageException { + xmlr.require(XMLStreamConstants.START_DOCUMENT, null, null); + xmlr.next(); + + final TableResult res = parseEntity(xmlr, clazzType, resolver, opContext); + res.setHttpStatusCode(httpStatusCode); + return res; + } + + /** + * 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. + */ + protected static HashMap readProperties(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. + */ + protected static void writeEntityToStream(final TableEntity entity, final boolean isTableEntry, + final XMLStreamWriter xmlw, final OperationContext opContext) throws XMLStreamException, StorageException { + final HashMap properties = entity.writeEntity(opContext); + if (properties == null) { + throw new IllegalArgumentException("Entity did not produce properties to serialize"); + } + + if (!isTableEntry) { + Utility.assertNotNullOrEmpty(TableConstants.PARTITION_KEY, entity.getPartitionKey()); + Utility.assertNotNullOrEmpty(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 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 outStream + * The OutputStream 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 creating or accessing the stream. + * @throws StorageException + * if a Storage service error occurs. + */ + protected static void writeSingleEntityToStream(final TableEntity entity, final boolean isTableEntry, + final OutputStream outStream, final OperationContext opContext) throws XMLStreamException, StorageException { + final XMLStreamWriter xmlw = AtomPubParser.generateTableWriter(outStream); + writeSingleEntityToStream(entity, isTableEntry, xmlw, opContext); + } + + /** + * Reserved for internal use. Writes a single entity to the specified XMLStreamWriter 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 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 creating or accessing the stream. + * @throws StorageException + * if a Storage service error occurs. + */ + protected static void writeSingleEntityToStream(final TableEntity entity, final boolean isTableEntry, + final XMLStreamWriter xmlw, final OperationContext opContext) throws XMLStreamException, StorageException { + // default is UTF8 + xmlw.writeStartDocument("UTF-8", "1.0"); + + writeEntityToStream(entity, isTableEntry, xmlw, opContext); + + // end doc + xmlw.writeEndDocument(); + xmlw.flush(); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/CloudTableClient.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/CloudTableClient.java new file mode 100644 index 0000000000000..b471594261c68 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/CloudTableClient.java @@ -0,0 +1,1359 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.InvalidKeyException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; + +import javax.xml.stream.XMLStreamException; + +import com.microsoft.windowsazure.services.core.storage.DoesServiceRequest; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.ResultContinuation; +import com.microsoft.windowsazure.services.core.storage.ResultContinuationType; +import com.microsoft.windowsazure.services.core.storage.ResultSegment; +import com.microsoft.windowsazure.services.core.storage.ServiceClient; +import com.microsoft.windowsazure.services.core.storage.StorageCredentials; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.LazySegmentedIterator; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.SegmentedStorageOperation; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * Provides a service client for accessing the Windows Azure Table service. + *

+ * The {@link CloudTableClient} class encapsulates the base URI for the Table service endpoint and the credentials for + * accessing the storage account, and provides methods to create, delete, list, and query tables, as well as methods to + * execute operations and queries on table entities. These methods invoke Storage Service REST API operations to make + * the requests and obtain the results that are returned. + *

+ * A Table service endpoint is the base URI for Table service resources, including the DNS name of the storage account: + *
+ *     http://myaccount.table.core.windows.net
+ * For more information, see the MSDN topic Addressing Table Service Resources. + *

+ * The credentials can be a combination of the storage account name and a key, or a shared access signature. For more + * information, see the MSDN topic Authenticating Access to Your Storage + * Account. + * + */ +public final class CloudTableClient extends ServiceClient { + + /** + * Reserved for internal use. An {@link EntityResolver} that projects table entity data as a String + * containing the table name. + */ + private final EntityResolver tableNameResolver = new EntityResolver() { + @Override + public String resolve(String partitionKey, String rowKey, Date timeStamp, + HashMap properties, String etag) { + return properties.get(TableConstants.TABLE_NAME).getValueAsString(); + } + }; + + /** + * Initializes an instance of the {@link CloudTableClient} class using a Table service endpoint. + *

+ * A {@link CloudTableClient} initialized with this constructor must have storage account credentials added before + * it can be used to access the Windows Azure storage service. + * + * @param baseUri + * A java.net.URI that represents the Table service endpoint used to initialize the + * client. + */ + public CloudTableClient(final URI baseUri) { + this(baseUri, null); + this.setTimeoutInMs(TableConstants.TABLE_DEFAULT_TIMEOUT_IN_MS); + } + + /** + * Initializes an instance of the {@link CloudTableClient} class using a Table service endpoint and + * storage account credentials. + * + * @param baseUri + * A java.net.URI object that represents the Table service endpoint used to initialize the + * client. + * @param credentials + * A {@link StorageCredentials} object that represents the storage account credentials for access. + */ + public CloudTableClient(final URI baseUri, StorageCredentials credentials) { + super(baseUri, credentials); + this.setTimeoutInMs(TableConstants.TABLE_DEFAULT_TIMEOUT_IN_MS); + } + + /** + * Creates a table with the specified name in the storage service. + *

+ * This method invokes the Create + * Table REST API to create the specified table, using the Table service endpoint and storage account + * credentials of this instance. + * + * @param tableName + * A String object containing the name of the table to create. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table cannot be + * created, or already exists. + */ + @DoesServiceRequest + public void createTable(final String tableName) throws StorageException { + this.createTable(tableName, null, null); + } + + /** + * Creates a table with the specified name in the storage service, using the specified {@link TableRequestOptions} + * and {@link OperationContext}. + *

+ * This method invokes the Create + * Table REST API to create the specified table, using the Table service endpoint and storage account + * credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param tableName + * A String object containing the name of the table to create. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table cannot be + * created, or already exists. + */ + @DoesServiceRequest + public void createTable(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(this); + + Utility.assertNotNullOrEmpty("tableName", tableName); + + final DynamicTableEntity tableEntry = new DynamicTableEntity(); + tableEntry.getProperties().put(TableConstants.TABLE_NAME, new EntityProperty(tableName)); + + this.execute(TableConstants.TABLES_SERVICE_TABLES_NAME, TableOperation.insert(tableEntry), options, opContext); + } + + /** + * Creates a table with the specified name in the storage service, if it does not already exist. + *

+ * This method first invokes the Query + * Tables REST API to determine if the table exists, and if not, invokes the Create Table Storage Service REST + * API to create the specified table, using the Table service endpoint and storage account credentials of this + * instance. + * + * @param tableName + * A String object containing the name of the table to create. + * + * @return + * A value of true if the operation created a new table, otherwise false. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table does not + * exist and cannot be created. + */ + @DoesServiceRequest + public boolean createTableIfNotExists(final String tableName) throws StorageException { + return this.createTableIfNotExists(tableName, null, null); + } + + /** + * Creates a table with the specified name in the storage service if it does not already exist, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method first invokes the Query + * Tables REST API to determine if the table exists, and if not, invokes the Create Table Storage Service REST + * API to create the specified table, using the Table service endpoint and storage account credentials of this + * instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param tableName + * A String object containing the name of the table to create. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A value of true if the operation created a new table, otherwise false. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table does not + * exist and cannot be created. + */ + @DoesServiceRequest + public boolean createTableIfNotExists(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(this); + + Utility.assertNotNullOrEmpty("tableName", tableName); + + if (this.doesTableExist(tableName, options, opContext)) { + return false; + } + else { + try { + this.createTable(tableName, options, opContext); + } + catch (StorageException ex) { + if (ex.getHttpStatusCode() == HttpURLConnection.HTTP_CONFLICT + && StorageErrorCodeStrings.TABLE_ALREADY_EXISTS.equals(ex.getErrorCode())) { + return false; + } + else { + throw ex; + } + } + return true; + } + } + + /** + * Deletes the table with the specified name in the storage service. + *

+ * This method invokes the Delete + * Table REST API to delete the specified table and any data it contains, using the Table service endpoint and + * storage account credentials of this instance. + * + * @param tableName + * A String object containing the name of the table to delete. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table deletion operation failed. + */ + @DoesServiceRequest + public void deleteTable(final String tableName) throws StorageException { + this.deleteTable(tableName, null, null); + } + + /** + * Deletes the table with the specified name in the storage service, using the specified {@link TableRequestOptions} + * and {@link OperationContext}. + *

+ * This method invokes the Delete + * Table REST API to delete the specified table and any data it contains, using the Table service endpoint and + * storage account credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param tableName + * A String object containing the name of the table to delete. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table deletion operation failed. + */ + @DoesServiceRequest + public void deleteTable(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(this); + + Utility.assertNotNullOrEmpty("tableName", tableName); + final DynamicTableEntity tableEntry = new DynamicTableEntity(); + tableEntry.getProperties().put(TableConstants.TABLE_NAME, new EntityProperty(tableName)); + + final TableOperation delOp = new TableOperation(tableEntry, TableOperationType.DELETE); + + final TableResult result = this.execute(TableConstants.TABLES_SERVICE_TABLES_NAME, delOp, options, opContext); + + if (result.getHttpStatusCode() == HttpURLConnection.HTTP_NO_CONTENT) { + return; + } + else { + throw new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, + "Unexpected http status code received.", result.getHttpStatusCode(), null, null); + } + } + + /** + * Deletes the table with the specified name in the storage service, if it exists. + *

+ * This method first invokes the Query + * Tables REST API to determine if the table exists, and if so, invokes the Delete Table Storage Service REST + * API to delete the table and any data it contains, using the Table service endpoint and storage account + * credentials of this instance. + * + * @param tableName + * A String object containing the name of the table to delete. + * + * @return + * A value of true if the operation deleted an existing table, otherwise false. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table deletion operation failed. + */ + @DoesServiceRequest + public boolean deleteTableIfExists(final String tableName) throws StorageException { + return this.deleteTableIfExists(tableName, null, null); + } + + /** + * Deletes the table with the specified name in the storage service, if it exists, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method first invokes the Query + * Tables REST API to determine if the table exists, and if so, invokes the Delete Table Storage Service REST + * API to delete the table and any data it contains, using the Table service endpoint and storage account + * credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param tableName + * A String object containing the name of the table to delete. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A value of true if the operation deleted an existing table, otherwise false. + * + * @throws StorageException + * if an error occurs accessing the storage service, or because the table deletion operation failed. + */ + @DoesServiceRequest + public boolean deleteTableIfExists(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(this); + + Utility.assertNotNullOrEmpty("tableName", tableName); + + if (this.doesTableExist(tableName, options, opContext)) { + try { + this.deleteTable(tableName, options, opContext); + } + catch (StorageException ex) { + if (ex.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND + && StorageErrorCodeStrings.RESOURCE_NOT_FOUND.equals(ex.getErrorCode())) { + return false; + } + else { + throw ex; + } + } + return true; + } + else { + return false; + } + } + + /** + * Determines if a table with the specified name exists in the storage service. + *

+ * This method invokes the Query + * Tables REST API to determine if the table exists, using the Table service endpoint and storage account + * credentials of this instance. + * + * @param tableName + * A String object containing the name of the table to find. + * + * @return + * A value of true if the table exists, otherwise false. + * + * @throws StorageException + * if an error occurs accessing the storage service. + */ + @DoesServiceRequest + public boolean doesTableExist(final String tableName) throws StorageException { + return this.doesTableExist(tableName, null, null); + } + + /** + * Determines if a table with the specified name exists in the storage service, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method invokes the Query + * Tables REST API to determine if the table exists, using the Table service endpoint and storage account + * credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param tableName + * A String object containing the name of the table to find. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A value of true if the table exists, otherwise false. + * + * @throws StorageException + * if an error occurs accessing the storage service. + */ + @DoesServiceRequest + public boolean doesTableExist(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(this); + + Utility.assertNotNullOrEmpty("tableName", tableName); + + final TableResult result = this.execute(TableConstants.TABLES_SERVICE_TABLES_NAME, + TableOperation.retrieve(tableName /* Used As PK */, null/* Row Key */, DynamicTableEntity.class), + options, opContext); + + if (result.getHttpStatusCode() == HttpURLConnection.HTTP_OK) { + return true; + } + else if (result.getHttpStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { + return false; + } + else { + throw new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, + "Unexpected http status code received.", result.getHttpStatusCode(), null, null); + } + } + + /** + * Executes the specified batch operation on a table as an atomic operation. A batch operation may contain up to 100 + * individual table operations, with the requirement that each operation entity must have same partition key. Only + * one retrieve operation is allowed per batch. Note that the total payload of a batch operation is limited to 4MB. + *

+ * This method invokes an Entity Group + * Transaction on the REST API to execute the specified batch operation on the table as an atomic unit, using + * the Table service endpoint and storage account credentials of this instance. + * + * @param tableName + * A String containing the name of the table to execute the operations on. + * @param batch + * The {@link TableBatchOperation} object representing the operations to execute on the table. + * + * @return + * A java.util.ArrayList of {@link TableResult} that contains the results, in order, of + * each {@link TableOperation} in the {@link TableBatchOperation} on the named table. + * + * @throws StorageException + * if an error occurs accessing the storage service, or the operation fails. + */ + @DoesServiceRequest + public ArrayList execute(final String tableName, final TableBatchOperation batch) + throws StorageException { + return this.execute(tableName, batch, null, null); + } + + /** + * Executes the specified batch operation on a table as an atomic operation, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. A batch operation may contain up to 100 individual + * table operations, with the requirement that each operation entity must have same partition key. Only one retrieve + * operation is allowed per batch. Note that the total payload of a batch operation is limited to 4MB. + *

+ * This method invokes an Entity Group + * Transaction on the REST API to execute the specified batch operation on the table as an atomic unit, using + * the Table service endpoint and storage account credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param tableName + * A String containing the name of the table to execute the operations on. + * @param batch + * The {@link TableBatchOperation} object representing the operations to execute on the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A java.util.ArrayList of {@link TableResult} that contains the results, in order, of + * each {@link TableOperation} in the {@link TableBatchOperation} on the named table. + * + * @throws StorageException + * if an error occurs accessing the storage service, or the operation fails. + */ + @DoesServiceRequest + public ArrayList execute(final String tableName, final TableBatchOperation batch, + TableRequestOptions options, OperationContext opContext) throws StorageException { + Utility.assertNotNull("batch", batch); + if (opContext == null) { + opContext = new OperationContext(); + } + + if (options == null) { + options = new TableRequestOptions(); + } + + opContext.initialize(); + options.applyDefaults(this); + return batch.execute(this, tableName, options, opContext); + } + + /** + * Executes the operation on a table. + *

+ * This method will invoke the Table + * Service REST API to execute the specified operation on the table, using the Table service endpoint and + * storage account credentials of this instance. + * + * @param tableName + * A String containing the name of the table to execute the operation on. + * @param operation + * The {@link TableOperation} object representing the operation to execute on the table. + * + * @return + * A {@link TableResult} containing the result of executing the {@link TableOperation} on the table. + * + * @throws StorageException + * if an error occurs accessing the storage service, or the operation fails. + */ + @DoesServiceRequest + public TableResult execute(final String tableName, final TableOperation operation) throws StorageException { + return this.execute(tableName, operation, null, null); + } + + /** + * Executes the operation on a table, using the specified {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method will invoke the Table + * Service REST API to execute the specified operation on the table, using the Table service endpoint and + * storage account credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param tableName + * A String containing the name of the table to execute the operation on. + * @param operation + * The {@link TableOperation} object representing the operation to execute on the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A {@link TableResult} containing the result of executing the {@link TableOperation} on the table. + * + * @throws StorageException + * if an error occurs accessing the storage service, or the operation fails. + */ + @DoesServiceRequest + public TableResult execute(final String tableName, final TableOperation operation, + final TableRequestOptions options, final OperationContext opContext) throws StorageException { + Utility.assertNotNull("operation", operation); + return operation.execute(this, tableName, options, opContext); + } + + /** + * Executes a query, applying the specified {@link EntityResolver} to the result. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use. + * @param resolver + * An {@link EntityResolver} instance which creates a projection of the table query result entities into + * the specified type R. + * + * @return + * A collection implementing the Iterable interface containing the projection into type + * R of the results of executing the query. + */ + @DoesServiceRequest + public Iterable execute(final TableQuery query, final EntityResolver resolver) { + return this.execute(query, resolver, null, null); + } + + /** + * Executes a query, applying the specified {@link EntityResolver} to the result, using the + * specified {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use. + * @param resolver + * An {@link EntityResolver} instance which creates a projection of the table query result entities into + * the specified type R. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A collection implementing the Iterable interface containing the projection into type + * R of the results of executing the query. + */ + @DoesServiceRequest + @SuppressWarnings("unchecked") + public Iterable execute(final TableQuery query, final EntityResolver resolver, + final TableRequestOptions options, final OperationContext opContext) { + Utility.assertNotNull("query", query); + Utility.assertNotNull("Query requires a valid class type or resolver.", resolver); + return (Iterable) this.generateIteratorForQuery(query, resolver, options, opContext); + } + + /** + * Executes a query. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use, + * specialized for a type T implementing {@link TableEntity}. + * + * @return + * A collection implementing the Iterable interface specialized for type T of the results of + * executing the query. + */ + @DoesServiceRequest + public Iterable execute(final TableQuery query) { + return this.execute(query, null, null); + } + + /** + * Executes a query, using the specified {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use, + * specialized for a type T implementing {@link TableEntity}. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A collection implementing the Iterable interface specialized for type T of the results of + * executing the query. + */ + @SuppressWarnings("unchecked") + @DoesServiceRequest + public Iterable execute(final TableQuery query, final TableRequestOptions options, + final OperationContext opContext) { + Utility.assertNotNull("query", query); + return (Iterable) this.generateIteratorForQuery(query, null, options, opContext); + } + + /** + * Executes a query in segmented mode with the specified {@link ResultContinuation} continuation token, + * applying the {@link EntityResolver} to the result. + * Executing a query with executeSegmented allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use. + * @param resolver + * An {@link EntityResolver} instance which creates a projection of the table query result entities into + * the specified type R. + * @param continuationToken + * A {@link ResultContinuation} object representing a continuation token from the server when the + * operation returns a partial result. Specify null on the initial call. Call the + * {@link ResultSegment#getContinuationToken()} method on the result to obtain the + * {@link ResultContinuation} object to use in the next call to resume the query. + * + * @return + * A {@link ResultSegment} containing the projection into type R of the results of executing + * the query. + * + * @throws IOException + * if an IO error occurred during the operation. + * @throws URISyntaxException + * if the URI generated for the query is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + @DoesServiceRequest + public ResultSegment executeSegmented(final TableQuery query, final EntityResolver resolver, + final ResultContinuation continuationToken) throws IOException, URISyntaxException, StorageException { + return this.executeSegmented(query, resolver, continuationToken, null, null); + } + + /** + * Executes a query in segmented mode with the specified {@link ResultContinuation} continuation token, + * using the specified {@link TableRequestOptions} and {@link OperationContext}, applying the {@link EntityResolver} + * to the result. + * Executing a query with executeSegmented allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use. + * @param resolver + * An {@link EntityResolver} instance which creates a projection of the table query result entities into + * the specified type R. + * @param continuationToken + * A {@link ResultContinuation} object representing a continuation token from the server when the + * operation returns a partial result. Specify null on the initial call. Call the + * {@link ResultSegment#getContinuationToken()} method on the result to obtain the + * {@link ResultContinuation} object to use in the next call to resume the query. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A {@link ResultSegment} containing the projection into type R of the results of executing + * the query. + * + * @throws IOException + * if an IO error occurred during the operation. + * @throws URISyntaxException + * if the URI generated for the query is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + @DoesServiceRequest + @SuppressWarnings("unchecked") + public ResultSegment executeSegmented(final TableQuery query, final EntityResolver resolver, + final ResultContinuation continuationToken, final TableRequestOptions options, + final OperationContext opContext) throws IOException, URISyntaxException, StorageException { + Utility.assertNotNull("Query requires a valid class type or resolver.", resolver); + return (ResultSegment) this + .executeQuerySegmentedImpl(query, resolver, continuationToken, options, opContext); + } + + /** + * Executes a query in segmented mode with a {@link ResultContinuation} continuation token. + * Executing a query with executeSegmented allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use, + * specialized for a type T implementing {@link TableEntity}. + * @param continuationToken + * A {@link ResultContinuation} object representing a continuation token from the server when the + * operation returns a partial result. Specify null on the initial call. Call the + * {@link ResultSegment#getContinuationToken()} method on the result to obtain the + * {@link ResultContinuation} object to use in the next call to resume the query. + * + * @return + * A {@link ResultSegment} specialized for type T of the results of executing the query. + * + * @throws IOException + * if an IO error occurred during the operation. + * @throws URISyntaxException + * if the URI generated for the query is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + @DoesServiceRequest + public ResultSegment executeSegmented(final TableQuery query, + final ResultContinuation continuationToken) throws IOException, URISyntaxException, StorageException { + return this.executeSegmented(query, continuationToken, null, null); + } + + /** + * Executes a query in segmented mode with a {@link ResultContinuation} continuation token, + * using the specified {@link TableRequestOptions} and {@link OperationContext}. + * Executing a query with executeSegmented allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + *

+ * This method will invoke a Query + * Entities operation on the Table + * Service REST API to query the table, using the Table service endpoint and storage account credentials of this + * instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param query + * A {@link TableQuery} instance specifying the table to query and the query parameters to use, + * specialized for a type T implementing {@link TableEntity}. + * @param continuationToken + * A {@link ResultContinuation} object representing a continuation token from the server when the + * operation returns a partial result. Specify null on the initial call. Call the + * {@link ResultSegment#getContinuationToken()} method on the result to obtain the + * {@link ResultContinuation} object to use in the next call to resume the query. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A {@link ResultSegment} specialized for type T of the results of executing the query. + * + * @throws IOException + * if an IO error occurred during the operation. + * @throws URISyntaxException + * if the URI generated for the query is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + @DoesServiceRequest + @SuppressWarnings("unchecked") + public ResultSegment executeSegmented(final TableQuery query, + final ResultContinuation continuationToken, final TableRequestOptions options, + final OperationContext opContext) throws IOException, URISyntaxException, StorageException { + Utility.assertNotNull("query", query); + return (ResultSegment) this.executeQuerySegmentedImpl(query, null, continuationToken, options, opContext); + } + + /** + * Lists the table names in the storage account. + *

+ * This method invokes the Query + * Tables REST API to list the table names, using the Table service endpoint and storage account credentials of + * this instance. + * + * @return + * An Iterable collection of the table names in the storage account. + */ + @DoesServiceRequest + public Iterable listTables() { + return this.listTables(null); + } + + /** + * Lists the table names in the storage account that match the specified prefix. + *

+ * This method invokes the Query + * Tables REST API to list the table names that match the prefix, using the Table service endpoint and storage + * account credentials of this instance. + * + * @param prefix + * A containing the prefix to match on table names to return. + * + * @return + * An Iterable collection of the table names in the storage account that match the specified + * prefix. + */ + @DoesServiceRequest + public Iterable listTables(final String prefix) { + return this.listTables(prefix, null, null); + } + + /** + * Lists the table names in the storage account that match the specified prefix, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method invokes the Query + * Tables REST API to list the table names that match the prefix, using the Table service endpoint and storage + * account credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param prefix + * A containing the prefix to match on table names to return. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * An Iterable collection of the table names in the storage account that match the specified + * prefix. + */ + @DoesServiceRequest + public Iterable listTables(final String prefix, final TableRequestOptions options, + final OperationContext opContext) { + return this.execute(this.generateListTablesQuery(prefix), this.tableNameResolver, options, opContext); + } + + /** + * Lists the table names in the storage account in segmented mode. This method allows listing of tables to be + * resumed after returning a partial set of results, using information returned by the server in the + * {@link ResultSegment} object. + *

+ * This method invokes the Query + * Tables REST API to list the table names, using the Table service endpoint and storage account credentials of + * this instance. + * + * @param prefix + * A containing the prefix to match on table names to return. + * + * @return + * A {@link ResultSegment} of String objects containing table names in the storage account. + * + * @throws IOException + * if an IO error occurred during the operation. + * @throws URISyntaxException + * if the URI generated for the operation is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + @DoesServiceRequest + public ResultSegment listTablesSegmented() throws IOException, URISyntaxException, StorageException { + return this.listTablesSegmented(null); + } + + /** + * Lists the table names in the storage account that match the specified prefix in segmented mode. This method + * allows listing of tables to be resumed after returning a partial set of results, using information returned by + * the server in the {@link ResultSegment} object. + *

+ * This method invokes the Query + * Tables REST API to list the table names that match the prefix, using the Table service endpoint and storage + * account credentials of this instance. + * + * @param prefix + * A containing the prefix to match on table names to return. + * + * @return + * A {@link ResultSegment} of String objects containing table names matching the prefix in the + * storage account. + * + * @throws IOException + * if an IO error occurred during the operation. + * @throws URISyntaxException + * if the URI generated for the operation is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + @DoesServiceRequest + public ResultSegment listTablesSegmented(final String prefix) throws IOException, URISyntaxException, + StorageException { + return this.listTablesSegmented(prefix, null, null, null, null); + } + + /** + * Lists up to the specified maximum of the table names in the storage account that match the specified prefix in a + * resumable mode with the specified {@link ResultContinuation} continuation token, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. This method allows listing of tables to be resumed + * after returning a page of results, using information returned by the server in the {@link ResultSegment} object. + *

+ * This method invokes the Query + * Tables REST API to list the table names that match the prefix, using the Table service endpoint and storage + * account credentials of this instance. + * + * Use the {@link TableRequestOptions} to override execution options such as the timeout or retry policy for the + * operation. + * + * @param prefix + * A containing the prefix to match on table names to return. + * @param maxResults + * The maximum number of table names to return in the {@link ResultSegment}. If this parameter is null, + * the query will list up to the maximum 1,000 results. + * @param continuationToken + * A {@link ResultContinuation} object representing a continuation token from the server when the + * operation returns a partial result. Specify null on the initial call. Call the + * {@link ResultSegment#getContinuationToken()} method on the result to obtain the + * {@link ResultContinuation} object to use in the next call to resume the query. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * A {@link ResultSegment} of String objects containing table names in the storage account. + * + * @throws IOException + * if an IO error occurred during the operation. + * @throws URISyntaxException + * if the URI generated for the operation is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + @DoesServiceRequest + public ResultSegment listTablesSegmented(final String prefix, final Integer maxResults, + final ResultContinuation continuationToken, final TableRequestOptions options, + final OperationContext opContext) throws IOException, URISyntaxException, StorageException { + return this.executeSegmented(this.generateListTablesQuery(prefix).take(maxResults), this.tableNameResolver, + continuationToken, options, opContext); + } + + /** + * Reserved for internal use. Generates a query to list table names with the given prefix. + * + * @param prefix + * A containing the prefix to match on table names to return. + * @return + * A {@link TableQuery} instance for listing table names with the specified prefix. + */ + private TableQuery generateListTablesQuery(final String prefix) { + TableQuery listQuery = TableQuery. from( + TableConstants.TABLES_SERVICE_TABLES_NAME, TableServiceEntity.class); + + if (!Utility.isNullOrEmpty(prefix)) { + // Append Max char to end '{' is 1 + 'z' in AsciiTable > uppperBound = prefix + '{' + final String prefixFilter = String.format("(%s ge '%s') and (%s lt '%s{')", TableConstants.TABLE_NAME, + prefix, TableConstants.TABLE_NAME, prefix); + + listQuery = listQuery.where(prefixFilter); + } + + return listQuery; + } + + /** + * Reserved for internal use. Implements the REST API call at the core of a segmented table query + * operation. + * + * @param queryToExecute + * The {@link TableQuery} to execute. + * @param resolver + * An {@link EntityResolver} instance to use to project the result entity as an instance of type + * R. Pass null to return the results as the table entity type. + * @param continuationToken + * The {@link ResultContinuation} to pass with the operation to resume a query, if any. Pass + * null for an initial query. + * @param taskReference + * A reference to the {@link StorageOperation} implementing the segmented operation. + * @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 ResultSegment} containing a collection of the query results specialized for the + * {@link TableEntity} or {@link EntityResolver} type returned by the query. + * @throws StorageException + * if a Storage service error occurs. + * @throws IOException + * if an IO error occurs. + * @throws URISyntaxException + * if the URI generated for the query is invalid. + * @throws XMLStreamException + * if an error occurs accessing the XMLStreamReader. + * @throws ParseException + * if an error occurs in parsing the response. + * @throws InstantiationException + * if an error occurs in object construction. + * @throws IllegalAccessException + * if an error occurs in reflection on an object type. + * @throws InvalidKeyException + * if the key for an entity is invalid. + */ + @SuppressWarnings("unchecked") + protected ResultSegment executeQuerySegmentedCore(final TableQuery queryToExecute, + final EntityResolver resolver, final ResultContinuation continuationToken, + final StorageOperation taskReference, final TableRequestOptions options, + final OperationContext opContext) throws StorageException, IOException, URISyntaxException, + XMLStreamException, ParseException, InstantiationException, IllegalAccessException, InvalidKeyException { + if (resolver == null) { + Utility.assertNotNull("Query requires a valid class type or resolver.", queryToExecute.getClazzType()); + } + + final HttpURLConnection queryRequest = TableRequest.query(this.getEndpoint(), + queryToExecute.getSourceTableName(), null/* identity */, options.getTimeoutIntervalInMs(), + queryToExecute.generateQueryBuilder(), continuationToken, options, opContext); + + this.getCredentials().signRequestLite(queryRequest, -1L, opContext); + + taskReference.setResult(ExecutionEngine.processRequest(queryRequest, opContext)); + + if (taskReference.getResult().getStatusCode() != HttpURLConnection.HTTP_OK) { + throw TableServiceException.generateTableServiceException(true, taskReference.getResult(), null, + queryRequest.getErrorStream()); + } + + ODataPayload clazzResponse = null; + ODataPayload resolvedResponse = null; + + InputStream inStream = queryRequest.getInputStream(); + + try { + if (resolver == null) { + clazzResponse = (ODataPayload) AtomPubParser.parseResponse(inStream, queryToExecute.getClazzType(), + null, opContext); + } + else { + resolvedResponse = (ODataPayload) AtomPubParser.parseResponse(inStream, + queryToExecute.getClazzType(), resolver, opContext); + } + } + finally { + inStream.close(); + } + + final ResultContinuation nextToken = TableResponse.getTableContinuationFromResponse(queryRequest); + + if (resolver == null) { + return new ResultSegment(clazzResponse.results, + queryToExecute.getTakeCount() == null ? clazzResponse.results.size() + : queryToExecute.getTakeCount(), nextToken); + } + else { + return new ResultSegment(resolvedResponse.results, + queryToExecute.getTakeCount() == null ? resolvedResponse.results.size() + : queryToExecute.getTakeCount(), nextToken); + } + } + + /** + * Reserved for internal use. Executes a segmented query operation using the specified retry and timeout policies. + * + * @param queryToExecute + * The {@link TableQuery} to execute. + * @param resolver + * An {@link EntityResolver} instance which creates a projection of the table query result entities into + * the specified type R. Pass null to return the results as the table entity + * type. + * @param continuationToken + * The {@link ResultContinuation} to pass with the operation to resume a query, if any. Pass + * null for an initial query. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * @return + * A {@link ResultSegment} containing a collection of the query results specialized for the + * {@link TableEntity} or {@link EntityResolver} type returned by the query. + * @throws StorageException + * if a Storage service error occurs. + */ + protected ResultSegment executeQuerySegmentedImpl(final TableQuery queryToExecute, + final EntityResolver resolver, final ResultContinuation continuationToken, TableRequestOptions options, + OperationContext opContext) throws StorageException { + if (opContext == null) { + opContext = new OperationContext(); + } + + if (options == null) { + options = new TableRequestOptions(); + } + + opContext.initialize(); + options.applyDefaults(this); + + Utility.assertContinuationType(continuationToken, ResultContinuationType.TABLE); + + final StorageOperation, ResultSegment> impl = new StorageOperation, ResultSegment>( + options) { + @Override + public ResultSegment execute(final CloudTableClient client, final TableQuery queryRef, + final OperationContext opContext) throws Exception { + + return CloudTableClient.this.executeQuerySegmentedCore(queryRef, resolver, continuationToken, this, + (TableRequestOptions) this.getRequestOptions(), opContext); + } + }; + return ExecutionEngine.executeWithRetry(this, queryToExecute, impl, options.getRetryPolicyFactory(), opContext); + } + + /** + * Reserved for internal use. Generates an iterator for a segmented query operation. + * + * @param queryRef + * The {@link TableQuery} to execute. + * @param resolver + * An {@link EntityResolver} instance which creates a projection of the table query result entities into + * the specified type R. Pass null to return the results as the table entity + * type. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * @return + * An instance of Iterable specialized for the {@link TableEntity} or {@link EntityResolver} + * type returned by the query. + */ + protected Iterable generateIteratorForQuery(final TableQuery queryRef, + final EntityResolver resolver, TableRequestOptions options, OperationContext opContext) { + if (opContext == null) { + opContext = new OperationContext(); + } + + if (options == null) { + options = new TableRequestOptions(); + } + + opContext.initialize(); + options.applyDefaults(this); + + if (resolver == null) { + final SegmentedStorageOperation, ResultSegment> impl = new SegmentedStorageOperation, ResultSegment>( + options, null) { + @Override + public ResultSegment execute(final CloudTableClient client, final TableQuery queryToExecute, + final OperationContext opContext) throws Exception { + + @SuppressWarnings("unchecked") + final ResultSegment result = (ResultSegment) CloudTableClient.this.executeQuerySegmentedCore( + queryToExecute, null, this.getToken(), this, + (TableRequestOptions) this.getRequestOptions(), opContext); + + // Note, setting the token on the SegmentedStorageOperation is + // key, this is how the iterator will share the token across executions + if (result != null) { + this.setToken(result.getContinuationToken()); + } + + return result; + } + }; + + return new LazySegmentedIterator, T>(impl, this, queryRef, + options.getRetryPolicyFactory(), opContext); + } + else { + final SegmentedStorageOperation, ResultSegment> impl = new SegmentedStorageOperation, ResultSegment>( + options, null) { + @Override + public ResultSegment execute(final CloudTableClient client, final TableQuery queryToExecute, + final OperationContext opContext) throws Exception { + + @SuppressWarnings("unchecked") + final ResultSegment result = (ResultSegment) CloudTableClient.this.executeQuerySegmentedCore( + queryToExecute, resolver, this.getToken(), this, + (TableRequestOptions) this.getRequestOptions(), opContext); + + // Note, setting the token on the SegmentedStorageOperation is + // key, this is how the iterator will share the token across executions + if (result != null) { + this.setToken(result.getContinuationToken()); + } + + return result; + } + }; + return new LazySegmentedIterator, R>(impl, this, queryRef, + options.getRetryPolicyFactory(), opContext); + } + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/DynamicTableEntity.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/DynamicTableEntity.java new file mode 100644 index 0000000000000..cfe58e086f67e --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/DynamicTableEntity.java @@ -0,0 +1,104 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.HashMap; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * A {@link TableEntity} type which allows callers direct access to the property map of the entity. This class extends + * {@link TableServiceEntity} to eliminate the use of reflection for serialization and deserialization. + * + */ +public class DynamicTableEntity extends TableServiceEntity { + private HashMap properties = new HashMap(); + + /** + * Nullary default constructor. + */ + public DynamicTableEntity() { + // empty ctor + } + + /** + * Constructs a {@link DynamicTableEntity} instance using the specified property map. + * + * @param properties + * A java.util.HashMap containing a map of String property names to + * {@link EntityProperty} data typed values to store in the new {@link DynamicTableEntity}. + */ + public DynamicTableEntity(final HashMap properties) { + this.setProperties(properties); + } + + /** + * Gets the property map for this {@link DynamicTableEntity} instance. + * + * @return + * A java.util.HashMap containing the map of String property names to + * {@link EntityProperty} data typed values for this {@link DynamicTableEntity} instance. + */ + public HashMap getProperties() { + return this.properties; + } + + /** + * Populates this {@link DynamicTableEntity} instance using the specified map of property names to + * {@link EntityProperty} data typed values. + * + * @param properties + * The java.util.HashMap of String property names to {@link EntityProperty} + * data + * typed values to store in this {@link DynamicTableEntity} instance. + * @param opContext + * An {@link OperationContext} object used to track the execution of the operation. + */ + @Override + public void readEntity(final HashMap properties, final OperationContext opContext) { + this.setProperties(properties); + } + + /** + * Sets the property map for this {@link DynamicTableEntity} instance. + * + * @param properties + * A java.util.HashMap containing the map of String property names to + * {@link EntityProperty} data typed values to set in this {@link DynamicTableEntity} instance. + */ + public void setProperties(final HashMap properties) { + this.properties = properties; + } + + /** + * Returns the map of property names to {@link EntityProperty} data values from this {@link DynamicTableEntity} + * instance. + * + * @param opContext + * An {@link OperationContext} object used to track the execution of the operation. + * + * @return + * A java.util.HashMap containing the map of String property names to + * {@link EntityProperty} data typed values stored in this {@link DynamicTableEntity} instance. + * @throws StorageException + * if a Storage service error occurs. + */ + @Override + public HashMap writeEntity(final OperationContext opContext) throws StorageException { + return this.getProperties(); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EdmType.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EdmType.java new file mode 100644 index 0000000000000..bf246941e4ce5 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EdmType.java @@ -0,0 +1,203 @@ +/** + * Copyright 2011 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.services.table.client; + +import com.microsoft.windowsazure.services.core.storage.Constants; + +/** + * A enumeration used to represent the primitive types of the Entity Data Model (EDM) in the Open Data Protocol (OData). + * The EDM is the underlying abstract data model used by OData services. The {@link EdmType} enumeration includes a + * {@link #parse(String)} method to convert EDM data type names to the enumeration type, and overrides the + * {@link #toString()} method to produce an EDM data type name. + *

+ * For more information about OData, see the Open Data Protocol website. + *

+ * For an overview of the available EDM primitive data types and names, see the Primitive Data Types section of the + * OData Protocol Overview. + *

+ * The Abstract Type System used to define the primitive types supported by OData is defined in detail in [MC-CSDL] (section 2.2.1). + */ +public enum EdmType { + /** + * Null Represents the absence of a value + */ + NULL, + + /** + * Edm.Binary Represents fixed- or variable-length binary data + */ + BINARY, + + /** + * Edm.Boolean Represents the mathematical concept of binary-valued logic + */ + BOOLEAN, + + /** + * Edm.Byte Represents a unsigned 8-bit integer value + */ + BYTE, + + /** + * Edm.DateTime Represents date and time with values ranging from 12:00:00 midnight, January 1, + * 1753 A.D. through 11:59:59 P.M, December 9999 A.D. + */ + DATE_TIME, + + /** + * Edm.Decimal Represents numeric values with fixed precision and scale. This type can describe a + * numeric value ranging from negative 10^255 + 1 to positive 10^255 -1 + */ + DECIMAL, + + /** + * Edm.Double Represents a floating point number with 15 digits precision that can represent values + * with approximate range of +/- 2.23e -308 through +/- 1.79e +308 + */ + DOUBLE, + + /** + * Edm.Single Represents a floating point number with 7 digits precision that can represent values + * with approximate range of +/- 1.18e -38 through +/- 3.40e +38 + */ + SINGLE, + + /** + * Edm.Guid Represents a 16-byte (128-bit) unique identifier value + */ + GUID, + + /** + * Edm.Int16 Represents a signed 16-bit integer value + */ + INT16, + + /** + * Edm.Int32 Represents a signed 32-bit integer value + */ + INT32, + + /** + * Edm.Int64 Represents a signed 64-bit integer value + */ + INT64, + + /** + * Edm.SByte Represents a signed 8-bit integer value + */ + SBYTE, + + /** + * Edm.String Represents fixed- or variable-length character data + */ + STRING, + + /** + * Edm.Time Represents the time of day with values ranging from 0:00:00.x to 23:59:59.y, where x + * and y depend upon the precision + */ + TIME, + + /** + * Edm.DateTimeOffset Represents date and time as an Offset in minutes from GMT, with values + * ranging from 12:00:00 midnight, January 1, 1753 A.D. through 11:59:59 P.M, December 9999 A.D + */ + DATE_TIME_OFFSET; + + /** + * Parses an EDM data type name and return the matching {@link EdmType} enumeration value. A null or + * empty value parameter is matched as {@link EdmType#STRING}. Note that only the subset of EDM data types + * supported in Windows Azure Table storage is parsed, consisting of {@link #BINARY}, {@link #BOOLEAN}, + * {@link #BYTE} , {@link #DATE_TIME}, {@link #DOUBLE}, {@link #GUID}, {@link #INT32}, {@link #INT64}, and + * {@link #STRING}. Any other type will cause an {@link IllegalArgumentException} to be thrown. + * + * @param value + * A String containing the name of an EDM data type. + * @return + * The {@link EdmType} enumeration value matching the specified EDM data type. + * @throws IllegalArgumentException + * if an EDM data type not supported in Windows Azure Table storage is passed as an argument. + * + */ + public static EdmType parse(final String value) { + if (value == null || value.length() == 0) { + return EdmType.STRING; + } + else if (value.equals(ODataConstants.EDMTYPE_DATETIME)) { + return EdmType.DATE_TIME; + } + else if (value.equals(ODataConstants.EDMTYPE_INT32)) { + return EdmType.INT32; + } + else if (value.equals(ODataConstants.EDMTYPE_BOOLEAN)) { + return EdmType.BOOLEAN; + } + else if (value.equals(ODataConstants.EDMTYPE_DOUBLE)) { + return EdmType.DOUBLE; + } + else if (value.equals(ODataConstants.EDMTYPE_INT64)) { + return EdmType.INT64; + } + else if (value.equals(ODataConstants.EDMTYPE_GUID)) { + return EdmType.GUID; + } + else if (value.equals(ODataConstants.EDMTYPE_BINARY)) { + return EdmType.BINARY; + } + + throw new IllegalArgumentException("Invalid value for edmtype: ".concat(value)); + } + + /** + * Returns the name of the EDM data type corresponding to the enumeration value. + * + * @return + * A String containing the name of the EDM data type. + */ + @Override + public String toString() { + if (this == EdmType.BINARY) { + return ODataConstants.EDMTYPE_BINARY; + } + else if (this == EdmType.STRING) { + return Constants.EMPTY_STRING; + } + else if (this == EdmType.BOOLEAN) { + return ODataConstants.EDMTYPE_BOOLEAN; + } + else if (this == EdmType.DOUBLE) { + return ODataConstants.EDMTYPE_DOUBLE; + } + else if (this == EdmType.GUID) { + return ODataConstants.EDMTYPE_GUID; + } + else if (this == EdmType.INT32) { + return ODataConstants.EDMTYPE_INT32; + } + else if (this == EdmType.INT64) { + return ODataConstants.EDMTYPE_INT64; + } + else if (this == EdmType.DATE_TIME) { + return ODataConstants.EDMTYPE_DATETIME; + } + else { + // VNext : Update here if we add to supported edmtypes in the future. + return Constants.EMPTY_STRING; + } + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java new file mode 100644 index 0000000000000..b449c874abb87 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java @@ -0,0 +1,496 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.text.ParseException; +import java.util.Date; +import java.util.UUID; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.utils.Base64; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * A class which represents a single typed property value in a table entity. An {@link EntityProperty} stores the data + * type as an {@link EdmType}. The value, which may be null for object types, but not for primitive types, + * is serialized and stored as a String. + *

+ * {@link EntityProperty} provides overloaded constructors and overloads of the setValue method for + * supported value types. Each overloaded constructor or setValue method sets the {@link EdmType} and + * serializes the value appropriately based on the parameter type. + *

+ * Use one of the getValueAsType methods to deserialize an {@link EntityProperty} as the + * appropriate Java type. The method will throw a {@link ParseException} or {@link IllegalArgumentException} if the + * {@link EntityProperty} cannot be deserialized as the Java type. + */ +public final class EntityProperty { + private String value; + private EdmType edmType = EdmType.NULL; + private boolean isNull = false; + + /** + * Constructs an {@link EntityProperty} instance from a boolean value. + * + * @param value + * The boolean value of the entity property to set. + */ + public EntityProperty(final boolean value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a byte[] value. + * + * @param value + * The byte[] value of the entity property to set. + */ + public EntityProperty(final byte[] value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a Byte[]. + * + * @param value + * The Byte[] to set as the entity property value. + */ + public EntityProperty(final Byte[] value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a Date value. + * + * @param value + * The Date to set as the entity property value. + */ + public EntityProperty(final Date value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a double value. + * + * @param value + * The double value of the entity property to set. + */ + public EntityProperty(final double value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from an int value. + * + * @param value + * The int value of the entity property to set. + */ + public EntityProperty(final int value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a long value. + * + * @param value + * The long value of the entity property to set. + */ + public EntityProperty(final long value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a String value. + * + * @param value + * The String to set as the entity property value. + */ + public EntityProperty(final String value) { + this.setValue(value); + } + + /** + * Reserved for internal use. Constructs an {@link EntityProperty} instance from a String value and a + * data type, and verifies that the value can be interpreted as the specified data type. + * + * @param value + * The String representation of the value to construct. + * @param edmType + * The {@link EdmType} data type of the value to construct. + * @throws ParseException + * if the String representation of the value cannot be interpreted as the data type. + */ + protected EntityProperty(final String value, final EdmType edmType) throws ParseException { + this.edmType = edmType; + this.value = value; + + // validate data is encoded correctly + if (edmType == EdmType.STRING) { + return; + } + else if (edmType == EdmType.BINARY) { + this.getValueAsByteArray(); + } + else if (edmType == EdmType.BOOLEAN) { + this.getValueAsBoolean(); + } + else if (edmType == EdmType.DOUBLE) { + this.getValueAsDouble(); + } + else if (edmType == EdmType.GUID) { + this.getValueAsUUID(); + } + else if (edmType == EdmType.INT32) { + this.getValueAsInteger(); + } + else if (edmType == EdmType.INT64) { + this.getValueAsLong(); + } + else if (edmType == EdmType.DATE_TIME) { + this.getValueAsDate(); + } + } + + /** + * Constructs an {@link EntityProperty} instance from a java.util.UUID value. + * + * @param value + * The java.util.UUID to set as the entity property value. + */ + public EntityProperty(final UUID value) { + this.setValue(value); + } + + /** + * Reserved for internal use. Constructs an {@link EntityProperty} instance as a null value with the + * specified type. + * + * @param type + * The {@link EdmType} to set as the entity property type. + */ + protected EntityProperty(EdmType type) { + this.value = null; + this.edmType = type; + this.isNull = true; + } + + /** + * Gets the {@link EdmType} storage data type for the {@link EntityProperty}. + * + * @return + * The {@link EdmType} enumeration value for the data type of the {@link EntityProperty}. + */ + public EdmType getEdmType() { + return this.edmType; + } + + /** + * Gets a flag indicating that the {@link EntityProperty} value is null. + * + * @return + * A boolean flag indicating that the {@link EntityProperty} value is null. + */ + public boolean getIsNull() { + return this.isNull; + } + + /** + * Gets the value of this {@link EntityProperty} as a boolean. + * + * @return + * A boolean representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is null or cannot be parsed as a boolean. + */ + public boolean getValueAsBoolean() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Boolean.parseBoolean(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a byte array. + * + * @return + * A byte[] representation of the {@link EntityProperty} value, or null. + */ + public byte[] getValueAsByteArray() { + return this.isNull ? null : Base64.decode(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a Byte array. + * + * @return + * A Byte[] representation of the {@link EntityProperty} value, or null. + */ + public Byte[] getValueAsByteObjectArray() { + return this.isNull ? null : Base64.decodeAsByteObjectArray(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a Date. + * + * @return + * A Date representation of the {@link EntityProperty} value, or null. + * + * @throws IllegalArgumentException + * if the value is not null and cannot be parsed as a Date. + */ + public Date getValueAsDate() { + if (this.isNull) { + return null; + } + + return Utility.parseDate(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a double. + * + * @return + * A double representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is null or cannot be parsed as a double. + */ + public double getValueAsDouble() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Double.parseDouble(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as an int. + * + * @return + * An int representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is null or cannot be parsed as an int. + */ + public int getValueAsInteger() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Integer.parseInt(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a long. + * + * @return + * A long representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is null or cannot be parsed as a long. + */ + public long getValueAsLong() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Long.parseLong(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a String. + * + * @return + * A String representation of the {@link EntityProperty} value, or null. + */ + public String getValueAsString() { + return this.isNull ? null : this.value; + } + + /** + * Gets the value of this {@link EntityProperty} as a java.util.UUID. + * + * @return + * A java.util.UUID representation of the {@link EntityProperty} value, or null. + * + * @throws IllegalArgumentException + * if the value cannot be parsed as a java.util.UUID. + */ + public UUID getValueAsUUID() { + return this.isNull ? null : UUID.fromString(this.value); + } + + /** + * Sets this {@link EntityProperty} using the serialized boolean value. + * + * @param value + * The boolean value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final boolean value) { + this.edmType = EdmType.BOOLEAN; + this.isNull = false; + this.value = value ? Constants.TRUE : Constants.FALSE; + } + + /** + * Sets this {@link EntityProperty} using the serialized byte[] value. + * + * @param value + * The byte[] value to set as the {@link EntityProperty} value. This value may be + * null. + */ + public synchronized final void setValue(final byte[] value) { + this.edmType = EdmType.BINARY; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = Base64.encode(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized Byte[] value. + * + * @param value + * The Byte[] value to set as the {@link EntityProperty} value. This value may be + * null. + */ + public synchronized final void setValue(final Byte[] value) { + this.edmType = EdmType.BINARY; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = Base64.encode(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized Date value. + * + * @param value + * The Date value to set as the {@link EntityProperty} value. This value may be + * null. + */ + public synchronized final void setValue(final Date value) { + this.edmType = EdmType.DATE_TIME; + + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = Utility.getTimeByZoneAndFormat(value, Utility.UTC_ZONE, Utility.ISO8061_LONG_PATTERN); + } + + /** + * Sets this {@link EntityProperty} using the serialized double value. + * + * @param value + * The double value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final double value) { + this.edmType = EdmType.DOUBLE; + this.isNull = false; + this.value = Double.toString(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized int value. + * + * @param value + * The int value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final int value) { + this.edmType = EdmType.INT32; + this.isNull = false; + this.value = Integer.toString(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized long value. + * + * @param value + * The long value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final long value) { + this.edmType = EdmType.INT64; + this.isNull = false; + this.value = Long.toString(value); + } + + /** + * Sets this {@link EntityProperty} using the String value. + * + * @param value + * The String value to set as the {@link EntityProperty} value. This value may be + * null. + */ + public synchronized final void setValue(final String value) { + this.edmType = EdmType.STRING; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = value; + } + + /** + * Sets this {@link EntityProperty} using the serialized java.util.UUID value. + * + * @param value + * The java.util.UUID value to set as the {@link EntityProperty} value. + * This value may be null. + */ + public synchronized final void setValue(final UUID value) { + this.edmType = EdmType.GUID; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = value.toString(); + } + + /** + * Reserved for internal use. Sets the null value flag to the specified boolean value. + * + * @param isNull + * The boolean value to set in the null value flag. + */ + protected void setIsNull(final boolean isNull) { + this.isNull = isNull; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java new file mode 100644 index 0000000000000..1f7d95d3a812a --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java @@ -0,0 +1,61 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.Date; +import java.util.HashMap; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * An interface to perform client side projection on a retrieved entity. An {@link EntityResolver} instance must + * implement a resolve method projecting the entity data represented by the parameters passed in as a new + * instance of the type specified by the type parameter. + *

+ * This interface is useful for converting directly from table entity data to a client object type without requiring a + * separate table entity class type that deserializes every property individually. For example, a client can perform a + * client side projection of a Customer entity by simply returning the String for the + * CustomerName property of each entity. The result of this projection will be a collection of + * Strings containing each customer name. + * + * @param + * The type of the object that the resolver produces. + */ +public interface EntityResolver { + /** + * Returns a reference to a new object instance of type T containing a projection of the specified + * table entity data. + * + * @param partitionKey + * A String containing the PartitionKey value for the entity. + * @param rowKey + * A String containing the RowKey value for the entity. + * @param timeStamp + * A Date containing the Timestamp value for the entity. + * @param properties + * The java.util.HashMap of String property names to {@link EntityProperty} + * data type and value pairs representing the table entity data. + * @param etag + * A String containing the Etag for the entity. + * @return + * A reference to an object instance of type T constructed as a projection of the table entity + * parameters. + * @throws StorageException + * if an error occurs during the operation. + */ + T resolve(String partitionKey, String rowKey, Date timeStamp, HashMap properties, + String etag) throws StorageException; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java new file mode 100644 index 0000000000000..53653dc951505 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java @@ -0,0 +1,37 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation set on a method to prevent its use in serializing or deserializing a property by reflection. Apply the + * @Ignore annotation to methods in a class implementing {@link TableEntity} to force them to be ignored + * during reflection-based serialization and deserialization. See the documentation for {@link TableServiceEntity} for + * more information on using reflection-based serialization and deserialization. + * + * @see StoreAs + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Ignore { + // No attributes +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java new file mode 100644 index 0000000000000..bffc95cdd4276 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java @@ -0,0 +1,25 @@ +/** + * Copyright 2011 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.services.table.client; + +/** + * Reserved for internal use. A class that represents a given MIME Header. + */ +class MimeHeader { + protected String boundary; + protected String contentType; + protected String contentTransferEncoding; + protected String subBoundary; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java new file mode 100644 index 0000000000000..143a4022e3740 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java @@ -0,0 +1,510 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.util.ArrayList; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * Reserved for internal use. A class used to read and write MIME requests and responses. + */ +class MimeHelper { + /** + * Reserved for internal use. A static factory method that generates a {@link StorageException} for invalid MIME + * responses. + * + * @return + * The {@link StorageException} for the invalid MIME response. + */ + protected static StorageException generateMimeParseException() { + return new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, "Invalid MIME response received.", + Constants.HeaderConstants.HTTP_UNUSED_306, null, null); + } + + /** + * Reserved for internal use. Returns the HTTP verb for a table operation. + * + * @param operation + * The {@link TableOperation} instance to get the HTTP verb for. + * @return + * A String containing the HTTP verb to use with the operation. + */ + protected static String getHttpVerbForOperation(final TableOperation operation) { + if (operation.getOperationType() == TableOperationType.INSERT) { + return "POST"; + } + else if (operation.getOperationType() == TableOperationType.DELETE) { + return "DELETE"; + } + else if (operation.getOperationType() == TableOperationType.MERGE + || operation.getOperationType() == TableOperationType.INSERT_OR_MERGE) { + return "MERGE"; + } + else if (operation.getOperationType() == TableOperationType.REPLACE + || operation.getOperationType() == TableOperationType.INSERT_OR_REPLACE) { + return "PUT"; + } + else if (operation.getOperationType() == TableOperationType.RETRIEVE) { + return "GET"; + } + else { + throw new IllegalArgumentException("Unknown table operation"); + } + } + + /** + * Reserved for internal use. Returns the next non-blank line from the {@link BufferedReader}. + * + * @param reader + * The {@link BufferedReader} to read lines from. + * @return + * A String containing the next non-blank line from the {@link BufferedReader}, or + * null. + * @throws IOException + * if an error occurs reading from the {@link BufferedReader}. + */ + protected static String getNextLineSkippingBlankLines(final BufferedReader reader) throws IOException { + String tString = null; + do { + tString = reader.readLine(); + } while (tString != null && tString.length() == 0); + + return tString; + } + + /** + * Reserved for internal use. Reads the response stream from a batch operation into an ArrayList of + * {@link MimePart} objects. + * + * @param inStream + * An {@link InputStream} containing the operation response stream. + * @param expectedBundaryName + * A String containing the MIME part boundary string. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * @return + * An ArrayList of {@link MimePart} objects parsed from the input stream. + * @throws IOException + * if an error occurs accessing the input stream. + * @throws StorageException + * if an error occurs parsing the input stream. + */ + protected static ArrayList readBatchResponseStream(final InputStream inStream, + final String expectedBundaryName, final OperationContext opContext) throws IOException, StorageException { + final ArrayList result = new ArrayList(); + final InputStreamReader streamReader = new InputStreamReader(inStream, "UTF-8"); + final BufferedReader reader = new BufferedReader(streamReader); + final String mungedExpectedBoundaryName = "--".concat(expectedBundaryName); + + final MimeHeader docHeader = readMimeHeader(reader, opContext); + if (docHeader.boundary == null || !docHeader.boundary.equals(mungedExpectedBoundaryName)) { + throw generateMimeParseException(); + } + + MimeHeader currHeader = null; + + // No explicit changeset present + if (docHeader.subBoundary == null) { + do { + result.add(readMimePart(reader, docHeader.boundary, opContext)); + currHeader = readMimeHeader(reader, opContext); + } while (currHeader != null); + } + else { + // explicit changeset present. + currHeader = readMimeHeader(reader, opContext); + if (currHeader == null) { + throw new TableServiceException( + -1, + "An Error Occurred while processing the request, check the extended error information for more details.", + null, reader); + } + else { + do { + result.add(readMimePart(reader, docHeader.subBoundary, opContext)); + currHeader = readMimeHeader(reader, opContext); + } while (currHeader != null); + } + } + + return result; + } + + /** + * Reserved for internal use. A static factory method that constructs a {@link MimeHeader} by parsing the MIME + * header + * data from a {@link BufferedReader}. + * + * @param reader + * The {@link BufferedReader} containing the response stream to parse. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * @return + * A {@link MimeHeader} constructed by parsing the MIME header data from the {@link BufferedReader}. + * @throws IOException + * if an error occurs accessing the input stream. + * @throws StorageException + * if an error occurs parsing the input stream. + */ + protected static MimeHeader readMimeHeader(final BufferedReader reader, final OperationContext opContext) + throws IOException, StorageException { + final MimeHeader retHeader = new MimeHeader(); + reader.mark(1024 * 1024); + + // First thing is separator + retHeader.boundary = getNextLineSkippingBlankLines(reader); + if (retHeader.boundary.endsWith("--")) { + return null; + } + if (!retHeader.boundary.startsWith("--")) { + reader.reset(); + return null; + } + + for (int m = 0; m < 2; m++) { + final String tempString = reader.readLine(); + if (tempString.length() == 0) { + break; + } + + if (tempString.startsWith("Content-Type:")) { + final String[] headerVals = tempString.split("Content-Type: "); + if (headerVals == null || headerVals.length != 2) { + throw generateMimeParseException(); + } + retHeader.contentType = headerVals[1]; + } + else if (tempString.startsWith("Content-Transfer-Encoding:")) { + final String[] headerVals = tempString.split("Content-Transfer-Encoding: "); + if (headerVals == null || headerVals.length != 2) { + throw generateMimeParseException(); + } + retHeader.contentTransferEncoding = headerVals[1]; + } + else { + throw generateMimeParseException(); + } + } + + // Validate headers + if (Utility.isNullOrEmpty(retHeader.boundary) || retHeader.contentType == null) { + throw generateMimeParseException(); + } + + if (retHeader.contentType.startsWith("multipart/mixed; boundary=")) { + final String[] headerVals = retHeader.contentType.split("multipart/mixed; boundary="); + if (headerVals == null || headerVals.length != 2) { + throw generateMimeParseException(); + } + retHeader.subBoundary = "--".concat(headerVals[1]); + } + else if (!retHeader.contentType.equals("application/http")) { + throw generateMimeParseException(); + } + + if (retHeader.contentTransferEncoding != null && !retHeader.contentTransferEncoding.equals("binary")) { + throw generateMimeParseException(); + } + + return retHeader; + } + + // Returns at start of next mime boundary header + /** + * Reserved for internal use. A static factory method that generates a {@link MimePart} containing the next MIME + * part read from the {@link BufferedReader}. + * The {@link BufferedReader} is left positioned at the start of the next MIME boundary header. + * + * @param reader + * The {@link BufferedReader} containing the response stream to parse. + * @param boundary + * A String containing the MIME part boundary string. + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * @return + * A {@link MimePart} constructed by parsing the next MIME part data from the {@link BufferedReader}. + * @throws IOException + * if an error occured accessing the input stream. + * @throws StorageException + * if an error occured parsing the input stream. + */ + protected static MimePart readMimePart(final BufferedReader reader, final String boundary, + final OperationContext opContext) throws IOException, StorageException { + final MimePart retPart = new MimePart(); + // Read HttpStatus code + String tempStr = getNextLineSkippingBlankLines(reader); + if (!tempStr.startsWith("HTTP/1.1 ")) { + throw generateMimeParseException(); + } + + final String[] headerVals = tempStr.split(" "); + + if (headerVals.length < 3) { + throw generateMimeParseException(); + } + + retPart.httpStatusCode = Integer.parseInt(headerVals[1]); + // "HTTP/1.1 XXX ".length() => 13 + retPart.httpStatusMessage = tempStr.substring(13); + + // Read headers + tempStr = reader.readLine(); + while (tempStr != null && tempStr.length() > 0) { + final String[] headerParts = tempStr.split(": "); + if (headerParts.length < 2) { + throw generateMimeParseException(); + } + + retPart.headers.put(headerParts[0], headerParts[1]); + tempStr = reader.readLine(); + } + + // Store xml payload + reader.mark(1024 * 1024); + tempStr = getNextLineSkippingBlankLines(reader); + + if (tempStr == null) { + throw generateMimeParseException(); + } + + // empty body + if (tempStr.startsWith(boundary)) { + reader.reset(); + retPart.payload = Constants.EMPTY_STRING; + return retPart; + } + else if (!tempStr.startsWith("Performing Entity Group + * Transactions. + * + * @param outStream + * The {@link OutputStream} to write the batch request to. + * @param tableName + * A String containing the name of the table to apply each operation to. + * @param batch + * A {@link TableBatchOperation} containing the operations to write to the output stream + * @param batchID + * A String containing the identifier to use as the MIME boundary for the batch request. + * @param changeSet + * A String containing the identifier to use as the MIME boundary for operations within the + * batch. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * @throws IOException + * if an IO error occurs. + * @throws URISyntaxException + * if an invalid URI is used. + * @throws StorageException + * if an error occurs accessing the Storage service. + * @throws XMLStreamException + * if an error occurs accessing the stream. + */ + protected static void writeBatchToStream(final OutputStream outStream, final String tableName, + final TableBatchOperation batch, final String batchID, final String changeSet, + final OperationContext opContext) throws IOException, URISyntaxException, StorageException, + XMLStreamException { + final OutputStreamWriter outWriter = new OutputStreamWriter(outStream, "UTF8"); + + int contentID = 0; + boolean inChangeSet = false; + for (final TableOperation op : batch) { + if (op.getOperationType() == TableOperationType.RETRIEVE) { + final QueryTableOperation qOp = (QueryTableOperation) op; + + if (inChangeSet) { + inChangeSet = false; + // Write Boundary end. + MimeHelper.writeMIMEBoundaryClosure(outWriter, changeSet); + outWriter.write("\r\n"); + } + + // Write MIME Header + MimeHelper.writeMIMEBoundary(outWriter, batchID); + outWriter.write("Content-Type: application/http\r\n"); + outWriter.write("Content-Transfer-Encoding: binary\r\n\r\n"); + + outWriter.write(String.format("%s %s HTTP/1.1\r\n", getHttpVerbForOperation(op), + qOp.generateRequestIdentityWithTable(tableName))); + + outWriter.write("Host: host\r\n\r\n"); + } + else { + if (!inChangeSet) { + inChangeSet = true; + // New batch mime part + MimeHelper.writeMIMEBoundary(outWriter, batchID); + MimeHelper.writeMIMEContentType(outWriter, changeSet); + outWriter.write("\r\n"); + } + + // New mime part for changeset + MimeHelper.writeMIMEBoundary(outWriter, changeSet); + + // Write Headers + outWriter.write("Content-Type: application/http\r\n"); + outWriter.write("Content-Transfer-Encoding: binary\r\n\r\n"); + + outWriter.write(String.format("%s %s HTTP/1.1\r\n", getHttpVerbForOperation(op), + op.generateRequestIdentityWithTable(tableName))); + + outWriter.write(String.format("Content-ID: %s\r\n", Integer.toString(contentID))); + + if (op.getOperationType() != TableOperationType.INSERT + && op.getOperationType() != TableOperationType.INSERT_OR_MERGE + && op.getOperationType() != TableOperationType.INSERT_OR_REPLACE) { + outWriter.write(String.format("If-Match: %s\r\n", op.getEntity().getEtag())); + } + + if (op.getOperationType() == TableOperationType.DELETE) { + // empty body + outWriter.write("\r\n"); + } + else { + outWriter.write("Content-Type: application/atom+xml;type=entry\r\n"); + final String opString = writeStringForOperation(op, opContext); + outWriter.write(String.format("Content-Length: %s\r\n\r\n", + Integer.toString(opString.getBytes("UTF-8").length))); + outWriter.write(opString); + } + contentID = contentID + 1; + } + } + + if (inChangeSet) { + MimeHelper.writeMIMEBoundaryClosure(outWriter, changeSet); + } + MimeHelper.writeMIMEBoundaryClosure(outWriter, batchID); + + outWriter.flush(); + } + + /** + * Reserved for internal use. Writes a MIME part boundary to the output stream. + * + * @param outWriter + * The {@link OutputStreamWriter} to write the MIME part boundary to. + * @param boundaryID + * The String containing the MIME part boundary string. + * @throws IOException + * if an error occurs writing to the output stream. + */ + protected static void writeMIMEBoundary(final OutputStreamWriter outWriter, final String boundaryID) + throws IOException { + outWriter.write(String.format("--%s\r\n", boundaryID)); + } + + /** + * Reserved for internal use. Writes a MIME part boundary closure to the output stream. + * + * @param outWriter + * The {@link OutputStreamWriter} to write the MIME part boundary closure to. + * @param boundaryID + * The String containing the MIME part boundary string. + * @throws IOException + * if an error occurs writing to the output stream. + */ + protected static void writeMIMEBoundaryClosure(final OutputStreamWriter outWriter, final String boundaryID) + throws IOException { + outWriter.write(String.format("--%s--\r\n", boundaryID)); + } + + /** + * Reserved for internal use. Writes a MIME content type string to the output stream. + * + * @param outWriter + * The {@link OutputStreamWriter} to write the MIME content type string to. + * @param boundaryID + * The String containing the MIME part boundary string. + * @throws IOException + * if an error occurs writing to the output stream. + */ + protected static void writeMIMEContentType(final OutputStreamWriter outWriter, final String boundaryName) + throws IOException { + outWriter.write(String.format("Content-Type: multipart/mixed; boundary=%s\r\n", boundaryName)); + } + + /** + * Reserved for internal use. Generates a String containing the entity associated with an operation in + * AtomPub format. + * + * @param operation + * A {@link TableOperation} containing the entity to write to the returned String. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * @return + * A String containing the entity associated with the operation in AtomPub format + * @throws StorageException + * if a Storage error occurs. + * @throws XMLStreamException + * if an error occurs creating or writing to the output string. + */ + protected static String writeStringForOperation(final TableOperation operation, final OperationContext opContext) + throws StorageException, XMLStreamException { + final StringWriter outWriter = new StringWriter(); + final XMLOutputFactory xmlOutFactoryInst = XMLOutputFactory.newInstance(); + final XMLStreamWriter xmlw = xmlOutFactoryInst.createXMLStreamWriter(outWriter); + + AtomPubParser.writeSingleEntityToStream(operation.getEntity(), false, xmlw, opContext); + outWriter.write("\r\n"); + + return outWriter.toString(); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimePart.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimePart.java new file mode 100644 index 0000000000000..254581502e2b8 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimePart.java @@ -0,0 +1,28 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.HashMap; + +/** + * Reserved for internal use. A class that represents a given MIME Part. + */ +class MimePart { + protected int httpStatusCode = -1; + protected String httpStatusMessage; + protected HashMap headers = new HashMap(); + protected String payload; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataConstants.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataConstants.java new file mode 100644 index 0000000000000..ed89049797d11 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataConstants.java @@ -0,0 +1,183 @@ +/** + * Copyright 2011 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.services.table.client; + +/** + * Reserved for internal use. A class that holds relevant constants for interacting with OData feeds. + */ +class ODataConstants { + /** + * The String representation of the Atom namespace. + */ + public static final String ATOM_NS = "http://www.w3.org/2005/Atom"; + + /** + * The String representation of the OData Data namespace. + */ + public static final String DATA_SERVICES_NS = "http://schemas.microsoft.com/ado/2007/08/dataservices"; + + /** + * The String representation of the OData Metadata namespace. + */ + public static final String DATA_SERVICES_METADATA_NS = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"; + + /** + * The String representation of the Atom namespace in brackets. + */ + public static final String BRACKETED_ATOM_NS = "{" + ATOM_NS + "}"; // default + + /** + * The String representation of the OData Data namespace in brackets. + */ + public static final String BRACKETED_DATA_SERVICES_NS = "{" + DATA_SERVICES_NS + "}"; // d: + + /** + * The String representation of the OData Metadata namespace in brackets. + */ + public static final String BRACKETED_DATA_SERVICES_METADATA_NS = "{" + DATA_SERVICES_METADATA_NS + "}"; // m: + + /** + * The String representation of the Atom Entry feed element name. + */ + public static final String FEED = "feed"; + + /** + * The String representation of the Atom Entry title element name. + */ + public static final String TITLE = "title"; + + /** + * The String representation of the Atom Entry id element name. + */ + public static final String ID = "id"; + + /** + * The String representation of the Atom Entry updated element name. + */ + public static final String UPDATED = "updated"; + + /** + * The String representation of the Atom Entry link element name. + */ + public static final String LINK = "link"; + + /** + * The String representation of the Atom Entry author element name. + */ + public static final String AUTHOR = "author"; + + /** + * The String representation of the Atom Entry name element name. + */ + public static final String NAME = "name"; + + /** + * The String representation of the Atom Entry entry element name. + */ + public static final String ENTRY = "entry"; + + /** + * The String representation of the Atom Entry category element name. + */ + public static final String CATEGORY = "category"; + + /** + * The String representation of the Atom Entry content element name. + */ + public static final String CONTENT = "content"; + + /** + * The String representation of the OData Metadata properties element name. + */ + public static final String PROPERTIES = "properties"; + + /** + * The String representation of the Atom Entry etag element name. + */ + public static final String ETAG = "etag"; + + /** + * The String representation of the type attribute name. + */ + public static final String TYPE = "type"; + + /** + * The String representation of the term element name. + */ + public static final String TERM = "term"; + + /** + * The String representation of scheme. + */ + public static final String SCHEME = "scheme"; + + /** + * The String representation of href. + */ + public static final String HREF = "href"; + + /** + * The String representation of rel. + */ + public static final String REL = "rel"; + + /** + * The String representation of the null attribute name. + */ + public static final String NULL = "null"; + + /** + * The String representation of the content type attribute value to send. + */ + public static final String ODATA_CONTENT_TYPE = "application/xml"; + + // Odata edm types + + /** + * The String representation of the Edm.DateTime metadata type attribute value. + */ + public static final String EDMTYPE_DATETIME = "Edm.DateTime"; + + /** + * The String representation of the Edm.Binary metadata type attribute value. + */ + public static final String EDMTYPE_BINARY = "Edm.Binary"; + + /** + * The String representation of the Edm.Boolean metadata type attribute value. + */ + public static final String EDMTYPE_BOOLEAN = "Edm.Boolean"; + + /** + * The String representation of the Edm.Double metadata type attribute value. + */ + public static final String EDMTYPE_DOUBLE = "Edm.Double"; + + /** + * The String representation of the Edm.Guid metadata type attribute value. + */ + public static final String EDMTYPE_GUID = "Edm.Guid"; + + /** + * The String representation of the Edm.Int32 metadata type attribute value. + */ + public static final String EDMTYPE_INT32 = "Edm.Int32"; + + /** + * The String representation of the Edm.Int64 metadata type attribute value. + */ + public static final String EDMTYPE_INT64 = "Edm.Int64"; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataPayload.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataPayload.java new file mode 100644 index 0000000000000..9f0f5c573becb --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataPayload.java @@ -0,0 +1,42 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.ArrayList; + +/** + * Reserved for internal use. A class that represents an OData payload and resulting entities. + */ +class ODataPayload { + /** + * A collection of table entities. + */ + protected ArrayList results; + + /** + * A collection of {@link TableResults} which include additional information about the entities returned by an + * operation. + */ + protected ArrayList tableResults; + + /** + * Constructs an {@link ODataPayload} instance with new empty entity and {@link TableResult} collections. + */ + protected ODataPayload() { + this.results = new ArrayList(); + this.tableResults = new ArrayList(); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/PropertyPair.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/PropertyPair.java new file mode 100644 index 0000000000000..24da5f2adc585 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/PropertyPair.java @@ -0,0 +1,277 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.UUID; + +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * Reserved for internal use. A class used internally during the reflection process to determine which properties should + * be serialized. + */ +class PropertyPair { + /** + * Reserved for internal use. A static factory method to generate a map of property names to {@link PropertyPair} + * instances for the specified class type. Uses reflection to find pairs of getter and setter methods that are + * annotated with {@link StoreAs} with a common property name, or of the form getPropertyName + * and setPropertyName, with a common type for the getter return value and the + * setter parameter, and stores the methods and the property name for each pair found in a map for use in + * serializing and deserializing entity data. + * + * @param clazzType + * The class type to check for matching getter and setter methods with a common return and parameter + * type, respectively. + */ + protected static HashMap generatePropertyPairs(final Class clazzType) { + final Method[] methods = clazzType.getMethods(); + final HashMap propMap = new HashMap(); + + String propName = null; + PropertyPair currProperty = null; + + for (final Method m : methods) { + if (m.getName().length() < 4 || (!m.getName().startsWith("get") && !m.getName().startsWith("set"))) { + continue; + } + + // TODO add logging + // System.out.println(m.getName()); + + propName = m.getName().substring(3); + + // Skip interface methods, these will be called explicitly + if (propName.equals(TableConstants.PARTITION_KEY) || propName.equals(TableConstants.ROW_KEY) + || propName.equals(TableConstants.TIMESTAMP) || propName.equals("Etag") + || propName.equals("LastModified")) { + continue; + } + + if (propMap.containsKey(propName)) { + currProperty = propMap.get(propName); + } + else { + currProperty = new PropertyPair(); + currProperty.name = propName; + propMap.put(propName, currProperty); + } + + // TODO add logging + // System.out.println(m.getReturnType()); + if (m.getName().startsWith("get") && m.getParameterTypes().length == 0) { + currProperty.getter = m; + } + else if (m.getName().startsWith("set") && m.getParameterTypes().length == 1 + && void.class.equals(m.getReturnType())) { + currProperty.setter = m; + } + + // Check for StoreAs Annotation + final StoreAs storeAsInstance = m.getAnnotation(StoreAs.class); + if (storeAsInstance != null) { + if (Utility.isNullOrEmpty(storeAsInstance.name())) { + throw new IllegalArgumentException(String.format( + "StoreAs Annotation found for property %s with empty value", currProperty.name)); + } + + if (currProperty.effectiveName != null && !currProperty.effectiveName.equals(currProperty.name) + && !currProperty.effectiveName.equals(storeAsInstance.name())) { + throw new IllegalArgumentException( + String.format( + "StoreAs Annotation found for both getter and setter for property %s with non equal values", + currProperty.name)); + } + + if (!currProperty.name.equals(storeAsInstance.name())) { + currProperty.effectiveName = storeAsInstance.name(); + } + } + } + + // Return only processable pairs + final ArrayList keysToRemove = new ArrayList(); + final ArrayList keysToAlter = new ArrayList(); + + for (final Entry e : propMap.entrySet()) { + if (!e.getValue().shouldProcess()) { + keysToRemove.add(e.getKey()); + continue; + } + + if (!Utility.isNullOrEmpty(e.getValue().effectiveName)) { + keysToAlter.add(e.getKey()); + } + else { + e.getValue().effectiveName = e.getValue().name; + } + } + + // remove all entries for keys that should not process + for (final String key : keysToRemove) { + propMap.remove(key); + } + + // Any store as properties should be re-stored into the hash under the efective name. + for (final String key : keysToAlter) { + final PropertyPair p = propMap.get(key); + propMap.remove(key); + propMap.put(p.effectiveName, p); + } + + return propMap; + } + + private Method getter = null; + private Method setter = null; + private String name = null; + String effectiveName = null; + + /** + * Reserved for internal use. Invokes the setter method on the specified instance parameter with the value of the + * {@link EntityProperty} deserialized as the appropriate type. + * + * @param prop + * The {@link EntityProperty} containing the value to pass to the setter on the instance. + * @param instance + * An instance of a class supporting this property with getter and setter methods of the + * appropriate name and parameter or return type. + * + * @throws IllegalArgumentException + * if the specified instance parameter is not an instance of the class + * or interface declaring the setter method (or of a subclass or implementor thereof). + * @throws IllegalAccessException + * if the setter method is inaccessible. + * @throws InvocationTargetException + * if the setter method throws an exception. + */ + protected void consumeTableProperty(final EntityProperty prop, final Object instance) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + if (prop.getEdmType() == EdmType.STRING) { + this.setter.invoke(instance, prop.getValueAsString()); + } + else if (prop.getEdmType() == EdmType.BINARY) { + if (this.setter.getParameterTypes()[0].equals(Byte[].class)) { + this.setter.invoke(instance, (Object) prop.getValueAsByteObjectArray()); + } + else { + this.setter.invoke(instance, prop.getValueAsByteArray()); + } + } + else if (prop.getEdmType() == EdmType.BOOLEAN) { + this.setter.invoke(instance, prop.getValueAsBoolean()); + } + else if (prop.getEdmType() == EdmType.DOUBLE) { + this.setter.invoke(instance, prop.getValueAsDouble()); + } + else if (prop.getEdmType() == EdmType.GUID) { + this.setter.invoke(instance, prop.getValueAsUUID()); + } + else if (prop.getEdmType() == EdmType.INT32) { + this.setter.invoke(instance, prop.getValueAsInteger()); + } + else if (prop.getEdmType() == EdmType.INT64) { + this.setter.invoke(instance, prop.getValueAsLong()); + } + else if (prop.getEdmType() == EdmType.DATE_TIME) { + this.setter.invoke(instance, prop.getValueAsDate()); + } + else { + throw new IllegalArgumentException(String.format("Property %s with Edm Type %s cannot be de-serialized.", + this.name, prop.getEdmType().toString())); + } + } + + /** + * Reserved for internal use. Generates an {@link EntityProperty} from the result of invoking the getter method for + * this property on the specified instance parameter. + * + * @param instance + * An instance of a class supporting this property with getter and setter methods of the + * appropriate name and parameter or return type. + * + * @return + * An {@link EntityProperty} with the data type and value returned by the invoked getter on the instance. + * + * @throws IllegalArgumentException + * if the specified instance parameter is not an instance of the class + * or interface declaring the getter method (or of a subclass or implementor thereof). + * @throws IllegalAccessException + * if the getter method is inaccessible. + * @throws InvocationTargetException + * if the getter method throws an exception. + */ + protected EntityProperty generateTableProperty(final Object instance) throws IllegalArgumentException, + IllegalAccessException, InvocationTargetException { + final Class getType = this.getter.getReturnType(); + Object val = this.getter.invoke(instance, (Object[]) null); + + if (getType.equals(byte[].class)) { + return val != null ? new EntityProperty((byte[]) val) : new EntityProperty(EdmType.BINARY); + } + else if (getType.equals(Byte[].class)) { + return val != null ? new EntityProperty((Byte[]) val) : new EntityProperty(EdmType.BINARY); + } + else if (getType.equals(String.class)) { + return val != null ? new EntityProperty((String) val) : new EntityProperty(EdmType.STRING); + } + else if (getType.equals(boolean.class) || getType.equals(Boolean.class)) { + return val != null ? new EntityProperty((Boolean) val) : new EntityProperty(EdmType.BOOLEAN); + } + else if (getType.equals(double.class) || getType.equals(Double.class)) { + return val != null ? new EntityProperty((Double) val) : new EntityProperty(EdmType.DOUBLE); + } + else if (getType.equals(UUID.class)) { + return val != null ? new EntityProperty((UUID) val) : new EntityProperty(EdmType.GUID); + } + else if (getType.equals(int.class) || getType.equals(Integer.class)) { + return val != null ? new EntityProperty((Integer) val) : new EntityProperty(EdmType.INT32); + } + else if (getType.equals(long.class) || getType.equals(Long.class)) { + return val != null ? new EntityProperty((Long) val) : new EntityProperty(EdmType.INT64); + } + else if (getType.equals(Date.class)) { + return val != null ? new EntityProperty((Date) val) : new EntityProperty(EdmType.DATE_TIME); + } + else { + throw new IllegalArgumentException(String.format("Property %s with return type %s cannot be serialized.", + this.getter.getName(), this.getter.getReturnType())); + } + } + + /** + * Reserved for internal use. A utility function that returns true if this property is accessible + * through reflection. + * + * @return + */ + protected boolean shouldProcess() { + if (Utility.isNullOrEmpty(this.name) || this.getter == null || this.getter.isAnnotationPresent(Ignore.class) + || this.setter == null || this.setter.isAnnotationPresent(Ignore.class) + || (!this.getter.getReturnType().equals(this.setter.getParameterTypes()[0]))) { + return false; + } + + // TODO add logging + // System.out.println("Valid property " + this.name + " Storing as " + this.effectiveName); + return true; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/QueryTableOperation.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/QueryTableOperation.java new file mode 100644 index 0000000000000..52dbf5ecdccb3 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/QueryTableOperation.java @@ -0,0 +1,268 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.text.ParseException; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * A class that extends {@link TableOperation} to implement a query to retrieve a single table entity. To execute a + * {@link QueryTableOperation} instance, call the execute method on a {@link CloudTableClient} instance. + * This operation can be executed directly or as part of a {@link TableBatchOperation}. If the + * {@link QueryTableOperation} returns an entity result, it is stored in the corresponding {@link TableResult} returned + * by the execute method. + */ +public class QueryTableOperation extends TableOperation { + private EntityResolver resolver; + + private Class clazzType; + + private String partitionKey; + + private String rowKey; + + /** + * Default constructor. + */ + protected QueryTableOperation() { + super(null, TableOperationType.RETRIEVE); + } + + /** + * Constructs a {@link QueryTableOperation} instance to retrieve a single table entity with the specified partition + * key and row key. + * + * @param partitionKey + * A String containing the PartitionKey value for the entity. + * @param rowKey + * A String containing the RowKey value for the entity. + */ + QueryTableOperation(final String partitionKey, final String rowKey) { + super(null, TableOperationType.RETRIEVE); + Utility.assertNotNullOrEmpty("partitionKey", partitionKey); + this.partitionKey = partitionKey; + this.rowKey = rowKey; + } + + /** + * Gets the PartitionKey value for the entity to retrieve. + * + * @return + * A String containing the PartitionKey value for the entity. + */ + public String getPartitionKey() { + return this.partitionKey; + } + + /** + * Gets the resolver to project the entity retrieved as a particular type. + * + * @return + * The {@link EntityResolver} instance. + */ + public EntityResolver getResolver() { + return this.resolver; + } + + /** + * Gets the RowKey value for the entity to retrieve. + * + * @return + * A String containing the RowKey value for the entity. + */ + public String getRowKey() { + return this.rowKey; + } + + /** + * Reserved for internal use. Gets the class type of the entity returned by the query. + * + * @return + * The java.lang.Class implementing {@link TableEntity} that represents the entity type for the + * query. + */ + protected Class getClazzType() { + return this.clazzType; + } + + /** + * Reserved for internal use. Parses the query table operation response into a {@link TableResult} to return. + * + * @param xmlr + * An XMLStreamReader containing the response to the query operation. + * @param httpStatusCode + * The HTTP status code returned from the operation request. + * @param etagFromHeader + * The String containing the Etag returned with the operation response. + * @param opContext + * An {@link OperationContext} object that represents the context for the current operation. + * + * @return + * The {@link TableResult} representing the result of the query operation. + * + * @throws XMLStreamException + * if an error occurs accessing the XMLStreamReader. + * @throws ParseException + * if an error occurs in parsing the response. + * @throws InstantiationException + * if an error occurs in object construction. + * @throws IllegalAccessException + * if an error occurs in reflection on an object type. + * @throws StorageException + * if an error occurs in the storage operation. + */ + @Override + protected TableResult parseResponse(final XMLStreamReader xmlr, final int httpStatusCode, + final String etagFromHeader, final OperationContext opContext) throws XMLStreamException, ParseException, + InstantiationException, IllegalAccessException, StorageException { + return AtomPubParser.parseSingleOpResponse(xmlr, httpStatusCode, this.getClazzType(), this.getResolver(), + opContext); + } + + /** + * Reserved for internal use. Performs a retrieve operation on the specified table, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * 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 and storage account + * credentials to use. + * @param tableName + * A String containing the name of the table to query. + * @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 query operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + protected TableResult performRetrieve(final CloudTableClient client, final String tableName, + final TableRequestOptions options, final OperationContext opContext) throws StorageException { + final boolean isTableEntry = TableConstants.TABLES_SERVICE_TABLES_NAME.equals(tableName); + if (this.getClazzType() != null) { + Utility.checkNullaryCtor(this.getClazzType()); + } + else { + Utility.assertNotNull("Query requires a valid class type or resolver.", this.getResolver()); + } + + final StorageOperation impl = new StorageOperation( + options) { + @Override + public TableResult execute(final CloudTableClient client, final QueryTableOperation operation, + final OperationContext opContext) throws Exception { + + final HttpURLConnection request = TableRequest.query(client.getEndpoint(), tableName, + generateRequestIdentity(isTableEntry, operation.getPartitionKey()), + options.getTimeoutIntervalInMs(), null/* Query Builder */, null/* Continuation Token */, + options, opContext); + + client.getCredentials().signRequestLite(request, -1L, opContext); + + this.setResult(ExecutionEngine.processRequest(request, opContext)); + + if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_OK) { + // Parse response for updates + InputStream inStream = request.getInputStream(); + final XMLStreamReader xmlr = Utility.createXMLStreamReaderFromStream(inStream); + TableResult res = null; + + try { + res = AtomPubParser.parseSingleOpResponse(xmlr, this.getResult().getStatusCode(), + operation.getClazzType(), operation.getResolver(), opContext); + } + finally { + inStream.close(); + } + + return res; + } + else if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_NOT_FOUND) { + // Empty result + return new TableResult(this.getResult().getStatusCode()); + } + else { + this.setNonExceptionedRetryableFailure(true); + return null; + } + + } + }; + + return ExecutionEngine.executeWithRetry(client, this, impl, options.getRetryPolicyFactory(), opContext); + } + + /** + * Reserved for internal use. Sets the class type of the entity returned by the query. + * + * @param clazzType + * The java.lang.Class implementing {@link TableEntity} that represents the entity type for + * the query. + */ + protected void setClazzType(final Class clazzType) { + Utility.assertNotNull("clazzType", clazzType); + Utility.checkNullaryCtor(clazzType); + this.clazzType = clazzType; + } + + /** + * Reserved for internal use. Sets the PartitionKey value for the entity to retrieve. + * + * @param partitionKey + * A String containing the PartitionKey value for the entity. + */ + protected void setPartitionKey(final String partitionKey) { + this.partitionKey = partitionKey; + } + + /** + * Reserved for internal use. Sets the resolver to project the entity retrieved as a particular type. + * + * @param resolver + * The {@link EntityResolver} instance to use. + */ + protected void setResolver(final EntityResolver resolver) { + Utility.assertNotNull("Query requires a valid class type or resolver.", resolver); + this.resolver = resolver; + } + + /** + * Reserved for internal use. Sets the RowKey value for the entity to retrieve. + * + * @param rowKey + * A String containing the RowKey value for the entity. + */ + protected void setRowKey(final String rowKey) { + this.rowKey = rowKey; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/StoreAs.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/StoreAs.java new file mode 100644 index 0000000000000..7918530c85f34 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/StoreAs.java @@ -0,0 +1,49 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to override the name a property is serialized and deserialized with using reflection. Use this + * annotation to specify the property name to associate with the data stored by a setter method or retrieved by a getter + * method in a class implementing {@link TableEntity} that uses reflection-based serialization and deserialization. Note + * that the names "PartitionKey", "RowKey", "Timestamp", and "Etag" are reserved and will be ignored if set with the + * @StoreAs annotation. + *

+ * Example: + *

+ * @StoreAs(name = "EntityPropertyName")
public String getObjectPropertyName() { ... }
+ *

+ * @StoreAs(name = "EntityPropertyName")
public void setObjectPropertyName(String name) { ... }
+ *

+ * This example shows how the methods that would get and set an entity property named ObjectPropertyName in the + * default case can be annotated to get and set an entity property named EntityPropertyName. See the + * documentation for {@link TableServiceEntity} for more information on using reflection-based serialization and + * deserialization. + * + * @see Ignore + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface StoreAs { + public String name(); +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableBatchOperation.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableBatchOperation.java new file mode 100644 index 0000000000000..596e519612c30 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableBatchOperation.java @@ -0,0 +1,514 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.InputStream; +import java.io.StringReader; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.UUID; + +import javax.xml.stream.XMLStreamReader; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * A class which represents a batch operation. A batch operation is a collection of table operations which are executed + * by the Storage Service REST API as a single atomic operation, by invoking an Entity Group Transaction. + *

+ * A batch operation may contain up to 100 individual table operations, with the requirement that each operation entity + * must have same partition key. A batch with a retrieve operation cannot contain any other operations. Note that the + * total payload of a batch operation is limited to 4MB. + */ +public class TableBatchOperation extends ArrayList { + private static final long serialVersionUID = -1192644463287355790L; + private boolean hasQuery = false; + private String partitionKey = null; + + /** + * Adds the table operation at the specified index in the batch operation ArrayList. + * + * @param index + * The index in the batch operation ArrayList to add the table operation at. + * @param element + * The {@link TableOperation} to add to the batch operation. + */ + @Override + public void add(final int index, final TableOperation element) { + Utility.assertNotNull("element", element); + + this.checkSingleQueryPerBatch(element); + + if (element.getOperationType() == TableOperationType.RETRIEVE) { + this.lockToPartitionKey(((QueryTableOperation) element).getPartitionKey()); + } + else { + this.lockToPartitionKey(element.getEntity().getPartitionKey()); + } + super.add(index, element); + } + + /** + * Adds the table operation to the batch operation ArrayList. + * + * @param element + * The {@link TableOperation} to add to the batch operation. + * @return + * true if the operation was added successfully. + */ + @Override + public boolean add(final TableOperation element) { + Utility.assertNotNull("element", element); + this.checkSingleQueryPerBatch(element); + if (element.getEntity() == null) { + // Query operation + this.lockToPartitionKey(((QueryTableOperation) element).getPartitionKey()); + } + else { + this.lockToPartitionKey(element.getEntity().getPartitionKey()); + } + + return super.add(element); + } + + /** + * Adds the collection of table operations to the batch operation ArrayList starting at the specified + * index. + * + * @param index + * The index in the batch operation ArrayList to add the table operation at. + * @param c + * The collection of {@link TableOperation} objects to add to the batch operation. + * @return + * true if the operations were added successfully. + */ + @Override + public boolean addAll(final int index, final java.util.Collection c) { + for (final TableOperation operation : c) { + Utility.assertNotNull("operation", operation); + this.checkSingleQueryPerBatch(operation); + + if (operation.getEntity() == null) { + // Query operation + this.lockToPartitionKey(((QueryTableOperation) operation).getPartitionKey()); + } + else { + this.lockToPartitionKey(operation.getEntity().getPartitionKey()); + } + } + + return super.addAll(index, c); + } + + /** + * Adds the collection of table operations to the batch operation ArrayList. + * + * @param c + * The collection of {@link TableOperation} objects to add to the batch operation. + * @return + * true if the operations were added successfully. + */ + @Override + public boolean addAll(final java.util.Collection c) { + for (final TableOperation operation : c) { + Utility.assertNotNull("operation", operation); + this.checkSingleQueryPerBatch(operation); + + if (operation.getEntity() == null) { + // Query operation + this.lockToPartitionKey(((QueryTableOperation) operation).getPartitionKey()); + } + else { + this.lockToPartitionKey(operation.getEntity().getPartitionKey()); + } + } + + return super.addAll(c); + } + + /** + * Clears all table operations from the batch operation. + */ + @Override + public void clear() { + super.clear(); + checkResetEntityLocks(); + } + + /** + * Adds a table operation to delete the specified entity to the batch operation. + * + * @param entity + * The {@link TableEntity} to delete. + */ + public void delete(final TableEntity entity) { + this.lockToPartitionKey(entity.getPartitionKey()); + this.add(TableOperation.delete(entity)); + } + + /** + * Adds a table operation to insert the specified entity to the batch operation. + * + * @param entity + * The {@link TableEntity} to insert. + */ + public void insert(final TableEntity entity) { + this.lockToPartitionKey(entity.getPartitionKey()); + this.add(TableOperation.insert(entity)); + } + + /** + * Adds a table operation to insert or merge the specified entity to the batch operation. + * + * @param entity + * The {@link TableEntity} to insert if not found or to merge if it exists. + */ + public void insertOrMerge(final TableEntity entity) { + this.lockToPartitionKey(entity.getPartitionKey()); + this.add(TableOperation.insertOrMerge(entity)); + } + + /** + * Adds a table operation to insert or replace the specified entity to the batch operation. + * + * @param entity + * The {@link TableEntity} to insert if not found or to replace if it exists. + */ + public void insertOrReplace(final TableEntity entity) { + this.lockToPartitionKey(entity.getPartitionKey()); + this.add(TableOperation.insertOrReplace(entity)); + } + + /** + * Adds a table operation to merge the specified entity to the batch operation. + * + * @param entity + * The {@link TableEntity} to merge. + */ + public void merge(final TableEntity entity) { + this.lockToPartitionKey(entity.getPartitionKey()); + this.add(TableOperation.merge(entity)); + } + + /** + * Adds a table operation to retrieve an entity of the specified class type with the specified PartitionKey and + * RowKey to the batch operation. + * + * @param partitionKey + * A String containing the PartitionKey of the entity to retrieve. + * @param rowKey + * A String containing the RowKey of the entity to retrieve. + * @param clazzType + * The class of the {@link TableEntity} type for the entity to retrieve. + */ + public void retrieve(final String partitionKey, final String rowKey, final Class clazzType) { + this.lockToPartitionKey(partitionKey); + this.add(TableOperation.retrieve(partitionKey, rowKey, clazzType)); + } + + /** + * Adds a table operation to retrieve an entity of the specified class type with the specified PartitionKey and + * RowKey to the batch operation. + * + * @param partitionKey + * A String containing the PartitionKey of the entity to retrieve. + * @param rowKey + * A String containing the RowKey of the entity to retrieve. + * @param resolver + * The {@link EntityResolver} implementation to project the entity to retrieve as a particular type in + * the result. + */ + public void retrieve(final String partitionKey, final String rowKey, final EntityResolver resolver) { + this.lockToPartitionKey(partitionKey); + this.add(TableOperation.retrieve(partitionKey, rowKey, resolver)); + } + + /** + * Removes the table operation at the specified index from the batch operation. + * + * @param index + * The index in the ArrayList of the table operation to remove from the batch operation. + */ + @Override + public TableOperation remove(int index) { + TableOperation op = super.remove(index); + checkResetEntityLocks(); + return op; + } + + /** + * Removes the specified Object from the batch operation. + * + * @param o + * The Object to remove from the batch operation. + * @return + * true if the object was removed successfully. + */ + @Override + public boolean remove(Object o) { + boolean ret = super.remove(o); + checkResetEntityLocks(); + return ret; + } + + /** + * Removes all elements of the specified collection from the batch operation. + * + * @param c + * The collection of elements to remove from the batch operation. + * @return + * true if the objects in the collection were removed successfully. + */ + @Override + public boolean removeAll(java.util.Collection c) { + boolean ret = super.removeAll(c); + checkResetEntityLocks(); + return ret; + } + + /** + * Adds a table operation to replace the specified entity to the batch operation. + * + * @param entity + * The {@link TableEntity} to replace. + */ + public void replace(final TableEntity entity) { + this.lockToPartitionKey(entity.getPartitionKey()); + this.add(TableOperation.replace(entity)); + } + + /** + * Reserved for internal use. Clears internal fields when the batch operation is empty. + */ + private void checkResetEntityLocks() { + if (this.size() == 0) { + this.partitionKey = null; + this.hasQuery = false; + } + } + + /** + * Reserved for internal use. Verifies that the batch operation either contains no retrieve operations, or contains + * only a single retrieve operation. + * + * @param op + * The {@link TableOperation} to be added if the verification succeeds. + */ + private void checkSingleQueryPerBatch(final TableOperation op) { + // if this has a query then no other operations can be added. + if (this.hasQuery) { + throw new IllegalArgumentException( + "A batch transaction with a retrieve operation cannot contain any other operations."); + } + + if (op.opType == TableOperationType.RETRIEVE) { + if (this.size() > 0) { + throw new IllegalArgumentException( + "A batch transaction with a retrieve operation cannot contain any other operations."); + } + else { + this.hasQuery = true; + } + } + } + + /** + * Reserved for internal use. Verifies that the specified PartitionKey value matches the value in the batch + * operation. + * + * @param partitionKey + * The String containing the PartitionKey value to check. + */ + private void lockToPartitionKey(final String partitionKey) { + if (this.partitionKey == null) { + this.partitionKey = partitionKey; + } + else { + if (partitionKey.length() != partitionKey.length() || !this.partitionKey.equals(partitionKey)) { + throw new IllegalArgumentException("All entities in a given batch must have the same partition key."); + } + } + } + + /** + * Reserved for internal use. Executes this batch operation on the specified table, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method will invoke the Storage Service REST API to execute this batch 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 and storage account + * credentials to use. + * @param tableName + * A String 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 + * An ArrayList of {@link TableResult} containing the results of executing the operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + protected ArrayList execute(final CloudTableClient client, final String tableName, + final TableRequestOptions options, final OperationContext opContext) throws StorageException { + + Utility.assertNotNullOrEmpty("TableName", tableName); + + if (this.size() == 0) { + throw new IllegalArgumentException("Cannot Execute an empty batch operation"); + } + + final StorageOperation> impl = new StorageOperation>( + options) { + @Override + public ArrayList execute(final CloudTableClient client, final TableBatchOperation batch, + final OperationContext opContext) throws Exception { + final String batchID = String.format("batch_%s", UUID.randomUUID().toString()); + final String changeSet = String.format("changeset_%s", UUID.randomUUID().toString()); + + final HttpURLConnection request = TableRequest.batch(client.getEndpoint(), + options.getTimeoutIntervalInMs(), batchID, null, options, opContext); + + client.getCredentials().signRequestLite(request, -1L, opContext); + + MimeHelper.writeBatchToStream(request.getOutputStream(), tableName, batch, batchID, changeSet, + opContext); + + final InputStream streamRef = ExecutionEngine.getInputStream(request, opContext); + ArrayList responseParts = null; + try { + this.setResult(opContext.getLastResult()); + final String contentType = request.getHeaderField(Constants.HeaderConstants.CONTENT_TYPE); + + final String[] headerVals = contentType.split("multipart/mixed; boundary="); + if (headerVals == null || headerVals.length != 2) { + throw new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, + "An incorrect Content-type was returned from the server.", + Constants.HeaderConstants.HTTP_UNUSED_306, null, null); + } + + responseParts = MimeHelper.readBatchResponseStream(streamRef, headerVals[1], opContext); + } + finally { + streamRef.close(); + } + + ExecutionEngine.getResponseCode(this.getResult(), request, opContext); + + if (this.getResult().getStatusCode() != HttpURLConnection.HTTP_ACCEPTED) { + this.setNonExceptionedRetryableFailure(true); + return null; + } + + final ArrayList result = new ArrayList(); + for (int m = 0; m < batch.size(); m++) { + final TableOperation currOp = batch.get(m); + final MimePart currMimePart = responseParts.get(m); + + boolean failFlag = false; + + // Validate response + if (currOp.opType == TableOperationType.INSERT) { + if (this.getResult().getStatusCode() == HttpURLConnection.HTTP_CONFLICT) { + throw new TableServiceException(currMimePart.httpStatusCode, + currMimePart.httpStatusMessage, currOp, new StringReader(currMimePart.payload)); + } + + // Insert should receive created. + if (currMimePart.httpStatusCode != HttpURLConnection.HTTP_CREATED) { + failFlag = true; + } + } + else if (currOp.opType == TableOperationType.RETRIEVE) { + if (currMimePart.httpStatusCode == HttpURLConnection.HTTP_NOT_FOUND) { + // Empty result + result.add(new TableResult(currMimePart.httpStatusCode)); + return result; + } + + // Point query should receive ok. + if (currMimePart.httpStatusCode != HttpURLConnection.HTTP_OK) { + failFlag = true; + } + } + else { + // Validate response code. + if (currMimePart.httpStatusCode == HttpURLConnection.HTTP_NOT_FOUND) { + // Throw so as to not retry. + throw new TableServiceException(currMimePart.httpStatusCode, + currMimePart.httpStatusMessage, currOp, new StringReader(currMimePart.payload)); + } + + if (currMimePart.httpStatusCode != HttpURLConnection.HTTP_NO_CONTENT) { + // All others should receive no content. (delete, merge, upsert etc) + failFlag = true; + } + } + + if (failFlag) { + TableServiceException potentiallyRetryableException = new TableServiceException( + currMimePart.httpStatusCode, currMimePart.httpStatusMessage, currOp, new StringReader( + currMimePart.payload)); + potentiallyRetryableException.setRetryable(true); + throw potentiallyRetryableException; + } + + XMLStreamReader xmlr = null; + + if (currOp.opType == TableOperationType.INSERT || currOp.opType == TableOperationType.RETRIEVE) { + xmlr = Utility.createXMLStreamReaderFromReader(new StringReader(currMimePart.payload)); + } + + result.add(currOp.parseResponse(xmlr, currMimePart.httpStatusCode, + currMimePart.headers.get(TableConstants.HeaderConstants.ETAG), opContext)); + } + + return result; + } + }; + + return ExecutionEngine.executeWithRetry(client, this, impl, options.getRetryPolicyFactory(), opContext); + } + + /** + * Reserved for internal use. Removes all the table operations at indexes in the specified range from the batch + * operation ArrayList. + * + * @param fromIndex + * The inclusive lower bound of the range of {@link TableOperation} objects to remove from the batch + * operation ArrayList. + * @param toIndex + * The exclusive upper bound of the range of {@link TableOperation} objects to remove from the batch + * operation ArrayList. + */ + @Override + protected void removeRange(int fromIndex, int toIndex) { + super.removeRange(fromIndex, toIndex); + checkResetEntityLocks(); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableConstants.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableConstants.java new file mode 100644 index 0000000000000..48d3f3d56ea09 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableConstants.java @@ -0,0 +1,143 @@ +/** + * Copyright 2011 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.services.table.client; + +/** + * Holds the constants used for the Table Service. + */ +public final class TableConstants { + /** + * The constants used in HTML header fields for Table service requests. + */ + public static class HeaderConstants { + /** + * The ETag header field label. + */ + public static final String ETAG = "ETag"; + + /** + * The Accept header value to send. + */ + public static final String ACCEPT_TYPE = "application/atom+xml,application/xml"; + + /** + * The Content-Type header value to send for single operations. + */ + public static final String ATOMPUB_TYPE = "application/atom+xml"; + + /** + * The Content-Type header value to send for batch operations. + */ + public static final String MULTIPART_MIXED_FORMAT = "multipart/mixed; boundary=%s"; + + /** + * The DataServiceVersion header field label. + */ + public static final String DATA_SERVICE_VERSION = "DataServiceVersion"; + + /** + * The DataServiceVersion header value to send. + */ + public static final String DATA_SERVICE_VERSION_VALUE = "1.0;NetFx"; + + /** + * The MaxDataServiceVersion header field label. + */ + public static final String MAX_DATA_SERVICE_VERSION = "MaxDataServiceVersion"; + + /** + * The MaxDataServiceVersion header value to send. + */ + public static final String MAX_DATA_SERVICE_VERSION_VALUE = "2.0;NetFx"; + } + + /** + * Default client side timeout, in milliseconds, for table clients. + */ + public static final int TABLE_DEFAULT_TIMEOUT_IN_MS = 60 * 1000; + + /** + * Stores the header prefix for continuation information. + */ + public static final String TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION = "x-ms-continuation-"; + + /** + * Stores the header suffix for the next partition key. + */ + public static final String TABLE_SERVICE_NEXT_PARTITION_KEY = "NextPartitionKey"; + + /** + * Stores the header suffix for the next row key. + */ + public static final String TABLE_SERVICE_NEXT_ROW_KEY = "NextRowKey"; + + /** + * Stores the header suffix for the next marker. + */ + public static final String TABLE_SERVICE_NEXT_MARKER = "NextMarker"; + + /** + * Stores the table suffix for the next table name. + */ + public static final String TABLE_SERVICE_NEXT_TABLE_NAME = "NextTableName"; + + /** + * The name of the partition key property. + */ + public static final String PARTITION_KEY = "PartitionKey"; + + /** + * The name of the row key property. + */ + public static final String ROW_KEY = "RowKey"; + + /** + * The name of the Timestamp property. + */ + public static final String TIMESTAMP = "Timestamp"; + + /** + * The name of the special table used to store tables. + */ + public static final String TABLES_SERVICE_TABLES_NAME = "Tables"; + + /** + * The name of the property that stores the table name. + */ + public static final String TABLE_NAME = "TableName"; + + /** + * The query filter clause name. + */ + public static final String FILTER = "$filter"; + + /** + * The query top clause name. + */ + public static final String TOP = "$top"; + + /** + * The query select clause name. + */ + public static final String SELECT = "$select"; + + /** + * Private Default Constructor. + */ + private TableConstants() { + // No op + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableEntity.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableEntity.java new file mode 100644 index 0000000000000..85dc20a8c37bf --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableEntity.java @@ -0,0 +1,170 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.Date; +import java.util.HashMap; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * An interface required for table entity types. The {@link TableEntity} interface declares getter and setter methods + * for the common entity properties, and writeEntity and readEntity methods for serialization + * and deserialization of all entity properties using a property map. Create classes implementing {@link TableEntity} to + * customize property storage, retrieval, serialization and deserialization, and to provide additional custom logic for + * a table entity. + *

+ * The Storage client library includes two implementations of {@link TableEntity} that provide for simple property + * access and serialization: + *

+ * {@link DynamicTableEntity} implements {@link TableEntity} and provides a simple property map to store and retrieve + * properties. Use a {@link DynamicTableEntity} for simple access to entity properties when only a subset of properties + * are returned (for example, by a select clause in a query), or for when your query can return multiple entity types + * with different properties. You can also use this type to perform bulk table updates of heterogeneous entities without + * losing property information. + *

+ * {@link TableServiceEntity} is an implementation of {@link TableEntity} that uses reflection-based serialization and + * deserialization behavior in its writeEntity and readEntity methods. + * {@link TableServiceEntity}-derived classes with methods that follow a convention for types and naming are serialized + * and deserialized automatically. + *

+ * Any class that implements {@link TableEntity} can take advantage of the automatic reflection-based serialization and + * deserialization behavior in {@link TableServiceEntity} by invoking the static methods + * TableServiceEntity.readEntityWithReflection in readEntity and + * TableServiceEntity.writeEntityWithReflection in writeEntity. The class must provide methods + * that follow the type and naming convention to be serialized and deserialized automatically. When both a getter method + * and setter method are found for a given property name and data type, then the appropriate method is invoked + * automatically to serialize or deserialize the data. The reflection code looks for getter and setter methods in pairs + * of the form + *

+ * public type getPropertyName() { ... } + *

+ * and + *

+ * public void setPropertyName(type parameter) { ... } + *

+ * where PropertyName is a property name for the table entity, and type is a Java type compatible with + * the EDM data type of the property. See the table in the class description for {@link TableServiceEntity} for a map of + * property types to their Java equivalents. The {@link StoreAs} annotation may be applied with a name + * attribute to specify a property name for reflection on getter and setter methods that do not follow the property name + * convention. Method names and the name attribute of {@link StoreAs} annotations are case sensitive for + * matching property names with reflection. Use the {@link Ignore} annotation to prevent methods from being used by + * reflection for automatic serialization and deserialization. Note that the names "PartitionKey", "RowKey", + * "Timestamp", and "Etag" are reserved and will be ignored if set with the {@link StoreAs} annotation in a subclass + * that uses the reflection methods. + *

+ * + * @see TableServiceEntity + * @see DynamicTableEntity + */ +public interface TableEntity { + + /** + * Gets the Etag value for the entity. This value is used to determine if the table entity has changed since it was + * last read from Windows Azure storage. + * + * @return + * A String containing the Etag for the entity. + */ + public String getEtag(); + + /** + * Gets the PartitionKey value for the entity. + * + * @return + * A String containing the PartitionKey value for the entity. + */ + public String getPartitionKey(); + + /** + * Gets the RowKey value for the entity. + * + * @return + * A String containing the RowKey value for the entity. + */ + public String getRowKey(); + + /** + * Gets the Timestamp for the entity. + * + * @return + * A Date containing the Timestamp value for the entity. + */ + public Date getTimestamp(); + + /** + * Populates an instance of the object implementing {@link TableEntity} using the specified properties parameter, + * containing a map of String property names to {@link EntityProperty} data typed values. + * + * @param properties + * The java.util.HashMap of String to {@link EntityProperty} data typed values + * to use to populate the table entity instance. + * @param opContext + * An {@link OperationContext} object used to track the execution of the operation. + * @throws StorageException + * if an error occurs during the operation. + */ + public void readEntity(HashMap properties, OperationContext opContext) + throws StorageException; + + /** + * Sets the Etag for the entity. + * + * @param etag + * The String containing the Etag to set for the entity. + */ + public void setEtag(String etag); + + /** + * Sets the PartitionKey value for the entity. + * + * @param partitionKey + * The String containing the PartitionKey value to set for the entity. + */ + public void setPartitionKey(String partitionKey); + + /** + * Sets the RowKey value for the entity. + * + * @param rowKey + * The String containing the RowKey value to set for the entity. + */ + public void setRowKey(String rowKey); + + /** + * Sets the Timestamp value for the entity. + * + * @param timeStamp + * The Date containing the Timestamp value to set for the entity. + */ + public void setTimestamp(Date timeStamp); + + /** + * Returns a map of String property names to {@link EntityProperty} data typed values + * that represents the serialized content of the table entity instance. + * + * @param opContext + * An {@link OperationContext} object used to track the execution of the operation. + * @return + * The java.util.HashMap of String property names to {@link EntityProperty} data + * typed values representing the properties of the table entity. + * + * @throws StorageException + * if an error occurs during the operation. + */ + public HashMap writeEntity(OperationContext opContext) throws StorageException; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperation.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperation.java new file mode 100644 index 0000000000000..c9c72e0856ddc --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperation.java @@ -0,0 +1,699 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.text.ParseException; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * A class which represents a single table operation. + *

+ * Use the static factory methods to construct {@link TableOperation} instances for operations on tables that insert, + * update, merge, delete, replace or retrieve table entities. To execute a {@link TableOperation} instance, call the + * execute method on a {@link CloudTableClient} instance. A {@link TableOperation} may be executed directly + * or as part of a {@link TableBatchOperation}. If a {@link TableOperation} returns an entity result, it is stored in + * the corresponding {@link TableResult} returned by the execute method. + * + */ +public class TableOperation { + /** + * A static factory method returning a {@link TableOperation} instance to delete the specified entity from Windows + * Azure storage. To execute this {@link TableOperation} on a given table, call the + * {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance with the + * table name and the {@link TableOperation} as arguments. + * + * @param entity + * The object instance implementing {@link TableEntity} to associate with the operation. + * @return + * A new {@link TableOperation} instance to insert the table entity. + */ + public static TableOperation delete(final TableEntity entity) { + Utility.assertNotNull("Entity", entity); + Utility.assertNotNullOrEmpty("Entity Etag", entity.getEtag()); + return new TableOperation(entity, TableOperationType.DELETE); + } + + /** + * A static factory method returning a {@link TableOperation} instance to insert the specified entity into Windows + * Azure storage. To execute this {@link TableOperation} on a given table, call the + * {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance with the + * table name and the {@link TableOperation} as arguments. + * + * @param entity + * The object instance implementing {@link TableEntity} to associate with the operation. + * @return + * A new {@link TableOperation} instance to insert the table entity. + */ + public static TableOperation insert(final TableEntity entity) { + Utility.assertNotNull("Entity", entity); + return new TableOperation(entity, TableOperationType.INSERT); + } + + /** + * A static factory method returning a {@link TableOperation} instance to merge the specified entity into Windows + * Azure storage, or insert it if it does not exist. To execute this {@link TableOperation} on a given table, call + * the {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance with + * the table name and the {@link TableOperation} as arguments. + * + * @param entity + * The object instance implementing {@link TableEntity} to associate with the operation. + * @return + * A new {@link TableOperation} instance for inserting or merging the table entity. + */ + public static TableOperation insertOrMerge(final TableEntity entity) { + Utility.assertNotNull("Entity", entity); + return new TableOperation(entity, TableOperationType.INSERT_OR_MERGE); + } + + /** + * A static factory method returning a {@link TableOperation} instance to replace the specified entity in Windows + * Azure storage, or insert it if it does not exist. To execute this {@link TableOperation} on a given table, call + * the {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance with + * the table name and the {@link TableOperation} as arguments. + * + * @param entity + * The object instance implementing {@link TableEntity} to associate with the operation. + * @return + * A new {@link TableOperation} instance for inserting or replacing the table entity. + */ + public static TableOperation insertOrReplace(final TableEntity entity) { + Utility.assertNotNull("Entity", entity); + return new TableOperation(entity, TableOperationType.INSERT_OR_REPLACE); + } + + /** + * A static factory method returning a {@link TableOperation} instance to merge the specified table entity into + * Windows Azure storage. To execute this {@link TableOperation} on a given table, call the + * {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance with the + * table name and the {@link TableOperation} as arguments. + * + * @param entity + * The object instance implementing {@link TableEntity} to associate with the operation. + * @return + * A new {@link TableOperation} instance for merging the table entity. + */ + public static TableOperation merge(final TableEntity entity) { + Utility.assertNotNull("Entity", entity); + Utility.assertNotNullOrEmpty("Entity Etag", entity.getEtag()); + return new TableOperation(entity, TableOperationType.MERGE); + } + + /** + * A static factory method returning a {@link TableOperation} instance to retrieve the specified table entity and + * return it as the specified type. To execute this {@link TableOperation} on a given table, call the + * {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance with the + * table name and the {@link TableOperation} as arguments. + * + * @param partitionKey + * A String containing the PartitionKey value for the entity to retrieve. + * @param rowKey + * A String containing the RowKey value for the entity to retrieve. + * @param clazzType + * The class type of the table entity object to retrieve. + * @return + * A new {@link TableOperation} instance for retrieving the table entity. + */ + public static TableOperation retrieve(final String partitionKey, final String rowKey, + final Class clazzType) { + final QueryTableOperation retOp = new QueryTableOperation(partitionKey, rowKey); + retOp.setClazzType(clazzType); + return retOp; + } + + /** + * A static factory method returning a {@link TableOperation} instance to retrieve the specified table entity and + * return a projection of it using the specified resolver. To execute this {@link TableOperation} on a given table, + * call the {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance + * with the table name and the {@link TableOperation} as arguments. + * + * @param partitionKey + * A String containing the PartitionKey value for the entity to retrieve. + * @param rowKey + * A String containing the RowKey value for the entity to retrieve. + * @param resolver + * The implementation of {@link EntityResolver} to use to project the result entity as type T. + * @return + * A new {@link TableOperation} instance for retrieving the table entity. + */ + public static TableOperation retrieve(final String partitionKey, final String rowKey, + final EntityResolver resolver) { + final QueryTableOperation retOp = new QueryTableOperation(partitionKey, rowKey); + retOp.setResolver(resolver); + return retOp; + } + + /** + * A static factory method returning a {@link TableOperation} instance to replace the specified table entity. To + * execute this {@link TableOperation} on a given table, call the + * {@link CloudTableClient#execute(String, TableOperation)} method on a {@link CloudTableClient} instance with the + * table name and the {@link TableOperation} as arguments. + * + * @param entity + * The object instance implementing {@link TableEntity} to associate with the operation. + * @return + * A new {@link TableOperation} instance for replacing the table entity. + */ + public static TableOperation replace(final TableEntity entity) { + Utility.assertNotNullOrEmpty("Entity Etag", entity.getEtag()); + return new TableOperation(entity, TableOperationType.REPLACE); + } + + /** + * The table entity instance associated with the operation. + */ + TableEntity entity; + + /** + * The {@link TableOperationType} enumeration value for the operation type. + */ + TableOperationType opType = null; + + /** + * Nullary Default Constructor. + */ + protected TableOperation() { + // empty ctor + } + + /** + * Reserved for internal use. Constructs a {@link TableOperation} with the specified table entity and operation + * type. + * + * @param entity + * The object instance implementing {@link TableEntity} to associate with the operation. + * @param opType + * The {@link TableOperationType} enumeration value for the operation type. + */ + protected TableOperation(final TableEntity entity, final TableOperationType opType) { + this.entity = entity; + this.opType = opType; + } + + /** + * Reserved for internal use. Performs a delete operation on the specified table, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * This method will invoke the Delete + * 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 String 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 impl = new StorageOperation( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + + final HttpURLConnection request = TableRequest.delete(client.getEndpoint(), tableName, + generateRequestIdentity(isTableEntry, tableIdentity), 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. Performs an insert operation on the specified table, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * 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 String 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 impl = new StorageOperation( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + final HttpURLConnection request = TableRequest.insert(client.getEndpoint(), tableName, + generateRequestIdentity(isTableEntry, tableIdentity), + 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. Perform a merge operation on the specified table, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * 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 String 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 impl = new StorageOperation( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + + final HttpURLConnection request = TableRequest.merge(client.getEndpoint(), tableName, + generateRequestIdentity(false, null), 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); + } + + /** + * Reserved for internal use. Perform an update operation on the specified table, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * 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 String 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 performUpdate(final CloudTableClient client, final String tableName, + final TableRequestOptions options, final OperationContext opContext) throws StorageException { + Utility.assertNotNullOrEmpty("Update requires a valid ETag", this.getEntity().getEtag()); + Utility.assertNotNullOrEmpty("Update requires a valid PartitionKey", this.getEntity().getPartitionKey()); + Utility.assertNotNullOrEmpty("Update requires a valid RowKey", this.getEntity().getRowKey()); + final StorageOperation impl = new StorageOperation( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + + final HttpURLConnection request = TableRequest.update(client.getEndpoint(), tableName, + generateRequestIdentity(false, null), 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); + } + + /** + * Reserved for internal use. Execute this table operation on the specified table, using the specified + * {@link TableRequestOptions} and {@link OperationContext}. + *

+ * 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 String 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. Generates the request identity, consisting of the specified entry name, or the + * PartitionKey and RowKey pair from the operation, to identify the operation target. + * + * @param isSingleIndexEntry + * Pass true to use the specified entryName parameter, or false to + * use PartitionKey and RowKey values from the operation as the request identity. + * @param entryName + * The entry name to use as the request identity if the isSingleIndexEntry parameter is + * true. + * @return + * A String containing the formatted request identity string. + */ + protected String generateRequestIdentity(boolean isSingleIndexEntry, final String entryName) { + if (isSingleIndexEntry) { + return String.format("'%s'", entryName); + } + + if (this.opType == TableOperationType.INSERT) { + return Constants.EMPTY_STRING; + } + else { + String pk = null; + String rk = null; + + if (this.opType == TableOperationType.RETRIEVE) { + final QueryTableOperation qOp = (QueryTableOperation) this; + pk = qOp.getPartitionKey(); + rk = qOp.getRowKey(); + } + else { + pk = this.getEntity().getPartitionKey(); + rk = this.getEntity().getRowKey(); + } + + return String.format("%s='%s',%s='%s'", TableConstants.PARTITION_KEY, pk, TableConstants.ROW_KEY, rk); + } + } + + /** + * Reserved for internal use. Generates the request identity string for the specified table. The request identity + * string combines the table name with the PartitionKey and RowKey from the operation to identify specific table + * entities. + * + * @param tableName + * A String containing the name of the table. + * @return + * A String containing the formatted request identity string for the specified table. + */ + protected String generateRequestIdentityWithTable(final String tableName) { + return String.format("/%s(%s)", tableName, generateRequestIdentity(false, null)); + } + + /** + * Reserved for internal use. Gets the table entity associated with this operation. + * + * @return + * The {@link TableEntity} instance associated with this operation. + */ + protected synchronized final TableEntity getEntity() { + return this.entity; + } + + /** + * Reserved for internal use. Gets the operation type for this operation. + * + * @return the opType + * The {@link TableOperationType} instance associated with this operation. + */ + protected synchronized final TableOperationType getOperationType() { + return this.opType; + } + + /** + * Reserved for internal use. Parses the table operation response into a {@link TableResult} to return. + * + * @param xmlr + * An XMLStreamReader containing the response to an insert operation. + * @param httpStatusCode + * The HTTP status code returned from the operation request. + * @param etagFromHeader + * The String containing the Etag returned with the operation response. + * @param opContext + * An {@link OperationContext} object that represents the context for the current operation. + * + * @return + * The {@link TableResult} representing the result of the operation. + * + * @throws XMLStreamException + * if an error occurs accessing the XMLStreamReader. + * @throws ParseException + * if an error occurs in parsing the response. + * @throws InstantiationException + * if an error occurs in object construction. + * @throws IllegalAccessException + * if an error occurs in reflection on an object type. + * @throws StorageException + * if an error occurs in the storage operation. + */ + protected TableResult parseResponse(final XMLStreamReader xmlr, final int httpStatusCode, + final String etagFromHeader, final OperationContext opContext) throws XMLStreamException, ParseException, + InstantiationException, IllegalAccessException, StorageException { + TableResult resObj = null; + if (this.opType == TableOperationType.INSERT) { + // Sending null for class type and resolver will ignore parsing the return payload. + resObj = AtomPubParser.parseSingleOpResponse(xmlr, httpStatusCode, null, null, opContext); + resObj.updateResultObject(this.getEntity()); + } + else { + resObj = new TableResult(httpStatusCode); + resObj.setResult(this.getEntity()); + + if (this.opType != TableOperationType.DELETE) { + this.getEntity().setEtag(etagFromHeader); + } + } + + return resObj; + } + + /** + * Reserved for internal use. Sets the {@link TableEntity} instance for the table operation. + * + * @param entity + * The {@link TableEntity} instance to set. + */ + protected synchronized final void setEntity(final TableEntity entity) { + this.entity = entity; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperationType.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperationType.java new file mode 100644 index 0000000000000..a1ceccabd3320 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperationType.java @@ -0,0 +1,43 @@ +/** + * Copyright 2011 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.services.table.client; + +/** + * Reserved for internal use. An enumeration type which represents the type of operation a {@link TableOperation} + * represents. + */ +enum TableOperationType { + INSERT, DELETE, REPLACE, RETRIEVE, MERGE, INSERT_OR_REPLACE, INSERT_OR_MERGE; + + /** + * Gets the {@link TableUpdateType} associated the operation type, if applicable. Applies to + * {@link #INSERT_OR_MERGE} and {@link #INSERT_OR_REPLACE} values. + * + * @return + * The applicable {@link TableUpdateType}, or null. + */ + public TableUpdateType getUpdateType() { + if (this == INSERT_OR_MERGE) { + return TableUpdateType.MERGE; + } + else if (this == INSERT_OR_REPLACE) { + return TableUpdateType.REPLACE; + } + else { + return null; + } + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableQuery.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableQuery.java new file mode 100644 index 0000000000000..a54279ae0ce17 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableQuery.java @@ -0,0 +1,773 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.Date; +import java.util.Formatter; +import java.util.UUID; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.UriQueryBuilder; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * A class which represents a query against a specified table. A {@link TableQuery} instance aggregates the query + * parameters to use when the query is executed. One of the execute or executeSegmented + * methods of {@link CloudTableClient} must be called to execute the query. The parameters are encoded and passed to the + * server when the table query is executed. + *

+ * To create a table query with fluent syntax, the {@link #from} static factory method and the {@link #where}, + * {@link #select}, and {@link #take} mutator methods each return a reference to the object which can be chained into a + * single expression. Use the {@link TableQuery#from(String, Class)} static class factory method to create a + * TableQuery instance that executes on the named table with entities of the specified {@link TableEntity} + * implementing type. Use the {@link #where} method to specify a filter expression for the entities returned. Use the + * {@link #select} method to specify the table entity properties to return. Use the {@link #take} method to limit the + * number of entities returned by the query. Note that nothing prevents calling these methods more than once on a + * TableQuery, so the values saved in the TableQuery will be the last encountered in order of + * execution. + *

+ * As an example, you could construct a table query using fluent syntax: + *

+ * TableQuery<TableServiceEntity> myQuery = TableQuery.from("Products", DynamicTableEntity.class)
+ *     .where("(PartitionKey eq 'ProductsMNO') and (RowKey ge 'Napkin')")
+ *     .take(25)
+ *     .select(new String[] {"InventoryCount"});
+ *

+ * This example creates a query on the "Products" table for all entities where the PartitionKey value is "ProductsMNO" + * and the RowKey value is greater than or equal to "Napkin" and requests the first 25 matching entities, selecting only + * the common properties and the property named "InventoryCount", and returns them as {@link DynamicTableEntity} + * objects. + *

+ * Filter expressions for use with the {@link #where} method or {@link #setFilterString} method can be created using + * fluent syntax with the overloaded {@link #generateFilterCondition} methods and {@link #combineFilters} method, using + * the comparison operators defined in {@link QueryComparisons} and the logical operators defined in {@link Operators}. + * Note that the first operand in a filter comparison must be a property name, and the second operand must evaluate to a + * constant. The PartitionKey and RowKey property values are String types for comparison purposes. + *

+ * The values that may be used in table queries are explained in more detail in the MSDN topic Querying Tables and Entities, but note + * that the space characters within values do not need to be URL-encoded, as this will be done when the query is + * executed. + *

+ * The {@link TableQuery#TableQuery(String, Class)} constructor and {@link TableQuery#from(String, Class)} static + * factory methods require a class type which implements {@link TableEntity} and contains a nullary constructor. If the + * query will be executed using an {@link EntityResolver}, the caller may specify {@link TableServiceEntity} + * .class as the class type. + * + * @param + * A class type which implements {@link TableEntity} and contains a nullary constructor. Note: when using an + * inner class to define the class type, mark the class as static. + */ +public class TableQuery { + /** + * A static class that maps identifiers to filter expression operators. + */ + public static class Operators { + /** + * And + */ + public static final String AND = "and"; + + /** + * Not + */ + public static final String NOT = "not"; + + /** + * Or + */ + public static final String OR = "or"; + } + + /** + * A static class that maps identifiers to filter property comparison operators. + */ + public static class QueryComparisons { + /** + * Equal + */ + public static final String EQUAL = "eq"; + + /** + * Not Equal + */ + public static final String NOT_EQUAL = "ne"; + + /** + * Greater Than + */ + public static final String GREATER_THAN = "gt"; + + /** + * Greater Than Or Equal + */ + public static final String GREATER_THAN_OR_EQUAL = "ge"; + + /** + * Less Than + */ + public static final String LESS_THAN = "lt"; + + /** + * Less Than Or Equal + */ + public static final String LESS_THAN_OR_EQUAL = "le"; + } + + /** + * A static factory method that constructs a {@link TableQuery} instance and defines its source table and + * table entity type. The method returns the {@link TableQuery} instance reference, allowing additional methods to + * be chained to modify the query. + *

+ * The created {@link TableQuery} instance is specialized for table entities of the specified class type T, using + * the table with the specified name as data source. Callers may specify {@link TableServiceEntity} + * .class as the class type parameter if no more specialized type is required. + * + * @param tablename + * A String containing the name of the source table to query. + * @param clazzType + * The java.lang.Class of the class T implementing the {@link TableEntity} + * interface that + * represents the table entity type for the query. + * + * @return + * The {@link TableQuery} instance with the source table name and entity type specialization set. + */ + public static TableQuery from(final String tablename, final Class clazzType) { + return new TableQuery(tablename, clazzType); + } + + /** + * Generates a property filter condition string for a boolean value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as a + * boolean, as in the following example: + *

+ * String condition = generateFilterCondition("BooleanProperty", QueryComparisons.EQUAL, false); + *

+ * This statement sets condition to the following value: + *

+ * BooleanProperty eq false + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A boolean containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final boolean value) { + return generateFilterCondition(propertyName, operation, value ? Constants.TRUE : Constants.FALSE, + EdmType.BOOLEAN); + } + + /** + * Generates a property filter condition string for a byte[] value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as a + * binary value, as in the following example: + *

+ * String condition = generateFilterCondition("ByteArray", QueryComparisons.EQUAL, new byte[] {0x01, 0x0f}); + *

+ * This statement sets condition to the following value: + *

+ * ByteArray eq X'010f' + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A byte[] containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final byte[] value) { + StringBuilder sb = new StringBuilder(); + Formatter formatter = new Formatter(sb); + for (byte b : value) { + formatter.format("%02x", b); + } + + return generateFilterCondition(propertyName, operation, sb.toString(), EdmType.BINARY); + } + + /** + * Generates a property filter condition string for a Byte[] value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as a + * binary value, as in the following example: + *

+ * String condition = generateFilterCondition("ByteArray", QueryComparisons.EQUAL, new Byte[] {0x01, 0xfe}); + *

+ * This statement sets condition to the following value: + *

+ * ByteArray eq X'01fe' + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A Byte[] containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final Byte[] value) { + StringBuilder sb = new StringBuilder(); + Formatter formatter = new Formatter(sb); + for (byte b : value) { + formatter.format("%02x", b); + } + + return generateFilterCondition(propertyName, operation, sb.toString(), EdmType.BINARY); + } + + /** + * Generates a property filter condition string for a Date value. Creates a formatted string to use in + * a filter expression that uses the specified operation to compare the property with the value, formatted as a + * datetime value, as in the following example: + *

+ * String condition = generateFilterCondition("FutureDate", QueryComparisons.GREATER_THAN, new Date()); + *

+ * This statement sets condition to something like the following value: + *

+ * FutureDate gt datetime'2013-01-31T09:00:00' + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A Date containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final Date value) { + return generateFilterCondition(propertyName, operation, + Utility.getTimeByZoneAndFormat(value, Utility.UTC_ZONE, Utility.ISO8061_LONG_PATTERN), + EdmType.DATE_TIME); + } + + /** + * Generates a property filter condition string for a double value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as + * a double value, as in the following example: + *

+ * String condition = generateFilterCondition("Circumference", QueryComparisons.EQUAL, 2 * 3.141592); + *

+ * This statement sets condition to the following value: + *

+ * Circumference eq 6.283184 + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A double containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final double value) { + return generateFilterCondition(propertyName, operation, Double.toString(value), EdmType.DOUBLE); + } + + /** + * Generates a property filter condition string for an int value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as + * a numeric value, as in the following example: + *

+ * String condition = generateFilterCondition("Population", QueryComparisons.GREATER_THAN, 1000); + *

+ * This statement sets condition to the following value: + *

+ * Population gt 1000 + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * An int containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final int value) { + return generateFilterCondition(propertyName, operation, Integer.toString(value), EdmType.INT32); + } + + /** + * Generates a property filter condition string for a long value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as + * a numeric value, as in the following example: + *

+ * String condition = generateFilterCondition("StellarMass", QueryComparisons.GREATER_THAN, 7000000000L); + *

+ * This statement sets condition to the following value: + *

+ * StellarMass gt 7000000000 + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A long containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final long value) { + return generateFilterCondition(propertyName, operation, Long.toString(value), EdmType.INT64); + } + + /** + * Generates a property filter condition string for a String value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as + * a string value, as in the following example: + *

+ * String condition = generateFilterCondition("Platform", QueryComparisons.EQUAL, "Windows Azure"); + *

+ * This statement sets condition to the following value: + *

+ * Platform eq 'Windows Azure' + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A String containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final String value) { + return generateFilterCondition(propertyName, operation, value, EdmType.STRING); + } + + /** + * Generates a property filter condition string. Creates a formatted string to use in a filter expression that uses + * the specified operation to compare the property with the value, formatted as the specified {@link EdmType}. + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A String containing the value to compare with the property. + * @param edmType + * The {@link EdmType} to format the value as. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, String value, EdmType edmType) { + String valueOperand = null; + + if (edmType == EdmType.BOOLEAN || edmType == EdmType.DOUBLE || edmType == EdmType.INT32 + || edmType == EdmType.INT64) { + valueOperand = value; + } + else if (edmType == EdmType.DATE_TIME) { + valueOperand = String.format("datetime'%s'", value); + } + else if (edmType == EdmType.GUID) { + valueOperand = String.format("guid'%s'", value); + } + else if (edmType == EdmType.BINARY) { + valueOperand = String.format("X'%s'", value); + } + else { + valueOperand = String.format("'%s'", value); + } + + return String.format("%s %s %s", propertyName, operation, valueOperand); + } + + /** + * Generates a property filter condition string for a UUID value. Creates a formatted string to use + * in a filter expression that uses the specified operation to compare the property with the value, formatted as + * a UUID value, as in the following example: + *

+ * String condition = generateFilterCondition("Identity", QueryComparisons.EQUAL, UUID.fromString( + * "c9da6455-213d-42c9-9a79-3e9149a57833")); + *

+ * This statement sets condition to the following value: + *

+ * Identity eq guid'c9da6455-213d-42c9-9a79-3e9149a57833' + * + * @param propertyName + * A String containing the name of the property to compare. + * @param operation + * A String containing the comparison operator to use. + * @param value + * A UUID containing the value to compare with the property. + * @return + * A String containing the formatted filter condition. + */ + public static String generateFilterCondition(String propertyName, String operation, final UUID value) { + return generateFilterCondition(propertyName, operation, value.toString(), EdmType.GUID); + } + + /** + * Creates a filter condition using the specified logical operator on two filter conditions. + * + * @param filterA + * A String containing the first formatted filter condition. + * @param operator + * A String containing Operators.AND or Operators.OR. + * @param filterB + * A String containing the first formatted filter condition. + * @return + * A String containing the combined filter expression. + */ + public static String combineFilters(String filterA, String operator, String filterB) { + return String.format("(%s) %s (%s)", filterA, operator, filterB); + } + + private Class clazzType = null; + private String sourceTableName = null; + private String[] columns = null; + private Integer takeCount; + private String filterString = null; + + /** + * Initializes an empty {@link TableQuery} instance. This table query cannot be executed without + * setting a source table and a table entity type. + */ + public TableQuery() { + // empty ctor + } + + /** + * Initializes a {@link TableQuery} with the specified source table and table entity type. Callers may specify + * {@link TableServiceEntity}.class as the class type parameter if no more specialized type is + * required. + * + * @param tablename + * A String containing the name of the source table to query. + * @param clazzType + * The java.lang.Class of the class T that represents the table entity type for + * the query. Class T must be a type that implements {@link TableEntity} and has a nullary + * constructor, + */ + public TableQuery(final String tableName, final Class clazzType) { + this.setSourceTableName(tableName); + this.setClazzType(clazzType); + } + + /** + * Gets the class type of the table entities returned by the query. + * + * @return + * The java.lang.Class of the class T that represents the table entity type for + * the query. + */ + public Class getClazzType() { + return this.clazzType; + } + + /** + * Gets an array of the table entity property names specified in the table query. All properties in the table are + * returned by default if no property names are specified with a select clause in the table query. The table entity + * properties to return may be specified with a call to the {@link #setColumns} or {@link #select} methods with a + * array of property names as parameter. + *

+ * Note that the system properties PartitionKey, RowKey, and Timestamp are + * automatically requested from the table service whether specified in the {@link TableQuery} or not. + * + * @return + * An array of String objects containing the property names of the table entity properties to + * return in the query. + */ + public String[] getColumns() { + return this.columns; + } + + /** + * Gets the class type of the table entities returned by the query. + * + * @return + * The java.lang.Class of the class T implementing the {@link TableEntity} + * interface that + * represents the table entity type for the query. + */ + public Class getEntityClass() { + return this.clazzType; + } + + /** + * Gets the filter expression specified in the table query. All entities in the table are returned by + * default if no filter expression is specified in the table query. A filter for the entities to return may be + * specified with a call to the {@link #setFilterString} or {@link #where} methods. + * + * @return + * A String containing the filter expression used in the query. + */ + public String getFilterString() { + return this.filterString; + } + + /** + * Gets the name of the source table specified in the table query. + * + * @return + * A String containing the name of the source table used in the query. + */ + public String getSourceTableName() { + return this.sourceTableName; + } + + /** + * Gets the number of entities the query returns specified in the table query. If this value is not + * specified in a table query, a maximum of 1,000 entries will be returned. The number of entities to return may be + * specified with a call to the {@link #setTakeCount} or {@link #take} methods. + *

+ * If the value returned by getTakeCount is greater than 1,000, the query will throw a + * {@link StorageException} when executed. + * + * @return + * The maximum number of entities for the table query to return. + */ + public Integer getTakeCount() { + return this.takeCount; + } + + /** + * Defines the property names of the table entity properties to return when the table query is executed. The + * select clause is optional on a table query, used to limit the table properties returned from the + * server. By default, a query will return all properties from the table entity. + *

+ * Note that the system properties PartitionKey, RowKey, and Timestamp are + * automatically requested from the table service whether specified in the {@link TableQuery} or not. + * + * @param columns + * An array of String objects containing the property names of the table entity properties + * to return when the query is executed. + * + * @return + * A reference to the {@link TableQuery} instance with the table entity properties to return set. + */ + public TableQuery select(final String[] columns) { + this.setColumns(columns); + return this; + } + + /** + * Sets the class type of the table entities returned by the query. A class type is required to execute a table + * query. + *

+ * Callers may specify {@link TableServiceEntity}.class as the class type parameter if no more + * specialized type is required. + * + * @param clazzType + * The java.lang.Class of the class T that represents the table entity type for + * the query. Class T must be a type that implements {@link TableEntity} and has a nullary + * constructor, + */ + public void setClazzType(final Class clazzType) { + Utility.assertNotNull("Query requires a valid class type.", clazzType); + Utility.checkNullaryCtor(clazzType); + this.clazzType = clazzType; + } + + /** + * Sets the property names of the table entity properties to return when the table query is executed. By default, a + * query will return all properties from the table entity. + *

+ * Note that the system properties PartitionKey, RowKey, and Timestamp are + * automatically requested from the table service whether specified in the {@link TableQuery} or not. + * + * @param columns + * An array of String objects containing the property names of the table entity properties + * to return when the query is executed. + */ + public void setColumns(final String[] columns) { + this.columns = columns; + } + + /** + * Sets the filter expression to use in the table query. A filter expression is optional; by default a table query + * will return all entities in the table. + *

+ * Filter expressions for use with the {@link #setFilterString} method can be created using fluent syntax with the + * overloaded {@link #generateFilterCondition} methods and {@link #combineFilters} method, using the comparison + * operators defined in {@link QueryComparisons} and the logical operators defined in {@link Operators}. Note that + * the first operand in a filter comparison must be a property name, and the second operand must evaluate to a + * constant. The PartitionKey and RowKey property values are String types for comparison purposes. For + * example, to query all entities with a PartitionKey value of "AccessLogs" on table query myQuery: + *

+ *     myQuery.setFilterString("PartitionKey eq 'AccessLogs'"); + *

+ * The values that may be used in table queries are explained in more detail in the MSDN topic + * + * Querying Tables and Entities, + * but note that the space characters within values do not need to be URL-encoded, as this will be done when the + * query is executed. + *

+ * Note that no more than 15 discrete comparisons are permitted within a filter string. + * + * @param filterString + * A String containing the filter expression to use in the query. + */ + public void setFilterString(final String filterString) { + Utility.assertNotNullOrEmpty("filterString", filterString); + this.filterString = filterString; + } + + /** + * Sets the name of the source table for the table query. A table query must have a source table to be executed. + * + * @param sourceTableName + * A String containing the name of the source table to use in the query. + */ + public void setSourceTableName(final String sourceTableName) { + Utility.assertNotNullOrEmpty("tableName", sourceTableName); + this.sourceTableName = sourceTableName; + } + + /** + * Sets the upper bound for the number of entities the query returns. If this value is not specified in a table + * query, by default a maximum of 1,000 entries will be returned. + *

+ * If the value specified for the takeCount parameter is greater than 1,000, the query will throw a + * {@link StorageException} when executed. + * + * @param takeCount + * The maximum number of entities for the table query to return. + */ + public void setTakeCount(final Integer takeCount) { + if (takeCount != null && takeCount <= 0) { + throw new IllegalArgumentException("Take count must be positive and greater than 0."); + } + + this.takeCount = takeCount; + } + + /** + * Defines the upper bound for the number of entities the query returns. If this value is not specified in a table + * query, by default a maximum of 1,000 entries will be returned. + *

+ * If the value specified for the take parameter is greater than 1,000, the query will throw a + * {@link StorageException} when executed. + * + * @param take + * The maximum number of entities for the table query to return. + * + * @return + * A reference to the {@link TableQuery} instance with the number of entities to return set. + */ + public TableQuery take(final Integer take) { + if (take != null) { + this.setTakeCount(take); + } + return this; + } + + /** + * Defines a filter expression for the table query. Only entities that satisfy the specified filter expression will + * be returned by the query. Setting a filter expression is optional; by default, all entities in the table are + * returned if no filter expression is specified in the table query. + *

+ * Filter expressions for use with the {@link #where} method can be created using fluent syntax with the overloaded + * {@link #generateFilterCondition} methods and {@link #combineFilters} method, using the comparison operators + * defined in {@link QueryComparisons} and the logical operators defined in {@link Operators}. Note that the first + * operand in a filter comparison must be a property name, and the second operand must evaluate to a constant. The + * PartitionKey and RowKey property values are String types for comparison purposes. For example, to + * query all entities with a PartitionKey value of "AccessLogs" on table query myQuery: + *

+ *     myQuery = myQuery.where("PartitionKey eq 'AccessLogs'"); + *

+ * The values that may be used in table queries are explained in more detail in the MSDN topic + * + * Querying Tables and Entities, + * but note that the space characters within values do not need to be URL-encoded, as this will be done when the + * query is executed. + *

+ * Note that no more than 15 discrete comparisons are permitted within a filter string. + * + * @param filter + * A String containing the filter expression to apply to the table query. + * @return + * A reference to the {@link TableQuery} instance with the filter on entities to return set. + */ + public TableQuery where(final String filter) { + this.setFilterString(filter); + return this; + } + + /** + * Reserved for internal use. Creates a {@link UriQueryBuilder} object representing the table query. + * + * @return + * A {@link UriQueryBuilder} object representing the table query. + * @throws StorageException + * if an error occurs in adding or encoding the query parameters. + */ + protected UriQueryBuilder generateQueryBuilder() throws StorageException { + final UriQueryBuilder builder = new UriQueryBuilder(); + if (!Utility.isNullOrEmpty(this.filterString)) { + builder.add(TableConstants.FILTER, this.filterString); + } + + if (this.takeCount != null) { + builder.add(TableConstants.TOP, this.takeCount.toString()); + } + + if (this.columns != null && this.columns.length > 0) { + final StringBuilder colBuilder = new StringBuilder(); + + boolean foundRk = false; + boolean foundPk = false; + boolean roundTs = false; + + for (int m = 0; m < this.columns.length; m++) { + if (TableConstants.ROW_KEY.equals(this.columns[m])) { + foundRk = true; + } + else if (TableConstants.PARTITION_KEY.equals(this.columns[m])) { + foundPk = true; + } + else if (TableConstants.TIMESTAMP.equals(this.columns[m])) { + roundTs = true; + } + + colBuilder.append(this.columns[m]); + if (m < this.columns.length - 1) { + colBuilder.append(","); + } + } + + if (!foundPk) { + colBuilder.append(","); + colBuilder.append(TableConstants.PARTITION_KEY); + } + + if (!foundRk) { + colBuilder.append(","); + colBuilder.append(TableConstants.ROW_KEY); + } + + if (!roundTs) { + colBuilder.append(","); + colBuilder.append(TableConstants.TIMESTAMP); + } + + builder.add(TableConstants.SELECT, colBuilder.toString()); + } + + return builder; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequest.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequest.java new file mode 100644 index 0000000000000..4642bec1b298a --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequest.java @@ -0,0 +1,432 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.ResultContinuation; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.PathUtility; +import com.microsoft.windowsazure.services.core.storage.utils.UriQueryBuilder; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.BaseRequest; + +/** + * Reserved for internal use. A class used to generate requests for Table objects. + */ +final class TableRequest { + /** + * Reserved for internal use. Adds continuation token values to the specified query builder, if set. + * + * @param builder + * The {@link UriQueryBuilder} object to apply the continuation token properties to. + * @param continuationToken + * The {@link ResultContinuation} object containing the continuation token values to apply to the query + * builder. Specify null if no continuation token values are set. + * + * @throws StorageException + * if an error occurs in accessing the query builder or continuation token. + */ + protected static void applyContinuationToQueryBuilder(final UriQueryBuilder builder, + final ResultContinuation continuationToken) throws StorageException { + if (continuationToken != null) { + if (continuationToken.getNextPartitionKey() != null) { + builder.add(TableConstants.TABLE_SERVICE_NEXT_PARTITION_KEY, continuationToken.getNextPartitionKey()); + } + + if (continuationToken.getNextRowKey() != null) { + builder.add(TableConstants.TABLE_SERVICE_NEXT_ROW_KEY, continuationToken.getNextRowKey()); + } + + if (continuationToken.getNextTableName() != null) { + builder.add(TableConstants.TABLE_SERVICE_NEXT_TABLE_NAME, continuationToken.getNextTableName()); + } + } + } + + /** + * Reserved for internal use. Constructs an HttpURLConnection to perform a table batch operation. + * + * @param rootUri + * A java.net.URI containing an absolute URI to the resource. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param batchID + * The String containing the batch identifier. + * @param queryBuilder + * The {@link UriQueryBuilder} for the operation. + * @param tableOptions + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. This parameter is unused. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * An HttpURLConnection to use to perform the operation. + * + * @throws IOException + * if there is an error opening the connection. + * @throws URISyntaxException + * if the resource URI is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + protected static HttpURLConnection batch(final URI rootUri, final int timeoutInMs, final String batchID, + final UriQueryBuilder queryBuilder, final TableRequestOptions tableOptions, final OperationContext opContext) + throws IOException, URISyntaxException, StorageException { + final URI queryUri = PathUtility.appendPathToUri(rootUri, "$batch"); + + final HttpURLConnection retConnection = BaseRequest.createURLConnection(queryUri, timeoutInMs, queryBuilder, + opContext); + // Note : accept behavior, java by default sends Accept behavior + // as text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 + retConnection.setRequestProperty(Constants.HeaderConstants.ACCEPT, TableConstants.HeaderConstants.ACCEPT_TYPE); + retConnection.setRequestProperty(Constants.HeaderConstants.ACCEPT_CHARSET, "UTF8"); + retConnection.setRequestProperty(TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION, + TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION_VALUE); + + retConnection.setRequestProperty(Constants.HeaderConstants.CONTENT_TYPE, + String.format(TableConstants.HeaderConstants.MULTIPART_MIXED_FORMAT, batchID)); + + retConnection.setRequestMethod("POST"); + retConnection.setDoOutput(true); + return retConnection; + } + + /** + * Reserved for internal use. Constructs the core HttpURLConnection to perform an operation. + * + * @param rootUri + * A java.net.URI containing an absolute URI to the resource. + * @param tableName + * The name of the table. + * @param identity + * The identity of the entity, to pass in the Service Managment REST operation URI as + * tableName(identity). If null, only the tableName + * value will be passed. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param queryBuilder + * The UriQueryBuilder for the request. + * @param requestMethod + * The HTTP request method to set. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. This parameter is unused. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. + * + * @return + * An HttpURLConnection to use to perform the operation. + * + * @throws IOException + * if there is an error opening the connection. + * @throws URISyntaxException + * if the resource URI is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + protected static HttpURLConnection coreCreate(final URI rootUri, final String tableName, final String eTag, + final String identity, final int timeoutInMs, final UriQueryBuilder queryBuilder, + final String requestMethod, final TableRequestOptions tableOptions, final OperationContext opContext) + throws IOException, URISyntaxException, StorageException { + + URI queryUri = null; + + // Do point query / delete etc. + if (!Utility.isNullOrEmpty(identity)) { + queryUri = PathUtility.appendPathToUri(rootUri, tableName.concat(String.format("(%s)", identity))); + } + else { + queryUri = PathUtility.appendPathToUri(rootUri, tableName); + } + + final HttpURLConnection retConnection = BaseRequest.createURLConnection(queryUri, timeoutInMs, queryBuilder, + opContext); + // Note : accept behavior, java by default sends Accept behavior + // as text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2 + retConnection.setRequestProperty(Constants.HeaderConstants.ACCEPT, TableConstants.HeaderConstants.ACCEPT_TYPE); + retConnection.setRequestProperty(Constants.HeaderConstants.ACCEPT_CHARSET, "UTF-8"); + retConnection.setRequestProperty(TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION, + TableConstants.HeaderConstants.MAX_DATA_SERVICE_VERSION_VALUE); + + retConnection.setRequestProperty(Constants.HeaderConstants.CONTENT_TYPE, + TableConstants.HeaderConstants.ATOMPUB_TYPE); + + if (!Utility.isNullOrEmpty(eTag)) { + retConnection.setRequestProperty(Constants.HeaderConstants.IF_MATCH, eTag); + } + + retConnection.setRequestMethod(requestMethod); + return retConnection; + } + + /** + * Reserved for internal use. Constructs an HttpURLConnection to perform a delete operation. + * + * @param rootUri + * A java.net.URI containing an absolute URI to the resource. + * @param tableName + * The name of the table. + * @param identity + * The identity of the entity. The resulting request will be formatted as /tableName(identity) if not + * null or empty. + * @param eTag + * The etag of the entity. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param queryBuilder + * The {@link UriQueryBuilder} for the operation. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * An HttpURLConnection to use to perform the operation. + * + * @throws IOException + * if there is an error opening the connection. + * @throws URISyntaxException + * if the resource URI is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + protected static HttpURLConnection delete(final URI rootUri, final String tableName, final String identity, + final String eTag, final int timeoutInMs, final UriQueryBuilder queryBuilder, + final TableRequestOptions tableOptions, final OperationContext opContext) throws IOException, + URISyntaxException, StorageException { + + return coreCreate(rootUri, tableName, eTag, identity, timeoutInMs, queryBuilder, "DELETE", tableOptions, + opContext); + } + + /** + * Reserved for internal use. Constructs an HttpURLConnection to perform an insert operation. + * + * @param rootUri + * A java.net.URI containing an absolute URI to the resource. + * @param tableName + * The name of the table. + * @param identity + * The identity of the entity. The resulting request will be formatted as /tableName(identity) if not + * null or empty. + * @param eTag + * The etag of the entity, can be null for straight inserts. + * @param updateType + * The {@link TableUpdateType} type of update to be performed. Specify null for straight + * inserts. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param queryBuilder + * The {@link UriQueryBuilder} for the operation. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * An HttpURLConnection to use to perform the operation. + * + * @throws IOException + * if there is an error opening the connection. + * @throws URISyntaxException + * if the resource URI is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + protected static HttpURLConnection insert(final URI rootUri, final String tableName, final String identity, + final String eTag, final TableUpdateType updateType, final int timeoutInMs, + final UriQueryBuilder queryBuilder, final TableRequestOptions tableOptions, final OperationContext opContext) + throws IOException, URISyntaxException, StorageException { + HttpURLConnection retConnection = null; + + if (updateType == null) { + retConnection = coreCreate(rootUri, tableName, eTag, null/* identity */, timeoutInMs, queryBuilder, + "POST", tableOptions, opContext); + } + else if (updateType == TableUpdateType.MERGE) { + retConnection = coreCreate(rootUri, tableName, null/* ETAG */, identity, timeoutInMs, queryBuilder, + "POST", tableOptions, opContext); + + retConnection.setRequestProperty("X-HTTP-Method", "MERGE"); + } + else if (updateType == TableUpdateType.REPLACE) { + retConnection = coreCreate(rootUri, tableName, null/* ETAG */, identity, timeoutInMs, queryBuilder, "PUT", + tableOptions, opContext); + } + + retConnection.setDoOutput(true); + + return retConnection; + } + + /** + * Reserved for internal use. Constructs an HttpURLConnection to perform a merge operation. + * + * @param rootUri + * A java.net.URI containing an absolute URI to the resource. + * @param tableName + * The name of the table. + * @param identity + * The identity of the entity. The resulting request will be formatted as /tableName(identity) if not + * null or empty. + * @param eTag + * The etag of the entity. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param queryBuilder + * The {@link UriQueryBuilder} for the operation. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * An HttpURLConnection to use to perform the operation. + * + * @throws IOException + * if there is an error opening the connection. + * @throws URISyntaxException + * if the resource URI is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + protected static HttpURLConnection merge(final URI rootUri, final String tableName, final String identity, + final String eTag, final int timeoutInMs, final UriQueryBuilder queryBuilder, + final TableRequestOptions tableOptions, final OperationContext opContext) throws IOException, + URISyntaxException, StorageException { + final HttpURLConnection retConnection = coreCreate(rootUri, tableName, eTag, identity, timeoutInMs, + queryBuilder, "POST", tableOptions, opContext); + retConnection.setRequestProperty("X-HTTP-Method", "MERGE"); + retConnection.setDoOutput(true); + return retConnection; + } + + /** + * Reserved for internal use. Constructs an HttpURLConnection to perform a single entity query operation. + * + * @param rootUri + * A java.net.URI containing an absolute URI to the resource. + * @param tableName + * The name of the table. + * @param identity + * The identity of the entity. The resulting request will be formatted as /tableName(identity) if not + * null or empty. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param queryBuilder + * The {@link UriQueryBuilder} for the operation. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * An HttpURLConnection to use to perform the operation. + * + * @throws IOException + * if there is an error opening the connection. + * @throws URISyntaxException + * if the resource URI is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + protected static HttpURLConnection query(final URI rootUri, final String tableName, final String identity, + final int timeoutInMs, UriQueryBuilder queryBuilder, final ResultContinuation continuationToken, + final TableRequestOptions tableOptions, final OperationContext opContext) throws IOException, + URISyntaxException, StorageException { + if (queryBuilder == null) { + queryBuilder = new UriQueryBuilder(); + } + + applyContinuationToQueryBuilder(queryBuilder, continuationToken); + final HttpURLConnection retConnection = coreCreate(rootUri, tableName, null, identity, timeoutInMs, + queryBuilder, "GET", tableOptions, opContext); + + return retConnection; + } + + /** + * Reserved for internal use. Constructs an HttpURLConnection to perform an update operation. + * + * @param rootUri + * A java.net.URI containing an absolute URI to the resource. + * @param tableName + * The name of the table. + * @param identity + * A String representing the identity of the entity. The resulting request will be formatted + * using /tableName(identity) if identity is not >code>null or empty. + * @param eTag + * The etag of the entity. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param queryBuilder + * The {@link UriQueryBuilder} for the operation. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify null to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify null to + * safely ignore operation context. + * + * @return + * An HttpURLConnection to use to perform the operation. + * + * @throws IOException + * if there is an error opening the connection. + * @throws URISyntaxException + * if the resource URI is invalid. + * @throws StorageException + * if a storage service error occurred during the operation. + */ + protected static HttpURLConnection update(final URI rootUri, final String tableName, final String identity, + final String eTag, final int timeoutInMs, final UriQueryBuilder queryBuilder, + final TableRequestOptions tableOptions, final OperationContext opContext) throws IOException, + URISyntaxException, StorageException { + final HttpURLConnection retConnection = coreCreate(rootUri, tableName, eTag, identity, timeoutInMs, + queryBuilder, "PUT", tableOptions, opContext); + + retConnection.setDoOutput(true); + return retConnection; + } + + /** + * Private Default Constructor. + */ + private TableRequest() { + // No op + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequestOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequestOptions.java new file mode 100644 index 0000000000000..1a4d210526f20 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequestOptions.java @@ -0,0 +1,35 @@ +/** + * Copyright 2011 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.services.table.client; + +import com.microsoft.windowsazure.services.core.storage.RequestOptions; + +/** + * Represents a set of timeout and retry policy options that may be specified for a table operation request. + */ +public class TableRequestOptions extends RequestOptions { + /** + * Reserved for internal use. Initializes the timeout and retry policy for this TableRequestOptions + * instance, if they are currently null, using the values specified in the {@link CloudTableClient} + * parameter. + * + * @param client + * The {@link CloudTableClient} client object to copy the timeout and retry policy from. + */ + protected void applyDefaults(final CloudTableClient client) { + super.applyBaseDefaults(client); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResponse.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResponse.java new file mode 100644 index 0000000000000..848e7b2afc0bc --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResponse.java @@ -0,0 +1,75 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.net.HttpURLConnection; + +import com.microsoft.windowsazure.services.core.storage.ResultContinuation; +import com.microsoft.windowsazure.services.core.storage.ResultContinuationType; + +/** + * Reserved for internal use. A class used to help parse responses from the Table service. + */ +class TableResponse { + /** + * Reserved for internal use. A static factory method that constructs a {@link ResultContinuation} instance from the + * continuation token information in a table operation response, if any. + * + * @param queryRequest + * The java.net.HttpURLConnection request response to parse for continuation token + * information. + * + * @return + * A {@link ResultContinuation} instance from continuation token information in the response, or + * null if none is found. + */ + protected static ResultContinuation getTableContinuationFromResponse(final HttpURLConnection queryRequest) { + final ResultContinuation retVal = new ResultContinuation(); + retVal.setContinuationType(ResultContinuationType.TABLE); + + boolean foundToken = false; + + String tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION + .concat(TableConstants.TABLE_SERVICE_NEXT_PARTITION_KEY)); + if (tString != null) { + retVal.setNextPartitionKey(tString); + foundToken = true; + } + + tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION + .concat(TableConstants.TABLE_SERVICE_NEXT_ROW_KEY)); + if (tString != null) { + retVal.setNextRowKey(tString); + foundToken = true; + } + + tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION + .concat(TableConstants.TABLE_SERVICE_NEXT_MARKER)); + if (tString != null) { + retVal.setNextMarker(tString); + foundToken = true; + } + + tString = queryRequest.getHeaderField(TableConstants.TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION + .concat(TableConstants.TABLE_SERVICE_NEXT_TABLE_NAME)); + if (tString != null) { + retVal.setNextTableName(tString); + foundToken = true; + } + + return foundToken ? retVal : null; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResult.java new file mode 100644 index 0000000000000..95e0621b63ed9 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResult.java @@ -0,0 +1,179 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.HashMap; + +/** + * A class which represents the result of a table operation. The {@link TableResult} class encapsulates the HTTP + * response + * and any table entity results returned by the Storage Service REST API operation called for a particular + * {@link TableOperation}. + * + */ +public class TableResult { + private Object result; + + private int httpStatusCode = -1; + + private String id; + + private String etag; + + private HashMap properties; + + /** + * Initializes an empty {@link TableResult} instance. + */ + public TableResult() { + // empty ctor + } + + /** + * Initializes a {@link TableResult} instance with the specified HTTP status code. + * + * @param httpStatusCode + * The HTTP status code for the table operation returned by the server. + */ + public TableResult(final int httpStatusCode) { + this.httpStatusCode = httpStatusCode; + } + + /** + * Gets the Etag returned with the table operation results. The server will return the same Etag value for a + * table, entity, or entity group returned by an operation as long as it is unchanged on the server. + * + * @return + * A String containing the Etag returned by the server with the table operation results. + */ + public String getEtag() { + return this.etag; + } + + /** + * Gets the HTTP status code returned by a table operation request. + * + * @return + * The HTTP status code for the table operation returned by the server. + */ + public int getHttpStatusCode() { + return this.httpStatusCode; + } + + /** + * Gets the AtomPub Entry Request ID value for the result returned by a table operation request. + * + * @return + * The Entry Request ID for the table operation result. + */ + public String getId() { + return this.id; + } + + /** + * Gets the map of properties for a table entity returned by the table operation. + * + * @return + * A java.util.HashMap of String property names to {@link EntityProperty} data + * typed values representing the properties of a table entity. + */ + public HashMap getProperties() { + return this.properties; + } + + /** + * Gets the result returned by the table operation as an Object. + * + * @return + * The result returned by the table operation as an Object. + */ + public Object getResult() { + return this.result; + } + + /** + * Gets the result returned by the table operation as an instance of the specified type. + * + * @return + * The result returned by the table operation as an instance of type T. + */ + @SuppressWarnings("unchecked") + public T getResultAsType() { + return (T) this.getResult(); + } + + /** + * Reserved for internal use. Sets the Etag associated with the table operation results. + * + * @param etag + * A String containing an Etag to associate with the table operation results. + */ + protected void setEtag(final String etag) { + this.etag = etag; + } + + /** + * Reserved for internal use. Sets the HTTP status code associated with the table operation results. + * + * @param httpStatusCode + * The HTTP status code value to associate with the table operation results. + */ + protected void setHttpStatusCode(final int httpStatusCode) { + this.httpStatusCode = httpStatusCode; + } + + /** + * Reserved for internal use. Sets the AtomPub Entry Request ID associated with the table operation result. + * + * @param id + * A String containing the request ID to associate with the table operation result. + */ + protected void setId(final String id) { + this.id = id; + } + + /** + * Reserved for internal use. Sets the map of properties for a table entity to associate with the table operation. + * + * @param properties + * A java.util.HashMap of String property names to {@link EntityProperty} data + * typed values representing the properties of a table entity to associate with the table operation. + */ + protected void setProperties(final HashMap properties) { + this.properties = properties; + } + + /** + * Reserved for internal use. Sets a result Object instance to associate with the table operation. + * + * @param result + * An instance of a result Object to associate with the table operation. + */ + protected void setResult(final Object result) { + this.result = result; + } + + /** + * Reserved for internal use. Sets the result to associate with the table operation as a {@link TableEntity}. + * + * @param ent + * An instance of an object implementing {@link TableEntity} to associate with the table operation. + */ + protected void updateResultObject(final TableEntity ent) { + this.result = ent; + ent.setEtag(this.etag); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceEntity.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceEntity.java new file mode 100644 index 0000000000000..d958bb328444b --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceEntity.java @@ -0,0 +1,414 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.lang.reflect.InvocationTargetException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map.Entry; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * The {@link TableServiceEntity} class represents the base object type for a table entity in the Storage service. + * {@link TableServiceEntity} provides a base implementation for the {@link TableEntity} interface that provides + * readEntity and writeEntity methods that by default serialize and deserialize all properties + * via reflection. A table entity class may extend this class and override the readEntity and + * writeEntity methods to provide customized or more performant serialization logic. + *

+ * The use of reflection allows subclasses of {@link TableServiceEntity} to be serialized and deserialized without + * having to implement the serialization code themselves. When both a getter method and setter method are found for a + * given property name and data type, then the appropriate method is invoked automatically to serialize or deserialize + * the data. To take advantage of the automatic serialization code, your table entity classes should provide getter and + * setter methods for each property in the corresponding table entity in Windows Azure table storage. The reflection + * code looks for getter and setter methods in pairs of the form + *

+ * public type getPropertyName() { ... } + *

+ * and + *

+ * public void setPropertyName(type parameter) { ... } + *

+ * where PropertyName is a property name for the table entity, and type is a Java type compatible with + * the EDM data type of the property. See the table below for a map of property types to their Java equivalents. The + * {@link StoreAs} annotation may be applied with a name attribute to specify a property name for + * reflection on getter and setter methods that do not follow the property name convention. Method names and the + * name attribute of {@link StoreAs} annotations are case sensitive for matching property names with + * reflection. Use the {@link Ignore} annotation to prevent methods from being used by reflection for automatic + * serialization and deserialization. Note that the names "PartitionKey", "RowKey", "Timestamp", and "Etag" are reserved + * and will be ignored if set with the {@link StoreAs} annotation in a subclass. + *

+ * The following table shows the supported property data types in Windows Azure storage and the corresponding Java types + * when deserialized. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Storage TypeEdmType ValueJava TypeDescription
Edm.Binary{@link EdmType#BINARY}byte[], Byte[]An array of bytes up to 64 KB in size.
Edm.Boolean{@link EdmType#BOOLEAN}boolean, BooleanA Boolean value.
Edm.Byte{@link EdmType#BYTE}boolean, BooleanA Boolean value.
Edm.DateTime{@link EdmType#DATE_TIME}DateA 64-bit value expressed as Coordinated Universal Time (UTC). The supported range begins from 12:00 midnight, + * January 1, 1601 A.D. (C.E.), UTC. The range ends at December 31, 9999.
Edm.Double{@link EdmType#DOUBLE}double, DoubleA 64-bit double-precision floating point value.
Edm.Guid{@link EdmType#GUID}UUIDA 128-bit globally unique identifier.
Edm.Int32{@link EdmType#INT32}int, IntegerA 32-bit integer value.
Edm.Int64{@link EdmType#INT64}long, LongA 64-bit integer value.
Edm.String{@link EdmType#STRING}StringA UTF-16-encoded value. String values may be up to 64 KB in size.
+ *

+ * See the MSDN topic Understanding the + * Table Service Data Model for an overview of tables, entities, and properties as used in the Windows Azure Storage + * service. + *

+ * For an overview of the available EDM primitive data types and names, see the + * + * Primitive Data Types section of + * the OData Protocol Overview. + *

+ * + * @see EdmType + */ +public class TableServiceEntity implements TableEntity { + /** + * Deserializes the table entity property map into the specified object instance using reflection. + *

+ * This static method takes an object instance that represents a table entity type and uses reflection on its class + * type to find methods to deserialize the data from the property map into the instance. + *

+ * Each property name and data type in the properties map is compared with the methods in the class type for a pair + * of getter and setter methods to use for serialization and deserialization. The class is scanned for methods with + * names that match the property name with "get" and "set" prepended, or with the {@link StoreAs} annotation set + * with the property name. The methods must have return types or parameter data types that match the data type of + * the corresponding {@link EntityProperty} value. If such a pair is found, the data is copied into the instance + * object by invoking the setter method on the instance. Properties that do not match a method pair by name and data + * type are not copied. + * + * @param instance + * A reference to an instance of a class implementing {@link TableEntity} to deserialize the table entity + * data into. + * @param properties + * A map of String property names to {@link EntityProperty} objects containing typed data + * values to deserialize into the instance parameter object. + * @param opContext + * An {@link OperationContext} object that represents the context for the current operation. + * + * @throws IllegalArgumentException + * if the table entity response received is invalid or improperly formatted. + * @throws IllegalAccessException + * if the table entity threw an exception during deserialization. + * @throws InvocationTargetException + * if a method invoked on the instance parameter threw an exception during deserialization. + */ + public static void readEntityWithReflection(final Object instance, + final HashMap properties, final OperationContext opContext) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + final HashMap props = PropertyPair.generatePropertyPairs(instance.getClass()); + + for (final Entry p : properties.entrySet()) { + if (props.containsKey(p.getKey())) { + // TODO add logging + // System.out.println("Consuming " + p.getKey() + ":" + p.getValue().getValueAsString()); + props.get(p.getKey()).consumeTableProperty(p.getValue(), instance); + } + } + } + + /** + * Serializes the property data from a table entity instance into a property map using reflection. + *

+ * This static method takes an object instance that represents a table entity type and uses reflection on its class + * type to find methods to serialize the data from the instance into the property map. + *

+ * Each property name and data type in the properties map is compared with the methods in the class type for a pair + * of getter and setter methods to use for serialization and deserialization. The class is scanned for methods with + * names that match the property name with "get" and "set" prepended, or with the {@link StoreAs} annotation set + * with the property name. The methods must have return types or parameter data types that match the data type of + * the corresponding {@link EntityProperty} value. If such a pair is found, the data is copied from the instance + * object by invoking the getter method on the instance. Properties that do not have a method pair with matching + * name and data type are not copied. + * + * @param instance + * A reference to an instance of a class implementing {@link TableEntity} to serialize the table entity + * data from. + * @return + * A map of String property names to {@link EntityProperty} objects containing typed data + * values serialized from the instance parameter object. + * + * @throws IllegalArgumentException + * if the table entity is invalid or improperly formatted. + * @throws IllegalAccessException + * if the table entity threw an exception during serialization. + * @throws InvocationTargetException + * if a method invoked on the instance parameter threw an exception during serialization. + */ + public static HashMap writeEntityWithReflection(final Object instance) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + final HashMap props = PropertyPair.generatePropertyPairs(instance.getClass()); + + final HashMap retVal = new HashMap(); + for (final Entry p : props.entrySet()) { + retVal.put(p.getValue().effectiveName, p.getValue().generateTableProperty(instance)); + } + + return retVal; + } + + /** + * Reserved for internal use. The value of the partition key in the entity. + */ + protected String partitionKey = null; + + /** + * Reserved for internal use. The value of the row key in the entity. + */ + protected String rowKey = null; + + /** + * Reserved for internal use. The value of the Etag for the entity. + */ + protected String etag = null; + + /** + * Reserved for internal use. The value of the Timestamp in the entity. + */ + protected Date timeStamp = new Date(); + + /** + * Initializes an empty {@link TableServiceEntity} instance. + */ + public TableServiceEntity() { + // Empty ctor + } + + /** + * Gets the Etag value for the entity. This value is used to determine if the table entity has changed since it was + * last read from Windows Azure storage. + * + * @return + * A String containing the Etag for the entity. + */ + @Override + public String getEtag() { + return this.etag; + } + + /** + * Gets the PartitionKey value for the entity. + * + * @return + * A String containing the PartitionKey value for the entity. + */ + @Override + public String getPartitionKey() { + return this.partitionKey; + } + + /** + * Gets the RowKey value for the entity. + * + * @return + * A String containing the RowKey value for the entity. + */ + @Override + public String getRowKey() { + return this.rowKey; + } + + /** + * Gets the Timestamp value for the entity. + * + * @return + * A Date containing the Timestamp value for the entity. + */ + @Override + public Date getTimestamp() { + return this.timeStamp; + } + + /** + * Populates this table entity instance using the map of property names to {@link EntityProperty} data typed values. + *

+ * This method invokes {@link TableServiceEntity#readEntityWithReflection} to populate the table entity instance the + * method is called on using reflection. Table entity classes that extend {@link TableServiceEntity} can take + * advantage of this behavior by implementing getter and setter methods for the particular properties of the table + * entity in Windows Azure storage the class represents. + *

+ * Override this method in classes that extend {@link TableServiceEntity} to invoke custom serialization code. + * + * @param properties + * The java.util.HashMap of String property names to {@link EntityProperty} + * data values to deserialize and store in this table entity instance. + * @param opContext + * An {@link OperationContext} object used to track the execution of the operation. + * @throws StorageException + * if an error occurs during the deserialization. + */ + @Override + public void readEntity(final HashMap properties, final OperationContext opContext) + throws StorageException { + try { + readEntityWithReflection(this, properties, opContext); + } + catch (IllegalArgumentException e) { + throw new StorageException(StorageErrorCodeStrings.INVALID_XML_DOCUMENT, + "The response received is invalid or improperly formatted.", + Constants.HeaderConstants.HTTP_UNUSED_306, null, e); + } + catch (IllegalAccessException e) { + throw new StorageException(StorageErrorCodeStrings.INVALID_XML_DOCUMENT, + "The entity threw an exception during deserialization", Constants.HeaderConstants.HTTP_UNUSED_306, + null, e); + } + catch (InvocationTargetException e) { + throw new StorageException(StorageErrorCodeStrings.INTERNAL_ERROR, + "The entity threw an exception during deserialization", Constants.HeaderConstants.HTTP_UNUSED_306, + null, e); + } + } + + /** + * Sets the Etag value for the entity. This value is used to determine if the table entity has changed since it was + * last read from Windows Azure storage. + * + * @param etag + * A String containing the Etag for the entity. + */ + @Override + public void setEtag(final String etag) { + this.etag = etag; + } + + /** + * Sets the PartitionKey value for the entity. + * + * @param partitionKey + * A String containing the PartitionKey value for the entity. + */ + @Override + public void setPartitionKey(final String partitionKey) { + this.partitionKey = partitionKey; + } + + /** + * Sets the RowKey value for the entity. + * + * @param rowKey + * A String containing the RowKey value for the entity. + */ + @Override + public void setRowKey(final String rowKey) { + this.rowKey = rowKey; + } + + /** + * Sets the Timestamp value for the entity. + * + * @param timeStamp + * A Date containing the Timestamp value for the entity. + */ + @Override + public void setTimestamp(final Date timeStamp) { + this.timeStamp = timeStamp; + } + + /** + * Returns a map of property names to {@link EntityProperty} data typed values created by serializing this table + * entity instance. + *

+ * This method invokes {@link #writeEntityWithReflection} to serialize the table entity instance the method is + * called on using reflection. Table entity classes that extend {@link TableServiceEntity} can take advantage of + * this behavior by implementing getter and setter methods for the particular properties of the table entity in + * Windows Azure storage the class represents. Note that the property names "PartitionKey", "RowKey", and + * "Timestamp" are reserved and will be ignored if set on other methods with the {@link StoreAs} annotation. + *

+ * Override this method in classes that extend {@link TableServiceEntity} to invoke custom serialization code. + * + * @param opContext + * An {@link OperationContext} object used to track the execution of the operation. + * @return + * A java.util.HashMap of String property names to {@link EntityProperty} data + * typed values representing the properties serialized from this table entity instance. + * @throws StorageException + * if an error occurs during the serialization. + */ + @Override + public HashMap writeEntity(final OperationContext opContext) throws StorageException { + try { + return writeEntityWithReflection(this); + } + catch (final IllegalAccessException e) { + throw new StorageException(StorageErrorCodeStrings.INTERNAL_ERROR, + "An attempt was made to access an inaccessible member of the entity during serialization.", + Constants.HeaderConstants.HTTP_UNUSED_306, null, e); + } + catch (final InvocationTargetException e) { + throw new StorageException(StorageErrorCodeStrings.INTERNAL_ERROR, + "The entity threw an exception during serialization", Constants.HeaderConstants.HTTP_UNUSED_306, + null, e); + } + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceException.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceException.java new file mode 100644 index 0000000000000..04f065a4407df --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceException.java @@ -0,0 +1,172 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; + +import javax.xml.stream.XMLStreamException; + +import com.microsoft.windowsazure.services.core.storage.RequestResult; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.StorageExtendedErrorInformation; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageErrorResponse; + +/** + * An exception that results when a table storage service operation fails to complete successfully. + */ +public class TableServiceException extends StorageException { + + private static final long serialVersionUID = 6037366449663934891L; + + /** + * Reserved for internal use. A static factory method to create a {@link TableServiceException} instance using + * the specified parameters. + * + * @param retryable + * A flag indicating the table operation can be retried. + * @param res + * A {@link RequestResult} containing the result of the table storage service operation. + * @param op + * The {@link TableOperation} representing the table operation that caused the exception. + * @param inStream + * The java.io.InputStream of the error response from the table operation request. + * @return + * A {@link TableServiceException} instance initialized with values from the input parameters. + * @throws IOException + * if an IO error occurs. + */ + protected static TableServiceException generateTableServiceException(boolean retryable, RequestResult res, + TableOperation op, InputStream inStream) throws IOException { + try { + TableServiceException retryableException = new TableServiceException(res.getStatusCode(), + res.getStatusMessage(), op, new InputStreamReader(inStream)); + retryableException.retryable = retryable; + + return retryableException; + } + finally { + inStream.close(); + } + } + + private TableOperation operation; + + /** + * Reserved for internal use. This flag indicates whether the operation that threw the exception can be retried. + */ + protected boolean retryable = false; + + /** + * Constructs a TableServiceException instance using the specified error code, message, status code, + * extended error information and inner exception. + * + * @param errorCode + * A String that represents the error code returned by the table operation. + * @param message + * A String that represents the error message returned by the table operation. + * @param statusCode + * The HTTP status code returned by the table operation. + * @param extendedErrorInfo + * A {@link StorageExtendedErrorInformation} object that represents the extended error information + * returned by the table operation. + * @param innerException + * An Exception object that represents a reference to the initial exception, if one exists. + */ + public TableServiceException(final String errorCode, final String message, final int statusCode, + final StorageExtendedErrorInformation extendedErrorInfo, final Exception innerException) { + super(errorCode, message, statusCode, extendedErrorInfo, innerException); + } + + /** + * Reserved for internal use. Constructs a TableServiceException instance using the specified HTTP + * status code, message, operation, and stream reader. + * + * @param httpStatusCode + * The int HTTP Status Code value returned by the table operation that caused the exception. + * @param message + * A String description of the error that caused the exception. + * @param operation + * The {@link TableOperation} object representing the table operation that was in progress when the + * exception occurred. + * @param reader + * The Java.IO.Stream derived stream reader for the HTTP request results returned by the + * table operation, if any. + */ + protected TableServiceException(final int httpStatusCode, final String message, final TableOperation operation, + final Reader reader) { + super(null, message, httpStatusCode, null, null); + this.operation = operation; + + if (reader != null) { + try { + final StorageErrorResponse error = new StorageErrorResponse(reader); + this.extendedErrorInformation = error.getExtendedErrorInformation(); + this.errorCode = this.extendedErrorInformation.getErrorCode(); + } + catch (XMLStreamException e) { + // no-op, if error parsing fails, just throw first exception. + } + } + } + + /** + * Gets the table operation that caused the TableServiceException to be thrown. + * + * @return + * The {@link TableOperation} object representing the table operation that caused this + * {@link TableServiceException} to be thrown. + */ + public TableOperation getOperation() { + return this.operation; + } + + /** + * Reserved for internal use. Gets a flag indicating the table operation can be retried. + * + * @return + * The boolean flag indicating whether the table operation that caused the exception can be + * retried. + */ + public boolean isRetryable() { + return this.retryable; + } + + /** + * Reserved for internal use. Sets the table operation that caused the TableServiceException to be + * thrown. + * + * @param operation + * The {@link TableOperation} object representing the table operation that caused this + * {@link TableServiceException} to be thrown. + */ + protected void setOperation(final TableOperation operation) { + this.operation = operation; + } + + /** + * Reserved for internal use. Sets a flag indicating the table operation can be retried. + * + * @param retryable + * The boolean flag to set indicating whether the table operation that caused the exception + * can be retried. + */ + protected void setRetryable(boolean retryable) { + this.retryable = retryable; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableUpdateType.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableUpdateType.java new file mode 100644 index 0000000000000..3fbf519729c44 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableUpdateType.java @@ -0,0 +1,31 @@ +/** + * Copyright 2011 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.services.table.client; + +/** + * Reserved for internal use. An enum that represents the type of update a given upsert operation will perform. + */ +enum TableUpdateType { + /** + * The table operation updates an existing entity. + */ + MERGE, + + /** + * The table operation replaces an existing entity. + */ + REPLACE; +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableBatchOperationTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableBatchOperationTests.java new file mode 100644 index 0000000000000..671ac7c308a75 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableBatchOperationTests.java @@ -0,0 +1,762 @@ +/** + * Copyright 2011 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.services.table.client; + +import static org.junit.Assert.*; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +public class TableBatchOperationTests extends TableTestBase { + @Test + public void batchDelete() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + + // insert entity + class1 ref = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + batch.delete(ref); + + ArrayList delResults = tClient.execute(testSuiteTableName, batch); + for (TableResult r : delResults) { + Assert.assertEquals(r.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + + try { + tClient.execute(testSuiteTableName, batch); + fail(); + } + catch (StorageException ex) { + Assert.assertEquals(ex.getHttpStatusCode(), HttpURLConnection.HTTP_NOT_FOUND); + } + } + + @Test + public void batchDeleteFail() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + + // Insert entity to delete + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + + class1 updatedEntity = generateRandomEnitity("jxscl_odata"); + updatedEntity.setPartitionKey(baseEntity.getPartitionKey()); + updatedEntity.setRowKey(baseEntity.getRowKey()); + updatedEntity.setEtag(baseEntity.getEtag()); + tClient.execute(testSuiteTableName, TableOperation.replace(updatedEntity)); + + // add delete to fail + batch.delete(baseEntity); + + try { + @SuppressWarnings("unused") + ArrayList results = tClient.execute(testSuiteTableName, batch); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Precondition Failed"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The update condition specified in the request was not satisfied.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "UpdateConditionNotSatisfied"); + } + } + + @Test + public void batchEmptyQuery() throws StorageException { + // insert entity + class1 ref = generateRandomEnitity("jxscl_odata"); + + TableBatchOperation batch = new TableBatchOperation(); + batch.retrieve(ref.getPartitionKey(), ref.getRowKey(), ref.getClass()); + + ArrayList results = tClient.execute(testSuiteTableName, batch); + + Assert.assertEquals(results.size(), 1); + Assert.assertNull(results.get(0).getResult()); + Assert.assertEquals(results.get(0).getHttpStatusCode(), HttpURLConnection.HTTP_NOT_FOUND); + } + + @Test + public void batchInsertFail() throws StorageException { + // insert entity + class1 ref = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + try { + TableBatchOperation batch = new TableBatchOperation(); + batch.insert(ref); + tClient.execute(testSuiteTableName, batch); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Conflict"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The specified entity already exists")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "EntityAlreadyExists"); + } + } + + @Test + public void batchLockToPartitionKey() throws StorageException { + try { + TableBatchOperation batch = new TableBatchOperation(); + batch.insert(generateRandomEnitity("jxscl_odata")); + batch.insert(generateRandomEnitity("jxscl_odata2")); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "All entities in a given batch must have the same partition key."); + } + } + + @Test + public void batchMergeFail() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + addInsertBatch(batch); + + // Insert entity to merge + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + + class1 updatedEntity = generateRandomEnitity("jxscl_odata"); + updatedEntity.setPartitionKey(baseEntity.getPartitionKey()); + updatedEntity.setRowKey(baseEntity.getRowKey()); + updatedEntity.setEtag(baseEntity.getEtag()); + tClient.execute(testSuiteTableName, TableOperation.replace(updatedEntity)); + + // add merge to fail + addMergeToBatch(baseEntity, batch); + + try { + @SuppressWarnings("unused") + ArrayList results = tClient.execute(testSuiteTableName, batch); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Precondition Failed"); + String errorAfterSemiColon = ex.getExtendedErrorInformation().getErrorMessage(); + errorAfterSemiColon = errorAfterSemiColon.substring(errorAfterSemiColon.indexOf(":") + 1); + Assert.assertTrue(errorAfterSemiColon + .startsWith("The update condition specified in the request was not satisfied.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "UpdateConditionNotSatisfied"); + } + } + + @Test + public void batchMultiQueryShouldThrow() throws StorageException { + class1 ref = generateRandomEnitity("jxscl_odata"); + class1 ref2 = generateRandomEnitity("jxscl_odata"); + + try { + TableBatchOperation batch = new TableBatchOperation(); + batch.retrieve(ref.getPartitionKey(), ref.getRowKey(), ref.getClass()); + batch.retrieve(ref2.getPartitionKey(), ref2.getRowKey(), ref2.getClass()); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), + "A batch transaction with a retrieve operation cannot contain any other operations."); + } + } + + @Test + public void batchAddNullShouldThrow() throws StorageException { + try { + TableBatchOperation batch = new TableBatchOperation(); + batch.add(null); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "element"); + } + } + + @Test + public void batchRetrieveWithNullResolver() throws StorageException { + try { + TableBatchOperation batch = new TableBatchOperation(); + batch.retrieve("foo", "blah", (EntityResolver) null); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "Query requires a valid class type or resolver."); + } + } + + @Test + public void batchOver100Entities() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + try { + for (int m = 0; m < 101; m++) { + batch.insert(generateRandomEnitity("jxscl_odata")); + } + + tClient.execute(testSuiteTableName, batch); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Bad Request"); + String errorAfterSemiColon = ex.getExtendedErrorInformation().getErrorMessage(); + errorAfterSemiColon = errorAfterSemiColon.substring(errorAfterSemiColon.indexOf(":") + 1); + Assert.assertTrue(errorAfterSemiColon.startsWith("One of the request inputs is not valid.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "InvalidInput"); + } + } + + @Test + public void batchQuery() throws StorageException { + // insert entity + class1 ref = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableBatchOperation batch = new TableBatchOperation(); + + batch.retrieve(ref.getPartitionKey(), ref.getRowKey(), ref.getClass()); + + ArrayList results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 1); + + Assert.assertEquals(results.get(0).getHttpStatusCode(), HttpURLConnection.HTTP_OK); + class1 retrievedRef = results.get(0).getResultAsType(); + + Assert.assertEquals(ref.getA(), retrievedRef.getA()); + Assert.assertEquals(ref.getB(), retrievedRef.getB()); + Assert.assertEquals(ref.getC(), retrievedRef.getC()); + Assert.assertTrue(Arrays.equals(ref.getD(), retrievedRef.getD())); + + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + } + + @Test + public void batchQueryAndOneMoreOperationShouldThrow() throws StorageException { + class1 ref2 = generateRandomEnitity("jxscl_odata"); + + try { + TableBatchOperation batch = new TableBatchOperation(); + batch.insert(generateRandomEnitity("jxscl_odata")); + batch.retrieve(ref2.getPartitionKey(), ref2.getRowKey(), ref2.getClass()); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), + "A batch transaction with a retrieve operation cannot contain any other operations."); + } + + try { + TableBatchOperation batch = new TableBatchOperation(); + batch.retrieve(ref2.getPartitionKey(), ref2.getRowKey(), ref2.getClass()); + batch.insert(generateRandomEnitity("jxscl_odata")); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), + "A batch transaction with a retrieve operation cannot contain any other operations."); + } + } + + @Test + public void batchReplaceFail() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + + // Insert entity to merge + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + + class1 updatedEntity = generateRandomEnitity("jxscl_odata"); + updatedEntity.setPartitionKey(baseEntity.getPartitionKey()); + updatedEntity.setRowKey(baseEntity.getRowKey()); + updatedEntity.setEtag(baseEntity.getEtag()); + tClient.execute(testSuiteTableName, TableOperation.replace(updatedEntity)); + + // add merge to fail + addReplaceToBatch(baseEntity, batch); + + try { + @SuppressWarnings("unused") + ArrayList results = tClient.execute(testSuiteTableName, batch); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Precondition Failed"); + String errorAfterSemiColon = ex.getExtendedErrorInformation().getErrorMessage(); + errorAfterSemiColon = errorAfterSemiColon.substring(errorAfterSemiColon.indexOf(":") + 1); + Assert.assertTrue(errorAfterSemiColon + .startsWith("The condition specified using HTTP conditional header(s) is not met.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "ConditionNotMet"); + } + } + + @Test + public void batchInsertEntityOver1MB() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + class1 bigEnt = new class1(); + + bigEnt.setA("foo_A"); + bigEnt.setB("foo_B"); + bigEnt.setC("foo_C"); + // 1mb right here + bigEnt.setD(new byte[1024 * 1024]); + bigEnt.setPartitionKey("jxscl_odata"); + bigEnt.setRowKey(UUID.randomUUID().toString()); + + batch.insert(bigEnt); + + for (int m = 0; m < 3; m++) { + class1 ref = new class1(); + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + batch.insert(ref); + } + + try { + tClient.execute(testSuiteTableName, batch); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Bad Request"); + String errorAfterSemiColon = ex.getExtendedErrorInformation().getErrorMessage(); + errorAfterSemiColon = errorAfterSemiColon.substring(errorAfterSemiColon.indexOf(":") + 1); + Assert.assertTrue(errorAfterSemiColon.startsWith("The entity is larger than allowed by the Table Service.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "EntityTooLarge"); + } + } + + @Test + public void batchInsertEntityWithPropertyMoreThan255chars() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + DynamicTableEntity bigEnt = new DynamicTableEntity(); + + String propName = ""; + for (int m = 0; m < 255; m++) { + propName.concat(Integer.toString(m % 9)); + } + + bigEnt.getProperties().put(propName, new EntityProperty("test")); + bigEnt.setPartitionKey("jxscl_odata"); + bigEnt.setRowKey(UUID.randomUUID().toString()); + + batch.insert(bigEnt); + + for (int m = 0; m < 3; m++) { + class1 ref = new class1(); + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + batch.insert(ref); + } + + try { + tClient.execute(testSuiteTableName, batch); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Bad Request"); + String errorAfterSemiColon = ex.getExtendedErrorInformation().getErrorMessage(); + errorAfterSemiColon = errorAfterSemiColon.substring(errorAfterSemiColon.indexOf(":") + 1); + Assert.assertTrue(errorAfterSemiColon.startsWith("One of the request inputs is not valid.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "InvalidInput"); + } + } + + @Test + public void batchSizeOver4mb() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + byte[] datArr = new byte[1024 * 128]; + Random rand = new Random(); + rand.nextBytes(datArr); + + // Each entity is approx 128kb, meaning ~32 entities will result in a request over 4mb. + try { + for (int m = 0; m < 32; m++) { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(datArr); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + batch.insert(ref); + } + + tClient.execute(testSuiteTableName, batch); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Bad Request"); + String errorAfterSemiColon = ex.getExtendedErrorInformation().getErrorMessage(); + Assert.assertTrue(errorAfterSemiColon + .startsWith("The content length for the requested operation has exceeded the limit.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "ContentLengthExceeded"); + } + } + + @Test + public void batchWithAllOperations() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + // insert + addInsertBatch(batch); + + { + // insert entity to delete + class1 delRef = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(delRef)); + batch.delete(delRef); + } + + { + // Insert entity to replace + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addReplaceToBatch(baseEntity, batch); + } + + { + // Insert entity to insert or replace + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addInsertOrReplaceToBatch(baseEntity, batch); + } + + { + // Insert entity to merge + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addMergeToBatch(baseEntity, batch); + } + + { + // Insert entity to merge, no pre-esisting entity + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addInsertOrMergeToBatch(baseEntity, batch); + } + + ArrayList results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 6); + + Iterator iter = results.iterator(); + + // insert + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + + // delete + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // replace + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // insert or replace + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // merge + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // insert or merge + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + } + + @Test + public void batchInsert() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + + // Add 3 inserts + for (int m = 0; m < 3; m++) { + addInsertBatch(batch); + } + + // insert entity + class1 ref = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + batch.delete(ref); + + ArrayList results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 4); + + Iterator iter = results.iterator(); + + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + + // delete + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + + @Test + public void batchMerge() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + addInsertBatch(batch); + + // insert entity to delete + class1 delRef = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(delRef)); + batch.delete(delRef); + + // Insert entity to merge + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addMergeToBatch(baseEntity, batch); + + ArrayList results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator iter = results.iterator(); + + // insert + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + + // delete + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // merge + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + + @Test + public void batchReplace() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + addInsertBatch(batch); + + // insert entity to delete + class1 delRef = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(delRef)); + batch.delete(delRef); + + // Insert entity to replace + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addReplaceToBatch(baseEntity, batch); + + ArrayList results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator iter = results.iterator(); + + // insert + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + + // delete + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // replace + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + + @Test + public void batchInsertOrMerge() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + addInsertBatch(batch); + + // insert entity to delete + class1 delRef = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(delRef)); + batch.delete(delRef); + + // Insert entity to merge + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addInsertOrMergeToBatch(baseEntity, batch); + + ArrayList results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator iter = results.iterator(); + + // insert + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + + // delete + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // merge + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + + @Test + public void batchInsertOrReplace() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + addInsertBatch(batch); + + // insert entity to delete + class1 delRef = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(delRef)); + batch.delete(delRef); + + // Insert entity to replace + class1 baseEntity = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + addInsertOrReplaceToBatch(baseEntity, batch); + + ArrayList results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator iter = results.iterator(); + + // insert + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + + // delete + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // replace + Assert.assertEquals(iter.next().getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + + @Test + public void emptyBatch() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + + try { + tClient.execute(testSuiteTableName, batch); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "Cannot Execute an empty batch operation"); + } + } + + @Test + public void insertBatch1() throws StorageException { + insertAndDeleteBatchWithX(1); + } + + @Test + public void insertBatch10() throws StorageException { + insertAndDeleteBatchWithX(10); + } + + @Test + public void insertBatch100() throws StorageException { + insertAndDeleteBatchWithX(100); + } + + @Test + public void upsertBatch1() throws StorageException { + upsertAndDeleteBatchWithX(1); + } + + @Test + public void upsertBatch10() throws StorageException { + upsertAndDeleteBatchWithX(10); + } + + @Test + public void upsertBatch100() throws StorageException { + upsertAndDeleteBatchWithX(100); + } + + private class1 addInsertBatch(TableBatchOperation batch) { + class1 ref = generateRandomEnitity("jxscl_odata"); + batch.insert(ref); + return ref; + } + + private class2 addInsertOrMergeToBatch(class1 baseEntity, TableBatchOperation batch) { + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + batch.insertOrMerge(secondEntity); + return secondEntity; + } + + private class2 addInsertOrReplaceToBatch(class1 baseEntity, TableBatchOperation batch) { + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + batch.insertOrReplace(secondEntity); + return secondEntity; + } + + private class2 addMergeToBatch(class1 baseEntity, TableBatchOperation batch) { + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + batch.merge(secondEntity); + return secondEntity; + } + + private class2 addReplaceToBatch(class1 baseEntity, TableBatchOperation batch) { + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + batch.replace(secondEntity); + return secondEntity; + } + + private void insertAndDeleteBatchWithX(int x) throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + for (int m = 0; m < x; m++) { + addInsertBatch(batch); + } + + TableBatchOperation delBatch = new TableBatchOperation(); + ArrayList results = tClient.execute(testSuiteTableName, batch); + for (TableResult r : results) { + Assert.assertEquals(r.getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + delBatch.delete((class1) r.getResult()); + } + + ArrayList delResults = tClient.execute(testSuiteTableName, delBatch); + for (TableResult r : delResults) { + Assert.assertEquals(r.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + } + + private void upsertAndDeleteBatchWithX(int x) throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + for (int m = 0; m < x; m++) { + addInsertOrMergeToBatch(generateRandomEnitity("jxscl_odata"), batch); + } + + TableBatchOperation delBatch = new TableBatchOperation(); + ArrayList results = tClient.execute(testSuiteTableName, batch); + for (TableResult r : results) { + Assert.assertEquals(r.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + delBatch.delete((class2) r.getResult()); + } + + ArrayList delResults = tClient.execute(testSuiteTableName, delBatch); + for (TableResult r : delResults) { + Assert.assertEquals(r.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + } + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableClientTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableClientTests.java new file mode 100644 index 0000000000000..7dc7bd3c4dbd6 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableClientTests.java @@ -0,0 +1,268 @@ +/** + * Copyright 2011 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.services.table.client; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.text.DecimalFormat; +import java.util.ArrayList; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.ResultSegment; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Client Tests + */ +public class TableClientTests extends TableTestBase { + @Test + public void listTablesSegmented() throws IOException, URISyntaxException, StorageException { + String tableBaseName = generateRandomTableName(); + ArrayList tables = new ArrayList(); + for (int m = 0; m < 20; m++) { + String name = String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(m)); + tClient.createTable(name); + tables.add(name); + } + + try { + int currTable = 0; + ResultSegment segment1 = tClient.listTablesSegmented(tableBaseName, 5, null, null, null); + Assert.assertEquals(5, segment1.getLength()); + for (String s : segment1.getResults()) { + Assert.assertEquals(s, + String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(currTable))); + currTable++; + } + + ResultSegment segment2 = tClient.listTablesSegmented(tableBaseName, 5, + segment1.getContinuationToken(), null, null); + Assert.assertEquals(5, segment2.getLength()); + for (String s : segment2.getResults()) { + Assert.assertEquals(s, + String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(currTable))); + currTable++; + } + + ResultSegment segment3 = tClient.listTablesSegmented(tableBaseName, 5, + segment2.getContinuationToken(), null, null); + Assert.assertEquals(5, segment3.getLength()); + for (String s : segment3.getResults()) { + Assert.assertEquals(s, + String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(currTable))); + currTable++; + } + } + finally { + for (String s : tables) { + tClient.deleteTable(s); + } + } + } + + @Test + public void listTablesSegmentedNoPrefix() throws IOException, URISyntaxException, StorageException { + String tableBaseName = generateRandomTableName(); + ArrayList tables = new ArrayList(); + for (int m = 0; m < 20; m++) { + String name = String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(m)); + tClient.createTable(name); + tables.add(name); + } + + try { + int currTable = 0; + ResultSegment segment1 = tClient.listTablesSegmented(null, 5, null, null, null); + Assert.assertEquals(5, segment1.getLength()); + for (String s : segment1.getResults()) { + if (s.startsWith(tableBaseName)) { + Assert.assertEquals(s, + String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(currTable))); + currTable++; + } + } + + ResultSegment segment2 = tClient.listTablesSegmented(null, 5, segment1.getContinuationToken(), + null, null); + Assert.assertEquals(5, segment2.getLength()); + for (String s : segment2.getResults()) { + if (s.startsWith(tableBaseName)) { + Assert.assertEquals(s, + String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(currTable))); + currTable++; + } + } + + ResultSegment segment3 = tClient.listTablesSegmented(null, 5, segment2.getContinuationToken(), + null, null); + Assert.assertEquals(5, segment3.getLength()); + for (String s : segment3.getResults()) { + if (s.startsWith(tableBaseName)) { + Assert.assertEquals(s, + String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(currTable))); + currTable++; + } + + } + } + finally { + for (String s : tables) { + tClient.deleteTable(s); + } + } + } + + @Test + public void listTablesWithIterator() throws IOException, URISyntaxException, StorageException { + String tableBaseName = generateRandomTableName(); + ArrayList tables = new ArrayList(); + for (int m = 0; m < 20; m++) { + String name = String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(m)); + tClient.createTable(name); + tables.add(name); + } + + try { + // With prefix + int currTable = 0; + for (String s : tClient.listTables(tableBaseName, null, null)) { + Assert.assertEquals(s, + String.format("%s%s", tableBaseName, new DecimalFormat("#0000").format(currTable))); + currTable++; + } + + Assert.assertEquals(20, currTable); + + // Without prefix + currTable = 0; + for (String s : tClient.listTables()) { + if (s.startsWith(tableBaseName)) { + currTable++; + } + } + + Assert.assertEquals(20, currTable); + } + finally { + for (String s : tables) { + tClient.deleteTable(s); + } + } + } + + @Test + public void tableCreateAndAttemptCreateOnceExists() throws StorageException { + String tableName = generateRandomTableName(); + try { + tClient.createTable(tableName); + Assert.assertTrue(tClient.doesTableExist(tableName)); + + // Should fail as it already exists + try { + tClient.createTable(tableName); + fail(); + } + catch (StorageException ex) { + Assert.assertEquals(ex.getErrorCode(), "TableAlreadyExists"); + } + } + finally { + // cleanup + tClient.deleteTableIfExists(tableName); + } + } + + @Test + public void tableCreateExistsAndDelete() throws StorageException { + String tableName = generateRandomTableName(); + try { + Assert.assertTrue(tClient.createTableIfNotExists(tableName)); + Assert.assertTrue(tClient.doesTableExist(tableName)); + Assert.assertTrue(tClient.deleteTableIfExists(tableName)); + } + finally { + // cleanup + tClient.deleteTableIfExists(tableName); + } + } + + @Test + public void tableCreateIfNotExists() throws StorageException { + String tableName = generateRandomTableName(); + try { + Assert.assertTrue(tClient.createTableIfNotExists(tableName)); + Assert.assertTrue(tClient.doesTableExist(tableName)); + Assert.assertFalse(tClient.createTableIfNotExists(tableName)); + } + finally { + // cleanup + tClient.deleteTableIfExists(tableName); + } + } + + @Test + public void tableDeleteIfExists() throws StorageException { + String tableName = generateRandomTableName(); + + Assert.assertFalse(tClient.deleteTableIfExists(tableName)); + + tClient.createTable(tableName); + Assert.assertTrue(tClient.doesTableExist(tableName)); + Assert.assertTrue(tClient.deleteTableIfExists(tableName)); + Assert.assertFalse(tClient.deleteTableIfExists(tableName)); + } + + @Test + public void tableDeleteWhenExistAndNotExists() throws StorageException { + String tableName = generateRandomTableName(); + try { + // Should fail as it doesnt already exists + try { + tClient.deleteTable(tableName); + fail(); + } + catch (StorageException ex) { + Assert.assertEquals(ex.getMessage(), "Not Found"); + } + + tClient.createTable(tableName); + Assert.assertTrue(tClient.doesTableExist(tableName)); + tClient.deleteTable(tableName); + Assert.assertFalse(tClient.doesTableExist(tableName)); + } + finally { + tClient.deleteTableIfExists(tableName); + } + } + + @Test + public void tableDoesTableExist() throws StorageException { + String tableName = generateRandomTableName(); + try { + Assert.assertFalse(tClient.doesTableExist(tableName)); + Assert.assertTrue(tClient.createTableIfNotExists(tableName)); + Assert.assertTrue(tClient.doesTableExist(tableName)); + } + finally { + // cleanup + tClient.deleteTableIfExists(tableName); + } + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableEscapingTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableEscapingTests.java new file mode 100644 index 0000000000000..ce90b73f1af4b --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableEscapingTests.java @@ -0,0 +1,231 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Escaping Tests + */ +public class TableEscapingTests extends TableTestBase { + @Test + public void emptyString() throws StorageException { + doEscapeTest("", false); + } + + @Test + public void emptyStringBatch() throws StorageException { + doEscapeTest("", true); + } + + @Test + public void randomChars() throws StorageException { + doEscapeTest("!$'\"()*+,;=", false); + } + + @Test + public void randomCharsBatch() throws StorageException { + doEscapeTest("!$'\"()*+,;=", true); + } + + @Test + public void regularPKInQuery() throws StorageException { + doQueryEscapeTest("data"); + } + + @Test + public void specialChars() throws StorageException { + doEscapeTest("\\ // @ ? ", false); + doEscapeTest("", false); + doEscapeTest("", false); + doEscapeTest("!<", false); + doEscapeTest("", false); + doEscapeTest("", false); + doEscapeTest("", false); + doEscapeTest("!<", false); + doEscapeTest(" query = TableQuery.from(testSuiteTableName, class1.class).where( + String.format("(PartitionKey eq '%s') and (A eq '%s')", ref.getPartitionKey(), data)); + + int count = 0; + + for (class1 ent : tClient.execute(query)) { + count++; + Assert.assertEquals(ent.getA(), ref.getA()); + Assert.assertEquals(ent.getB(), ref.getB()); + Assert.assertEquals(ent.getC(), ref.getC()); + Assert.assertEquals(ent.getPartitionKey(), ref.getPartitionKey()); + Assert.assertEquals(ent.getRowKey(), ref.getRowKey()); + } + + Assert.assertEquals(count, 1); + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableOperationTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableOperationTests.java new file mode 100644 index 0000000000000..4beb6ebc4ed21 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableOperationTests.java @@ -0,0 +1,591 @@ +/** + * Copyright 2011 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.services.table.client; + +import static org.junit.Assert.*; + +import java.net.HttpURLConnection; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Operation Tests + */ +public class TableOperationTests extends TableTestBase { + @Test + public void delete() throws StorageException { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + TableOperation op = TableOperation.insert(ref); + + tClient.execute(testSuiteTableName, op); + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + + TableResult res2 = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertTrue(res2.getResult() == null); + } + + @Test + public void deleteFail() throws StorageException { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + String oldEtag = ref.getEtag(); + + // update entity + ref.setA("updated"); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + ref.setEtag(oldEtag); + + try { + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Precondition Failed"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The update condition specified in the request was not satisfied.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "UpdateConditionNotSatisfied"); + } + + TableResult res2 = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + ref = res2.getResultAsType(); + // actually delete it + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + + // now try to delete it and fail + try { + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Not Found"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The specified resource does not exist.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "ResourceNotFound"); + } + } + + @Test + public void emptyRetrieve() throws StorageException { + class1 ref = new class1(); + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertNull(res.getResult()); + Assert.assertEquals(res.getHttpStatusCode(), HttpURLConnection.HTTP_NOT_FOUND); + } + + @Test + public void insertOrMerge() throws StorageException { + class1 baseEntity = new class1(); + baseEntity.setA("foo_A"); + baseEntity.setB("foo_B"); + baseEntity.setC("foo_C"); + baseEntity.setD(new byte[] { 0, 1, 2 }); + baseEntity.setPartitionKey("jxscl_odata"); + baseEntity.setRowKey(UUID.randomUUID().toString()); + + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + // Insert or merge Entity - ENTITY DOES NOT EXIST NOW. + TableResult insertResult = tClient.execute(testSuiteTableName, TableOperation.insertOrMerge(baseEntity)); + + Assert.assertEquals(insertResult.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // Insert or replace Entity - ENTITY EXISTS -> WILL REPLACE + tClient.execute(testSuiteTableName, TableOperation.insertOrMerge(secondEntity)); + + // Retrieve entity + TableResult queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + + DynamicTableEntity retrievedEntity = queryResult. getResultAsType(); + + Assert.assertNotNull("Property A", retrievedEntity.getProperties().get("A")); + Assert.assertEquals(baseEntity.getA(), retrievedEntity.getProperties().get("A").getValueAsString()); + + Assert.assertNotNull("Property B", retrievedEntity.getProperties().get("B")); + Assert.assertEquals(baseEntity.getB(), retrievedEntity.getProperties().get("B").getValueAsString()); + + Assert.assertNotNull("Property C", retrievedEntity.getProperties().get("C")); + Assert.assertEquals(baseEntity.getC(), retrievedEntity.getProperties().get("C").getValueAsString()); + + Assert.assertNotNull("Property D", retrievedEntity.getProperties().get("D")); + Assert.assertTrue(Arrays.equals(baseEntity.getD(), retrievedEntity.getProperties().get("D") + .getValueAsByteArray())); + + // Validate New properties exist + Assert.assertNotNull("Property L", retrievedEntity.getProperties().get("L")); + Assert.assertEquals(secondEntity.getL(), retrievedEntity.getProperties().get("L").getValueAsString()); + + Assert.assertNotNull("Property M", retrievedEntity.getProperties().get("M")); + Assert.assertEquals(secondEntity.getM(), retrievedEntity.getProperties().get("M").getValueAsString()); + + Assert.assertNotNull("Property N", retrievedEntity.getProperties().get("N")); + Assert.assertEquals(secondEntity.getN(), retrievedEntity.getProperties().get("N").getValueAsString()); + + Assert.assertNotNull("Property O", retrievedEntity.getProperties().get("O")); + Assert.assertEquals(secondEntity.getO(), retrievedEntity.getProperties().get("O").getValueAsString()); + } + + @Test + public void insertOrReplace() throws StorageException { + class1 baseEntity = new class1(); + baseEntity.setA("foo_A"); + baseEntity.setB("foo_B"); + baseEntity.setC("foo_C"); + baseEntity.setD(new byte[] { 0, 1, 2 }); + baseEntity.setPartitionKey("jxscl_odata"); + baseEntity.setRowKey(UUID.randomUUID().toString()); + + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + // Insert or replace Entity - ENTITY DOES NOT EXIST NOW. + TableResult insertResult = tClient.execute(testSuiteTableName, TableOperation.insertOrReplace(baseEntity)); + + Assert.assertEquals(insertResult.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // Insert or replace Entity - ENTITY EXISTS -> WILL REPLACE + tClient.execute(testSuiteTableName, TableOperation.insertOrReplace(secondEntity)); + + // Retrieve entity + TableResult queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + + DynamicTableEntity retrievedEntity = queryResult.getResultAsType(); + + // Validate old properties dont exist + Assert.assertTrue(retrievedEntity.getProperties().get("A") == null); + Assert.assertTrue(retrievedEntity.getProperties().get("B") == null); + Assert.assertTrue(retrievedEntity.getProperties().get("C") == null); + Assert.assertTrue(retrievedEntity.getProperties().get("D") == null); + + // Validate New properties exist + Assert.assertNotNull("Property L", retrievedEntity.getProperties().get("L")); + Assert.assertEquals(secondEntity.getL(), retrievedEntity.getProperties().get("L").getValueAsString()); + + Assert.assertNotNull("Property M", retrievedEntity.getProperties().get("M")); + Assert.assertEquals(secondEntity.getM(), retrievedEntity.getProperties().get("M").getValueAsString()); + + Assert.assertNotNull("Property N", retrievedEntity.getProperties().get("N")); + Assert.assertEquals(secondEntity.getN(), retrievedEntity.getProperties().get("N").getValueAsString()); + + Assert.assertNotNull("Property O", retrievedEntity.getProperties().get("O")); + Assert.assertEquals(secondEntity.getO(), retrievedEntity.getProperties().get("O").getValueAsString()); + } + + @Test + public void merge() throws StorageException { + // Insert base entity + class1 baseEntity = new class1(); + baseEntity.setA("foo_A"); + baseEntity.setB("foo_B"); + baseEntity.setC("foo_C"); + baseEntity.setD(new byte[] { 0, 1, 2 }); + baseEntity.setPartitionKey("jxscl_odata"); + baseEntity.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + tClient.execute(testSuiteTableName, TableOperation.merge(secondEntity)); + + TableResult res2 = tClient.execute(testSuiteTableName, TableOperation.retrieve(secondEntity.getPartitionKey(), + secondEntity.getRowKey(), DynamicTableEntity.class)); + DynamicTableEntity mergedEntity = (DynamicTableEntity) res2.getResult(); + + Assert.assertNotNull("Property A", mergedEntity.getProperties().get("A")); + Assert.assertEquals(baseEntity.getA(), mergedEntity.getProperties().get("A").getValueAsString()); + + Assert.assertNotNull("Property B", mergedEntity.getProperties().get("B")); + Assert.assertEquals(baseEntity.getB(), mergedEntity.getProperties().get("B").getValueAsString()); + + Assert.assertNotNull("Property C", mergedEntity.getProperties().get("C")); + Assert.assertEquals(baseEntity.getC(), mergedEntity.getProperties().get("C").getValueAsString()); + + Assert.assertNotNull("Property D", mergedEntity.getProperties().get("D")); + Assert.assertTrue(Arrays.equals(baseEntity.getD(), mergedEntity.getProperties().get("D").getValueAsByteArray())); + + Assert.assertNotNull("Property L", mergedEntity.getProperties().get("L")); + Assert.assertEquals(secondEntity.getL(), mergedEntity.getProperties().get("L").getValueAsString()); + + Assert.assertNotNull("Property M", mergedEntity.getProperties().get("M")); + Assert.assertEquals(secondEntity.getM(), mergedEntity.getProperties().get("M").getValueAsString()); + + Assert.assertNotNull("Property N", mergedEntity.getProperties().get("N")); + Assert.assertEquals(secondEntity.getN(), mergedEntity.getProperties().get("N").getValueAsString()); + + Assert.assertNotNull("Property O", mergedEntity.getProperties().get("O")); + Assert.assertEquals(secondEntity.getO(), mergedEntity.getProperties().get("O").getValueAsString()); + } + + @Test + public void mergeFail() throws StorageException { + // Insert base entity + class1 baseEntity = new class1(); + baseEntity.setA("foo_A"); + baseEntity.setB("foo_B"); + baseEntity.setC("foo_C"); + baseEntity.setD(new byte[] { 0, 1, 2 }); + baseEntity.setPartitionKey("jxscl_odata"); + baseEntity.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + String oldEtag = baseEntity.getEtag(); + + tClient.execute(testSuiteTableName, TableOperation.merge(secondEntity)); + + secondEntity.setEtag(oldEtag); + secondEntity.setL("updated"); + try { + tClient.execute(testSuiteTableName, TableOperation.merge(secondEntity)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Precondition Failed"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The update condition specified in the request was not satisfied.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "UpdateConditionNotSatisfied"); + } + + // delete entity + TableResult queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + + DynamicTableEntity retrievedEntity = queryResult.getResultAsType(); + tClient.execute(testSuiteTableName, TableOperation.delete(retrievedEntity)); + + try { + tClient.execute(testSuiteTableName, TableOperation.merge(secondEntity)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Not Found"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The specified resource does not exist.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "ResourceNotFound"); + } + } + + @Test + public void retrieveWithoutResolver() throws StorageException { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + @SuppressWarnings("unused") + class1 retrievedEnt = res.getResultAsType(); + + Assert.assertEquals(((class1) res.getResult()).getA(), ref.getA()); + } + + @Test + public void retrieveWithResolver() throws StorageException { + class1 ref = new class1(); + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + TableOperation op = TableOperation.insert(ref); + + tClient.execute(testSuiteTableName, op); + + TableResult res4 = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), new EntityResolver() { + @Override + public String resolve(String partitionKey, String rowKey, Date timeStamp, + HashMap properties, String etag) { + return properties.get("A").getValueAsString(); + } + })); + + Assert.assertEquals(res4.getResult().toString(), ref.getA()); + } + + @Test + public void retrieveWithNullResolver() throws StorageException { + try { + TableOperation.retrieve("foo", "blah", (EntityResolver) null); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "Query requires a valid class type or resolver."); + } + } + + @Test + public void insertFail() throws StorageException { + class1 ref = new class1(); + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + TableOperation op = TableOperation.insert(ref); + + tClient.execute(testSuiteTableName, op); + try { + tClient.execute(testSuiteTableName, op); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Conflict"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The specified entity already exists")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "EntityAlreadyExists"); + } + } + + @Test + public void replace() throws StorageException { + class1 baseEntity = new class1(); + baseEntity.setA("foo_A"); + baseEntity.setB("foo_B"); + baseEntity.setC("foo_C"); + baseEntity.setD(new byte[] { 0, 1, 2 }); + baseEntity.setPartitionKey("jxscl_odata"); + baseEntity.setRowKey(UUID.randomUUID().toString()); + + // Insert entity + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + + TableResult queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + // Retrieve entity + DynamicTableEntity retrievedEntity = queryResult. getResultAsType(); + Assert.assertNotNull("Property D", retrievedEntity.getProperties().get("D")); + Assert.assertTrue(Arrays.equals(baseEntity.getD(), retrievedEntity.getProperties().get("D") + .getValueAsByteArray())); + + // Remove property and update + retrievedEntity.getProperties().remove("D"); + + tClient.execute(testSuiteTableName, TableOperation.replace(retrievedEntity)); + + // Retrieve Entity + queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + + retrievedEntity = queryResult. getResultAsType(); + + // Validate + Assert.assertNotNull("Property A", retrievedEntity.getProperties().get("A")); + Assert.assertEquals(baseEntity.getA(), retrievedEntity.getProperties().get("A").getValueAsString()); + + Assert.assertNotNull("Property B", retrievedEntity.getProperties().get("B")); + Assert.assertEquals(baseEntity.getB(), retrievedEntity.getProperties().get("B").getValueAsString()); + + Assert.assertNotNull("Property C", retrievedEntity.getProperties().get("C")); + Assert.assertEquals(baseEntity.getC(), retrievedEntity.getProperties().get("C").getValueAsString()); + + Assert.assertTrue(retrievedEntity.getProperties().get("D") == null); + } + + @Test + public void replaceFail() throws StorageException { + class1 baseEntity = new class1(); + baseEntity.setA("foo_A"); + baseEntity.setB("foo_B"); + baseEntity.setC("foo_C"); + baseEntity.setD(new byte[] { 0, 1, 2 }); + baseEntity.setPartitionKey("jxscl_odata"); + baseEntity.setRowKey(UUID.randomUUID().toString()); + + // Insert entity + tClient.execute(testSuiteTableName, TableOperation.insert(baseEntity)); + + String oldEtag = baseEntity.getEtag(); + + TableResult queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + + // Retrieve entity + DynamicTableEntity retrievedEntity = queryResult. getResultAsType(); + Assert.assertNotNull("Property D", retrievedEntity.getProperties().get("D")); + Assert.assertTrue(Arrays.equals(baseEntity.getD(), retrievedEntity.getProperties().get("D") + .getValueAsByteArray())); + + // Remove property and update + retrievedEntity.getProperties().remove("D"); + + tClient.execute(testSuiteTableName, TableOperation.replace(retrievedEntity)); + + retrievedEntity.setEtag(oldEtag); + + try { + tClient.execute(testSuiteTableName, TableOperation.replace(retrievedEntity)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Precondition Failed"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The condition specified using HTTP conditional header(s) is not met.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "ConditionNotMet"); + } + + // delete entity + queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + + tClient.execute(testSuiteTableName, TableOperation.delete((DynamicTableEntity) queryResult.getResultAsType())); + + try { + tClient.execute(testSuiteTableName, TableOperation.replace(retrievedEntity)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Not Found"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The specified resource does not exist.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "ResourceNotFound"); + } + } + + @Test + public void insertEntityOver1MB() throws StorageException { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + // 1mb right here + ref.setD(new byte[1024 * 1024]); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + try { + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Bad Request"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The entity is larger than allowed by the Table Service.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "EntityTooLarge"); + } + } + + @Test + public void insertEntityWithPropertyMoreThan255chars() throws StorageException { + DynamicTableEntity ref = new DynamicTableEntity(); + + String propName = ""; + for (int m = 0; m < 255; m++) { + propName.concat(Integer.toString(m % 9)); + } + + ref.getProperties().put(propName, new EntityProperty("test")); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + try { + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Bad Request"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("One of the request inputs is not valid.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "InvalidInput"); + } + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableQueryTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableQueryTests.java new file mode 100644 index 0000000000000..dd46b99bae991 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableQueryTests.java @@ -0,0 +1,427 @@ +/** + * Copyright 2011 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.services.table.client; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.security.InvalidKeyException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.ResponseReceivedEvent; +import com.microsoft.windowsazure.services.core.storage.ResultSegment; +import com.microsoft.windowsazure.services.core.storage.StorageEvent; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.table.client.TableQuery.QueryComparisons; + +/** + * Table Query Tests + */ +public class TableQueryTests extends TableTestBase { + @BeforeClass + public static void setup() throws URISyntaxException, StorageException, InvalidKeyException { + TableTestBase.setup(); + + // Insert 500 entities in Batches to query + for (int i = 0; i < 5; i++) { + TableBatchOperation batch = new TableBatchOperation(); + + for (int j = 0; j < 100; j++) { + class1 ent = generateRandomEnitity("javatables_batch_" + Integer.toString(i)); + ent.setRowKey(String.format("%06d", j)); + batch.insert(ent); + } + + tClient.execute(testSuiteTableName, batch); + } + } + + @Test + public void tableQueryWithDynamicEntity() { + // Create entity to check against + class1 randEnt = TableTestBase.generateRandomEnitity(null); + + final Iterable result = tClient.execute(TableQuery.from(testSuiteTableName, + DynamicTableEntity.class)); + + // Validate results + for (DynamicTableEntity ent : result) { + Assert.assertEquals(ent.getProperties().size(), 4); + Assert.assertEquals(ent.getProperties().get("A").getValueAsString(), randEnt.getA()); + Assert.assertEquals(ent.getProperties().get("B").getValueAsString(), randEnt.getB()); + Assert.assertEquals(ent.getProperties().get("C").getValueAsString(), randEnt.getC()); + Assert.assertTrue(Arrays.equals(ent.getProperties().get("D").getValueAsByteArray(), randEnt.getD())); + } + } + + @Test + public void tableQueryWithProjection() { + // Create entity to check against + class1 randEnt = TableTestBase.generateRandomEnitity(null); + final Iterable result = tClient.execute(TableQuery.from(testSuiteTableName, class1.class).select( + new String[] { "A", "C" })); + + // Validate results + for (class1 ent : result) { + // Validate core properties were sent. + Assert.assertNotNull(ent.getPartitionKey()); + Assert.assertNotNull(ent.getRowKey()); + Assert.assertNotNull(ent.getTimestamp()); + + // Validate correct columsn returned. + Assert.assertEquals(ent.getA(), randEnt.getA()); + Assert.assertEquals(ent.getB(), null); + Assert.assertEquals(ent.getC(), randEnt.getC()); + Assert.assertEquals(ent.getD(), null); + } + } + + @Test + public void ensureSelectOnlySendsReservedColumnsOnce() { + OperationContext opContext = new OperationContext(); + opContext.getResponseReceivedEventHandler().addListener(new StorageEvent() { + + @Override + public void eventOccurred(ResponseReceivedEvent eventArg) { + HttpURLConnection conn = (HttpURLConnection) eventArg.getConnectionObject(); + + String urlString = conn.getURL().toString(); + + Assert.assertEquals(urlString.indexOf("PartitionKey"), urlString.lastIndexOf("PartitionKey")); + Assert.assertEquals(urlString.indexOf("RowKey"), urlString.lastIndexOf("RowKey")); + Assert.assertEquals(urlString.indexOf("Timestamp"), urlString.lastIndexOf("Timestamp")); + } + }); + + final Iterable result = tClient.execute( + TableQuery.from(testSuiteTableName, class1.class).select( + new String[] { "PartitionKey", "RowKey", "Timestamp" }), null, opContext); + + // Validate results + for (class1 ent : result) { + Assert.assertEquals(ent.getA(), null); + Assert.assertEquals(ent.getB(), null); + Assert.assertEquals(ent.getC(), null); + Assert.assertEquals(ent.getD(), null); + } + } + + @Test + public void tableQueryWithReflection() { + // Create entity to check against + class1 randEnt = TableTestBase.generateRandomEnitity(null); + + final Iterable result = tClient.execute(TableQuery.from(testSuiteTableName, class1.class)); + + // Validate results + for (class1 ent : result) { + Assert.assertEquals(ent.getA(), randEnt.getA()); + Assert.assertEquals(ent.getB(), randEnt.getB()); + Assert.assertEquals(ent.getC(), randEnt.getC()); + Assert.assertTrue(Arrays.equals(ent.getD(), randEnt.getD())); + } + } + + @Test + public void tableQueryWithResolver() { + // Create entity to check against + class1 randEnt = TableTestBase.generateRandomEnitity(null); + + final Iterable result = tClient.execute(TableQuery.from(testSuiteTableName, TableServiceEntity.class), + new EntityResolver() { + @Override + public class1 resolve(String partitionKey, String rowKey, Date timeStamp, + HashMap properties, String etag) { + Assert.assertEquals(properties.size(), 4); + class1 ref = new class1(); + ref.setA(properties.get("A").getValueAsString()); + ref.setB(properties.get("B").getValueAsString()); + ref.setC(properties.get("C").getValueAsString()); + ref.setD(properties.get("D").getValueAsByteArray()); + return ref; + } + }); + + // Validate results + for (class1 ent : result) { + Assert.assertEquals(ent.getA(), randEnt.getA()); + Assert.assertEquals(ent.getB(), randEnt.getB()); + Assert.assertEquals(ent.getC(), randEnt.getC()); + Assert.assertTrue(Arrays.equals(ent.getD(), randEnt.getD())); + } + } + + @Test + public void tableQueryWithTake() throws IOException, URISyntaxException, StorageException { + // Create entity to check against + class1 randEnt = TableTestBase.generateRandomEnitity(null); + final ResultSegment result = tClient.executeSegmented(TableQuery.from(testSuiteTableName, class1.class) + .select(new String[] { "A", "C" }).take(25), null); + + int count = 0; + // Validate results + for (class1 ent : result.getResults()) { + count++; + Assert.assertEquals(ent.getA(), randEnt.getA()); + Assert.assertEquals(ent.getB(), null); + Assert.assertEquals(ent.getC(), randEnt.getC()); + Assert.assertEquals(ent.getD(), null); + } + + Assert.assertEquals(count, 25); + } + + @Test + public void tableQueryWithFilter() throws StorageException { + class1 randEnt = TableTestBase.generateRandomEnitity(null); + TableQuery query = TableQuery.from(testSuiteTableName, class1.class).where( + String.format("(PartitionKey eq '%s') and (RowKey ge '%s')", "javatables_batch_1", "000050")); + + int count = 0; + + for (class1 ent : tClient.execute(query)) { + Assert.assertEquals(ent.getA(), randEnt.getA()); + Assert.assertEquals(ent.getB(), randEnt.getB()); + Assert.assertEquals(ent.getC(), randEnt.getC()); + Assert.assertEquals(ent.getPartitionKey(), "javatables_batch_1"); + Assert.assertEquals(ent.getRowKey(), String.format("%06d", count + 50)); + count++; + } + + Assert.assertEquals(count, 50); + } + + @Test + public void tableQueryWithContinuation() throws StorageException { + class1 randEnt = TableTestBase.generateRandomEnitity(null); + TableQuery query = TableQuery.from(testSuiteTableName, class1.class) + .where(String.format("(PartitionKey ge '%s') and (RowKey ge '%s')", "javatables_batch_1", "000050")) + .take(25); + + // take will cause the query to return 25 at a time + + int count = 0; + int pk = 1; + for (class1 ent : tClient.execute(query)) { + Assert.assertEquals(ent.getA(), randEnt.getA()); + Assert.assertEquals(ent.getB(), randEnt.getB()); + Assert.assertEquals(ent.getC(), randEnt.getC()); + Assert.assertEquals(ent.getPartitionKey(), "javatables_batch_" + Integer.toString(pk)); + Assert.assertEquals(ent.getRowKey(), String.format("%06d", count % 50 + 50)); + count++; + + if (count % 50 == 0) { + pk++; + } + } + + Assert.assertEquals(count, 200); + } + + @Test + public void testQueryWithNullClassType() throws StorageException { + try { + TableQuery.from(testSuiteTableName, null); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "Query requires a valid class type."); + } + } + + @Test + public void testQueryWithInvalidTakeCount() throws StorageException { + try { + TableQuery.from(testSuiteTableName, TableServiceEntity.class).take(0); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "Take count must be positive and greater than 0."); + } + + try { + TableQuery.from(testSuiteTableName, TableServiceEntity.class).take(-1); + } + catch (IllegalArgumentException ex) { + Assert.assertEquals(ex.getMessage(), "Take count must be positive and greater than 0."); + } + } + + @Test + public void tableInvalidQuery() throws StorageException, IOException, URISyntaxException { + TableQuery query = TableQuery.from(testSuiteTableName, class1.class).where( + String.format("(PartitionKey ) and (RowKey ge '%s')", "javatables_batch_1", "000050")); + try { + tClient.executeSegmented(query, null); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Bad Request"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("One of the request inputs is not valid.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "InvalidInput"); + } + } + + @Test + public void testQueryOnSupportedTypes() throws StorageException { + // Setup + TableBatchOperation batch = new TableBatchOperation(); + String pk = UUID.randomUUID().toString(); + + ComplexEntity middleRef = null; + + for (int j = 0; j < 100; j++) { + ComplexEntity ent = new ComplexEntity(); + ent.setPartitionKey(pk); + ent.setRowKey(String.format("%04d", j)); + ent.setBinary(new Byte[] { 0x01, 0x02, (byte) j }); + ent.setBinaryPrimitive(new byte[] { 0x01, 0x02, (byte) j }); + ent.setBool(j % 2 == 0 ? true : false); + ent.setBoolPrimitive(j % 2 == 0 ? true : false); + ent.setDateTime(new Date()); + ent.setDouble(j + ((double) j) / 100); + ent.setDoublePrimitive(j + ((double) j) / 100); + ent.setInt32(j); + ent.setInt64((long) j); + ent.setIntegerPrimitive(j); + ent.setLongPrimitive(j); + ent.setGuid(UUID.randomUUID()); + ent.setString(String.format("%04d", j)); + + try { + // Add delay to make times unique + Thread.sleep(100); + } + catch (InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + batch.insert(ent); + if (j == 50) { + middleRef = ent; + } + } + + tClient.execute(testSuiteTableName, batch); + + try { + // 1. Filter on String + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("String", QueryComparisons.GREATER_THAN_OR_EQUAL, "0050"), 50); + + // 2. Filter on UUID + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("Guid", QueryComparisons.EQUAL, middleRef.getGuid()), 1); + + // 3. Filter on Long + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("Int64", QueryComparisons.GREATER_THAN_OR_EQUAL, + middleRef.getInt64()), 50); + + executeQueryAndAssertResults(TableQuery.generateFilterCondition("LongPrimitive", + QueryComparisons.GREATER_THAN_OR_EQUAL, middleRef.getInt64()), 50); + + // 4. Filter on Double + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("Double", QueryComparisons.GREATER_THAN_OR_EQUAL, + middleRef.getDouble()), 50); + + executeQueryAndAssertResults(TableQuery.generateFilterCondition("DoublePrimitive", + QueryComparisons.GREATER_THAN_OR_EQUAL, middleRef.getDouble()), 50); + + // 5. Filter on Integer + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("Int32", QueryComparisons.GREATER_THAN_OR_EQUAL, + middleRef.getInt32()), 50); + + executeQueryAndAssertResults(TableQuery.generateFilterCondition("IntegerPrimitive", + QueryComparisons.GREATER_THAN_OR_EQUAL, middleRef.getInt32()), 50); + + // 6. Filter on Date + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("DateTime", QueryComparisons.GREATER_THAN_OR_EQUAL, + middleRef.getDateTime()), 50); + + // 7. Filter on Boolean + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("Bool", QueryComparisons.EQUAL, middleRef.getBool()), 50); + + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("BoolPrimitive", QueryComparisons.EQUAL, middleRef.getBool()), + 50); + + // 8. Filter on Binary + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("Binary", QueryComparisons.EQUAL, middleRef.getBinary()), 1); + + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("BinaryPrimitive", QueryComparisons.EQUAL, + middleRef.getBinaryPrimitive()), 1); + + // 9. Filter on Binary GTE + executeQueryAndAssertResults( + TableQuery.generateFilterCondition("Binary", QueryComparisons.GREATER_THAN_OR_EQUAL, + middleRef.getBinary()), 50); + + executeQueryAndAssertResults(TableQuery.generateFilterCondition("BinaryPrimitive", + QueryComparisons.GREATER_THAN_OR_EQUAL, middleRef.getBinaryPrimitive()), 50); + + // 10. Complex Filter on Binary GTE + executeQueryAndAssertResults(TableQuery.combineFilters( + TableQuery.generateFilterCondition(TableConstants.PARTITION_KEY, QueryComparisons.EQUAL, + middleRef.getPartitionKey()), + TableQuery.Operators.AND, + TableQuery.generateFilterCondition("Binary", QueryComparisons.GREATER_THAN_OR_EQUAL, + middleRef.getBinary())), 50); + + executeQueryAndAssertResults(TableQuery.generateFilterCondition("BinaryPrimitive", + QueryComparisons.GREATER_THAN_OR_EQUAL, middleRef.getBinaryPrimitive()), 50); + + } + finally { + // cleanup + TableBatchOperation delBatch = new TableBatchOperation(); + TableQuery query = TableQuery.from(testSuiteTableName, ComplexEntity.class).where( + String.format("PartitionKey eq '%s'", pk)); + + for (ComplexEntity e : tClient.execute(query)) { + delBatch.delete(e); + } + + tClient.execute(testSuiteTableName, delBatch); + } + } + + private void executeQueryAndAssertResults(String filter, int expectedResults) { + int count = 0; + TableQuery query = TableQuery.from(testSuiteTableName, ComplexEntity.class).where(filter); + for (@SuppressWarnings("unused") + ComplexEntity e : tClient.execute(query)) { + count++; + } + + Assert.assertEquals(expectedResults, count); + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableSerializerTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableSerializerTests.java new file mode 100644 index 0000000000000..e7380c67c6fe6 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableSerializerTests.java @@ -0,0 +1,269 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Serializer Tests + */ +public class TableSerializerTests extends TableTestBase { + @Test + public void testComplexEntityInsert() throws IOException, URISyntaxException, StorageException { + ComplexEntity ref = new ComplexEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.populateEntity(); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ComplexEntity retrievedComplexRef = res.getResultAsType(); + ref.assertEquality(retrievedComplexRef); + } + + @Test + public void testIgnoreAnnotation() throws IOException, URISyntaxException, StorageException { + // Ignore On Getter + IgnoreOnGetter ignoreGetter = new IgnoreOnGetter(); + ignoreGetter.setPartitionKey("jxscl_odata"); + ignoreGetter.setRowKey(UUID.randomUUID().toString()); + ignoreGetter.setIgnoreString("ignore data"); + + tClient.execute(testSuiteTableName, TableOperation.insert(ignoreGetter)); + + TableResult res = tClient + .execute(testSuiteTableName, TableOperation.retrieve(ignoreGetter.getPartitionKey(), + ignoreGetter.getRowKey(), IgnoreOnGetter.class)); + + IgnoreOnGetter retrievedIgnoreG = res.getResultAsType(); + Assert.assertEquals(retrievedIgnoreG.getIgnoreString(), null); + + // Ignore On Setter + IgnoreOnSetter ignoreSetter = new IgnoreOnSetter(); + ignoreSetter.setPartitionKey("jxscl_odata"); + ignoreSetter.setRowKey(UUID.randomUUID().toString()); + ignoreSetter.setIgnoreString("ignore data"); + + tClient.execute(testSuiteTableName, TableOperation.insert(ignoreSetter)); + + res = tClient + .execute(testSuiteTableName, TableOperation.retrieve(ignoreSetter.getPartitionKey(), + ignoreSetter.getRowKey(), IgnoreOnSetter.class)); + + IgnoreOnSetter retrievedIgnoreS = res.getResultAsType(); + Assert.assertEquals(retrievedIgnoreS.getIgnoreString(), null); + + // Ignore On Getter AndSetter + IgnoreOnGetterAndSetter ignoreGetterSetter = new IgnoreOnGetterAndSetter(); + ignoreGetterSetter.setPartitionKey("jxscl_odata"); + ignoreGetterSetter.setRowKey(UUID.randomUUID().toString()); + ignoreGetterSetter.setIgnoreString("ignore data"); + + tClient.execute(testSuiteTableName, TableOperation.insert(ignoreGetterSetter)); + + res = tClient.execute(testSuiteTableName, TableOperation.retrieve(ignoreGetterSetter.getPartitionKey(), + ignoreGetterSetter.getRowKey(), IgnoreOnGetterAndSetter.class)); + + IgnoreOnGetterAndSetter retrievedIgnoreGS = res.getResultAsType(); + Assert.assertEquals(retrievedIgnoreGS.getIgnoreString(), null); + } + + @Test + public void testNulls() throws IOException, URISyntaxException, StorageException { + ComplexEntity ref = new ComplexEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.populateEntity(); + + // Binary object + ref.setBinary(null); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + ref = res.getResultAsType(); + + Assert.assertNull("Binary should be null", ref.getBinary()); + + // Bool + ref.setBool(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Bool should be null", ref.getBool()); + + // Date + ref.setDateTime(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Date should be null", ref.getDateTime()); + + // Double + ref.setDouble(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Double should be null", ref.getDouble()); + + // UUID + ref.setGuid(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("UUID should be null", ref.getGuid()); + + // Int32 + ref.setInt32(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Int32 should be null", ref.getInt32()); + + // Int64 + ref.setInt64(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Int64 should be null", ref.getInt64()); + + // String + ref.setString(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("String should be null", ref.getString()); + } + + @Test + public void testStoreAsAnnotation() throws IOException, URISyntaxException, StorageException { + StoreAsEntity ref = new StoreAsEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.setStoreAsString("StoreAsOverride Data"); + ref.populateEntity(); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), StoreAsEntity.class)); + + StoreAsEntity retrievedStoreAsRef = res.getResultAsType(); + Assert.assertEquals(retrievedStoreAsRef.getStoreAsString(), ref.getStoreAsString()); + + // Same query with a class without the storeAs annotation + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ComplexEntity retrievedComplexRef = res.getResultAsType(); + Assert.assertEquals(retrievedComplexRef.getString(), ref.getStoreAsString()); + + tClient.execute(testSuiteTableName, TableOperation.delete(retrievedComplexRef)); + } + + @Test + public void testInvalidStoreAsAnnotation() throws IOException, URISyntaxException, StorageException { + InvalidStoreAsEntity ref = new InvalidStoreAsEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.setStoreAsString("StoreAsOverride Data"); + ref.populateEntity(); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), InvalidStoreAsEntity.class)); + + InvalidStoreAsEntity retrievedStoreAsRef = res.getResultAsType(); + Assert.assertEquals(retrievedStoreAsRef.getStoreAsString(), null); + } + + @Test + public void whitespaceTest() throws StorageException { + class1 ref = new class1(); + + ref.setA("B "); + ref.setB(" A "); + ref.setC(" "); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertEquals(((class1) res.getResult()).getA(), ref.getA()); + } + + @Test + public void newLineTest() throws StorageException { + class1 ref = new class1(); + + ref.setA("B "); + ref.setB(" A "); + ref.setC("\r\n"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertEquals(((class1) res.getResult()).getA(), ref.getA()); + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableTestBase.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableTestBase.java new file mode 100644 index 0000000000000..b30f200fa61a9 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableTestBase.java @@ -0,0 +1,599 @@ +/** + * Copyright 2011 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.services.table.client; + +import java.net.URISyntaxException; +import java.security.InvalidKeyException; +import java.util.Arrays; +import java.util.Date; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import com.microsoft.windowsazure.services.blob.client.CloudBlobClient; +import com.microsoft.windowsazure.services.core.storage.CloudStorageAccount; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.queue.client.CloudQueueClient; + +/** + * Table Test Base + */ +public class TableTestBase { + public static boolean USE_DEV_FABRIC = false; + public static final String CLOUD_ACCOUNT_HTTP = "DefaultEndpointsProtocol=http;AccountName=[ACCOUNT NAME];AccountKey=[ACCOUNT KEY]"; + public static final String CLOUD_ACCOUNT_HTTPS = "DefaultEndpointsProtocol=https;AccountName=[ACCOUNT NAME];AccountKey=[ACCOUNT KEY]"; + + public static class class1 extends TableServiceEntity { + public String A; + + public String B; + + public String C; + + public byte[] D; + + public class1() { + // empty ctor + } + + public synchronized String getA() { + return this.A; + } + + public synchronized String getB() { + return this.B; + } + + public synchronized String getC() { + return this.C; + } + + public synchronized byte[] getD() { + return this.D; + } + + public synchronized void setA(final String a) { + this.A = a; + } + + public synchronized void setB(final String b) { + this.B = b; + } + + public synchronized void setC(final String c) { + this.C = c; + } + + public synchronized void setD(final byte[] d) { + this.D = d; + } + } + + public class class2 extends TableServiceEntity { + private String L; + private String M; + + private String N; + + private String O; + + /** + * @return the l + */ + public String getL() { + return this.L; + } + + /** + * @return the m + */ + public String getM() { + return this.M; + } + + /** + * @return the n + */ + public String getN() { + return this.N; + } + + /** + * @return the o + */ + public String getO() { + return this.O; + } + + /** + * @param l + * the l to set + */ + public void setL(String l) { + this.L = l; + } + + /** + * @param m + * the m to set + */ + public void setM(String m) { + this.M = m; + } + + /** + * @param n + * the n to set + */ + public void setN(String n) { + this.N = n; + } + + /** + * @param o + * the o to set + */ + public void setO(String o) { + this.O = o; + } + } + + public static class ComplexEntity extends TableServiceEntity { + private Date dateTime = null; + private Boolean Bool = null; + private boolean BoolPrimitive = false; + private Byte[] Binary = null; + private byte[] binaryPrimitive = null; + private double DoublePrimitive = -1; + private Double Double = null; + private UUID Guid = null; + private int IntegerPrimitive = -1; + private Integer Int32 = null; + private long LongPrimitive = -1L; + private Long Int64 = null; + private String String = null; + + public ComplexEntity() { + // Empty Ctor + } + + public void assertEquality(ComplexEntity other) { + Assert.assertEquals(this.getPartitionKey(), other.getPartitionKey()); + Assert.assertEquals(this.getRowKey(), other.getRowKey()); + + Assert.assertEquals(this.getDateTime(), other.getDateTime()); + Assert.assertEquals(this.getGuid(), other.getGuid()); + Assert.assertEquals(this.getString(), other.getString()); + + Assert.assertEquals(this.getDouble(), other.getDouble()); + Assert.assertEquals(this.getDoublePrimitive(), other.getDoublePrimitive()); + Assert.assertEquals(this.getInt32(), other.getInt32()); + Assert.assertEquals(this.getIntegerPrimitive(), other.getIntegerPrimitive()); + Assert.assertEquals(this.getBool(), other.getBool()); + Assert.assertEquals(this.getBoolPrimitive(), other.getBoolPrimitive()); + Assert.assertEquals(this.getInt64(), other.getInt64()); + Assert.assertEquals(this.getIntegerPrimitive(), other.getIntegerPrimitive()); + Assert.assertTrue(Arrays.equals(this.getBinary(), other.getBinary())); + Assert.assertTrue(Arrays.equals(this.getBinaryPrimitive(), other.getBinaryPrimitive())); + } + + /** + * @return the binary + */ + public Byte[] getBinary() { + return this.Binary; + } + + /** + * @return the binaryPrimitive + */ + public byte[] getBinaryPrimitive() { + return this.binaryPrimitive; + } + + /** + * @return the bool + */ + public Boolean getBool() { + return this.Bool; + } + + /** + * @return the bool + */ + public boolean getBoolPrimitive() { + return this.BoolPrimitive; + } + + /** + * @return the dateTime + */ + public Date getDateTime() { + return this.dateTime; + } + + /** + * @return the double + */ + public Double getDouble() { + return this.Double; + } + + /** + * @return the doublePrimitive + */ + public double getDoublePrimitive() { + return this.DoublePrimitive; + } + + /** + * @return the guid + */ + public UUID getGuid() { + return this.Guid; + } + + /** + * @return the int32 + */ + public Integer getInt32() { + return this.Int32; + } + + /** + * @return the int64 + */ + public Long getInt64() { + return this.Int64; + } + + /** + * @return the integerPrimitive + */ + public int getIntegerPrimitive() { + return this.IntegerPrimitive; + } + + /** + * @return the longPrimitive + */ + public long getLongPrimitive() { + return this.LongPrimitive; + } + + /** + * @return the string + */ + public String getString() { + return this.String; + } + + public void populateEntity() { + this.setBinary(new Byte[] { 1, 2, 3, 4 }); + this.setBinaryPrimitive(new byte[] { 1, 2, 3, 4 }); + this.setBool(true); + this.setBoolPrimitive(true); + this.setDateTime(new Date()); + this.setDouble(2342.2342); + this.setDoublePrimitive(2349879.2342); + this.setInt32(2342); + this.setInt64((long) 87987987); + this.setIntegerPrimitive(2342); + this.setLongPrimitive(87987987); + this.setGuid(UUID.randomUUID()); + this.setString("foo"); + } + + /** + * @param binary + * the binary to set + */ + public void setBinary(final Byte[] binary) { + this.Binary = binary; + } + + /** + * @param binaryPrimitive + * the binaryPrimitive to set + */ + public void setBinaryPrimitive(byte[] binaryPrimitive) { + this.binaryPrimitive = binaryPrimitive; + } + + /** + * @param bool + * the bool to set + */ + public void setBool(final Boolean bool) { + this.Bool = bool; + } + + /** + * @param boolPrimitive + * the boolPrimitive to set + */ + public void setBoolPrimitive(boolean boolPrimitive) { + this.BoolPrimitive = boolPrimitive; + } + + /** + * @param dateTime + * the dateTime to set + */ + public void setDateTime(final Date dateTime) { + this.dateTime = dateTime; + } + + /** + * @param d + * the double to set + */ + public void setDouble(final Double d) { + this.Double = d; + } + + /** + * @param doublePrimitive + * the doublePrimitive to set + */ + public void setDoublePrimitive(double doublePrimitive) { + this.DoublePrimitive = doublePrimitive; + } + + /** + * @param guid + * the guid to set + */ + public void setGuid(final UUID guid) { + this.Guid = guid; + } + + /** + * @param int32 + * the int32 to set + */ + public void setInt32(final Integer int32) { + this.Int32 = int32; + } + + /** + * @param int64 + * the int64 to set + */ + public void setInt64(final Long int64) { + this.Int64 = int64; + } + + /** + * @param integerPrimitive + * the integerPrimitive to set + */ + public void setIntegerPrimitive(int integerPrimitive) { + this.IntegerPrimitive = integerPrimitive; + } + + /** + * @param longPrimitive + * the longPrimitive to set + */ + public void setLongPrimitive(long longPrimitive) { + this.LongPrimitive = longPrimitive; + } + + /** + * @param string + * the string to set + */ + public void setString(final String string) { + this.String = string; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(java.lang.String.format("%s:%s\n", "PK", this.getPartitionKey())); + builder.append(java.lang.String.format("%s:%s\n", "RK", this.getRowKey())); + builder.append(java.lang.String.format("%s:%s\n", "Timestamp", this.getTimestamp())); + builder.append(java.lang.String.format("%s:%s\n", "etag", this.getEtag())); + + builder.append(java.lang.String.format("%s:%s\n", "DateTime", this.getDateTime())); + builder.append(java.lang.String.format("%s:%s\n", "Bool", this.getBool())); + builder.append(java.lang.String.format("%s:%s\n", "Binary", this.getBinary())); + builder.append(java.lang.String.format("%s:%s\n", "Double", this.getDouble())); + builder.append(java.lang.String.format("%s:%s\n", "Guid", this.getGuid())); + builder.append(java.lang.String.format("%s:%s\n", "Int32", this.getInt32())); + builder.append(java.lang.String.format("%s:%s\n", "Int64", this.getInt64())); + builder.append(java.lang.String.format("%s:%s\n", "String", this.getString())); + + return builder.toString(); + } + } + + public static class IgnoreOnGetter extends class1 { + private String tString = null; + + /** + * @return the string + */ + @Ignore + public String getIgnoreString() { + return this.tString; + } + + /** + * @param string + * the string to set + */ + + public void setIgnoreString(final String string) { + this.tString = string; + } + } + + public static class IgnoreOnGetterAndSetter extends class1 { + private String tString = null; + + /** + * @return the string + */ + @Ignore + public String getIgnoreString() { + return this.tString; + } + + /** + * @param string + * the string to set + */ + @Ignore + public void setIgnoreString(final String string) { + this.tString = string; + } + } + + public static class IgnoreOnSetter extends class1 { + private String tString = null; + + /** + * @return the string + */ + public String getIgnoreString() { + return this.tString; + } + + /** + * @param string + * the string to set + */ + @Ignore + public void setIgnoreString(final String string) { + this.tString = string; + } + } + + public static class StoreAsEntity extends ComplexEntity { + private String storeAsString = null; + + /** + * @return the string + */ + @StoreAs(name = "String") + public String getStoreAsString() { + return this.storeAsString; + } + + /** + * @param string + * the string to set + */ + @StoreAs(name = "String") + public void setStoreAsString(final String string) { + this.storeAsString = string; + } + } + + public static class InvalidStoreAsEntity extends ComplexEntity { + private String storeAsString = null; + + /** + * @return the string + */ + @StoreAs(name = "PartitionKey") + public String getStoreAsString() { + return this.storeAsString; + } + + /** + * @param string + * the string to set + */ + @StoreAs(name = "PartitionKey") + public void setStoreAsString(final String string) { + this.storeAsString = string; + } + } + + public static class TableEnt extends TableServiceEntity { + String TableName; + + /** + * @return the tableName + */ + public String getTableName() { + return this.TableName; + } + + /** + * @param tableName + * the tableName to set + */ + public void setTableName(String tableName) { + this.TableName = tableName; + } + } + + protected static CloudStorageAccount httpAcc; + protected static CloudBlobClient bClient; + protected static CloudQueueClient qClient; + protected static CloudTableClient tClient; + protected static String testSuiteTableName = generateRandomTableName(); + + public static class1 generateRandomEnitity(String pk) { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey(pk); + ref.setRowKey(UUID.randomUUID().toString()); + return ref; + } + + @BeforeClass + public static void setup() throws URISyntaxException, StorageException, InvalidKeyException { + + // UNCOMMENT TO USE FIDDLER + // System.setProperty("http.proxyHost", "localhost"); + // System.setProperty("http.proxyPort", "8888"); + // System.setProperty("https.proxyHost", "localhost"); + // System.setProperty("https.proxyPort", "8888"); + if (USE_DEV_FABRIC) { + httpAcc = CloudStorageAccount.getDevelopmentStorageAccount(); + } + else { + httpAcc = CloudStorageAccount.parse(CLOUD_ACCOUNT_HTTP); + } + + bClient = httpAcc.createCloudBlobClient(); + tClient = httpAcc.createCloudTableClient(); + qClient = httpAcc.createCloudQueueClient(); + testSuiteTableName = generateRandomTableName(); + tClient.createTable(testSuiteTableName); + } + + @AfterClass + public static void teardown() throws StorageException { + tClient.deleteTable(testSuiteTableName); + } + + protected static String generateRandomTableName() { + String tableName = "table" + UUID.randomUUID().toString(); + return tableName.replace("-", ""); + } +}