diff --git a/microsoft-azure-api/pom.xml b/microsoft-azure-api/pom.xml
index 5da3a62a9e6c..d9b0c4c4f2cf 100644
--- a/microsoft-azure-api/pom.xml
+++ b/microsoft-azure-api/pom.xml
@@ -80,6 +80,16 @@
commons-logging1.1.1
+
+ javax.mail
+ mail
+ 1.4
+
+
+ org.apache.commons
+ commons-lang3
+ 3.1
+
@@ -104,6 +114,7 @@
org.apache.maven.pluginsmaven-compiler-plugin
+ 2.3.21.61.6
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/serviceruntime/XmlRoleEnvironmentDataDeserializer.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/serviceruntime/XmlRoleEnvironmentDataDeserializer.java
index 4924bc65439b..8f27d2a1e346 100644
--- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/serviceruntime/XmlRoleEnvironmentDataDeserializer.java
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/serviceruntime/XmlRoleEnvironmentDataDeserializer.java
@@ -85,7 +85,7 @@ private Map translateRoles(RoleEnvironmentInfo environmentInfo, Ro
for (RoleInfo roleInfo : environmentInfo.getRoles().getRole()) {
Map instances = translateRoleInstances(roleInfo.getInstances());
- if (roleInfo.getName() == currentRole) {
+ if (roleInfo.getName().equals(currentRole)) {
instances.put(currentInstance.getId(), currentInstance);
}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java
index 197537a9db82..8d9d63bc8972 100644
--- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/Exports.java
@@ -2,20 +2,21 @@
* 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
+ * 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.
+ * 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.blob;
import com.microsoft.windowsazure.services.blob.implementation.BlobExceptionProcessor;
import com.microsoft.windowsazure.services.blob.implementation.BlobRestProxy;
+import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter;
import com.microsoft.windowsazure.services.blob.implementation.SharedKeyFilter;
import com.microsoft.windowsazure.services.blob.implementation.SharedKeyLiteFilter;
import com.microsoft.windowsazure.services.core.Builder;
@@ -28,5 +29,6 @@ public void register(Builder.Registry registry) {
registry.add(BlobRestProxy.class);
registry.add(SharedKeyLiteFilter.class);
registry.add(SharedKeyFilter.class);
+ registry.add(ISO8601DateConverter.class);
}
}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java
index 3907ac20db40..5bba95ef4599 100644
--- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/BlobRestProxy.java
@@ -69,10 +69,10 @@
import com.microsoft.windowsazure.services.blob.models.SetContainerMetadataOptions;
import com.microsoft.windowsazure.services.core.ServiceException;
import com.microsoft.windowsazure.services.core.ServiceFilter;
+import com.microsoft.windowsazure.services.core.utils.CommaStringBuilder;
import com.microsoft.windowsazure.services.core.utils.pipeline.ClientFilterAdapter;
import com.microsoft.windowsazure.services.core.utils.pipeline.HttpURLConnectionClient;
import com.microsoft.windowsazure.services.core.utils.pipeline.PipelineHelpers;
-import com.microsoft.windowsazure.services.core.utils.pipeline.PipelineHelpers.EnumCommaStringBuilder;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.WebResource.Builder;
@@ -155,18 +155,18 @@ private HashMap getMetadataFromHeaders(ClientResponse response)
}
private WebResource addOptionalBlobListingIncludeQueryParam(ListBlobsOptions options, WebResource webResource) {
- EnumCommaStringBuilder sb = new EnumCommaStringBuilder();
+ CommaStringBuilder sb = new CommaStringBuilder();
sb.addValue(options.isIncludeSnapshots(), "snapshots");
sb.addValue(options.isIncludeUncommittedBlobs(), "uncommittedblobs");
sb.addValue(options.isIncludeMetadata(), "metadata");
- webResource = addOptionalQueryParam(webResource, "include", sb.getValue());
+ webResource = addOptionalQueryParam(webResource, "include", sb.toString());
return webResource;
}
private WebResource addOptionalContainerIncludeQueryParam(ListContainersOptions options, WebResource webResource) {
- EnumCommaStringBuilder sb = new EnumCommaStringBuilder();
+ CommaStringBuilder sb = new CommaStringBuilder();
sb.addValue(options.isIncludeMetadata(), "metadata");
- webResource = addOptionalQueryParam(webResource, "include", sb.getValue());
+ webResource = addOptionalQueryParam(webResource, "include", sb.toString());
return webResource;
}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateAdapter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateAdapter.java
index 39e67d931a38..af860e50936d 100644
--- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateAdapter.java
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateAdapter.java
@@ -25,11 +25,11 @@ public class ContainerACLDateAdapter extends XmlAdapter {
@Override
public Date unmarshal(String arg0) throws Exception {
- return new ContainerACLDateConverter().parse(arg0);
+ return new ISO8601DateConverter().parse(arg0);
}
@Override
public String marshal(Date arg0) throws Exception {
- return new ContainerACLDateConverter().format(arg0);
+ return new ISO8601DateConverter().format(arg0);
}
}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateConverter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateConverter.java
deleted file mode 100644
index 209a345d26a0..000000000000
--- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateConverter.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/**
- * 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.blob.implementation;
-
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-
-/*
- * "not quite" ISO 8601 date time conversion routines
- */
-public class ContainerACLDateConverter {
- // Note: because of the trailing "0000000", this is not quite ISO 8601 compatible
- private static final String DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'";
-
- public String format(Date date) {
- return getFormat().format(date);
- }
-
- public Date parse(String date) throws ParseException {
- return getFormat().parse(date);
- }
-
- public Date parseNoThrow(String date) {
- try {
- return parse(date);
- }
- catch (ParseException e) {
- return null;
- }
- }
-
- private DateFormat getFormat() {
- DateFormat iso8601Format = new SimpleDateFormat(DATETIME_PATTERN, Locale.US);
- iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT"));
- return iso8601Format;
- }
-}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverter.java
new file mode 100644
index 000000000000..e796bf00794c
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverter.java
@@ -0,0 +1,59 @@
+/**
+ * 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.blob.implementation;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+/*
+ * "not quite" ISO 8601 date time conversion routines
+ */
+public class ISO8601DateConverter {
+ // Note: because of the trailing "0000000", this is not quite ISO 8601 compatible
+ private static final String DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSSSSSS'Z'";
+ private static final String SHORT_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+ public String format(Date date) {
+ return getFormat().format(date);
+ }
+
+ public Date parse(String date) throws ParseException {
+ if (date == null)
+ return null;
+
+ // Sometimes, the date comes back without the ".SSSSSSS" part (presumably when the decimal value
+ // of the date is "0". Use the short format in that case.
+ if (date.indexOf('.') < 0)
+ return getShortFormat().parse(date);
+ else
+ return getFormat().parse(date);
+ }
+
+ private DateFormat getFormat() {
+ DateFormat iso8601Format = new SimpleDateFormat(DATETIME_PATTERN, Locale.US);
+ iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return iso8601Format;
+ }
+
+ private DateFormat getShortFormat() {
+ DateFormat iso8601Format = new SimpleDateFormat(SHORT_DATETIME_PATTERN, Locale.US);
+ iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT"));
+ return iso8601Format;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java
index 6481932c7cde..9072ead43564 100644
--- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyFilter.java
@@ -2,15 +2,15 @@
* 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
+ * 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.
+ * 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.blob.implementation;
@@ -44,6 +44,18 @@ public SharedKeyFilter(@Named(BlobConfiguration.ACCOUNT_NAME) String accountName
this.signer = new HmacSHA256Sign(accountKey);
}
+ protected String getHeader(ClientRequest cr, String headerKey) {
+ return SharedKeyUtils.getHeader(cr, headerKey);
+ }
+
+ protected HmacSHA256Sign getSigner() {
+ return signer;
+ }
+
+ protected String getAccountName() {
+ return accountName;
+ }
+
@Override
public ClientResponse handle(ClientRequest cr) throws ClientHandlerException {
// Only sign if no other filter is handling authorization
@@ -60,6 +72,7 @@ public ClientResponse handle(ClientRequest cr) throws ClientHandlerException {
return this.getNext().handle(cr);
}
+ @Override
public void onBeforeStreamingEntity(ClientRequest clientRequest) {
// All headers should be known at this point, time to sign!
sign(clientRequest);
@@ -105,11 +118,11 @@ public void sign(ClientRequest cr) {
cr.getHeaders().putSingle("Authorization", "SharedKey " + this.accountName + ":" + signature);
}
- private void addOptionalDateHeader(ClientRequest cr) {
+ protected void addOptionalDateHeader(ClientRequest cr) {
String date = getHeader(cr, "Date");
if (date == "") {
date = new RFC1123DateConverter().format(new Date());
- cr.getHeaders().add("Date", date);
+ cr.getHeaders().putSingle("Date", date);
}
}
@@ -209,8 +222,4 @@ private String getCanonicalizedResource(ClientRequest cr) {
return result;
}
-
- private String getHeader(ClientRequest cr, String headerKey) {
- return SharedKeyUtils.getHeader(cr, headerKey);
- }
}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java
index 0134195d5c3a..37ff1321d5d9 100644
--- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/SharedKeyLiteFilter.java
@@ -2,15 +2,15 @@
* 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
+ * 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.
+ * 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.blob.implementation;
@@ -59,6 +59,7 @@ public ClientResponse handle(ClientRequest cr) throws ClientHandlerException {
return this.getNext().handle(cr);
}
+ @Override
public void onBeforeStreamingEntity(ClientRequest clientRequest) {
// All headers should be known at this point, time to sign!
sign(clientRequest);
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 970cc3850ead..c2a741ef3ed2 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 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 000000000000..b471594261c6
--- /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 000000000000..cfe58e086f67
--- /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 000000000000..bf246941e4ce
--- /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.
+ *
.
+ *
+ * @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 000000000000..254581502e2b
--- /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 000000000000..ed89049797d1
--- /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 000000000000..9f0f5c573bec
--- /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 000000000000..24da5f2adc58
--- /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 000000000000..52dbf5ecdccb
--- /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 extends TableEntity> 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 extends TableEntity> 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 extends TableEntity> 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 000000000000..7918530c85f3
--- /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.
+ *
+ * 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 000000000000..596e519612c3
--- /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 extends TableOperation> 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 extends TableOperation> 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 extends TableEntity> 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 000000000000..48d3f3d56ea0
--- /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 000000000000..85dc20a8c37b
--- /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
+ *
+ * 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 000000000000..c9c72e0856dd
--- /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 extends TableEntity> 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 000000000000..a1ceccabd332
--- /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 000000000000..a54279ae0ce1
--- /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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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 000000000000..4642bec1b298
--- /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 000000000000..1a4d210526f2
--- /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 000000000000..848e7b2afc0b
--- /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 000000000000..95e0621b63ed
--- /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 000000000000..d958bb328444
--- /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
+ *
+ * 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 Type
+ *
EdmType Value
+ *
Java Type
+ *
Description
+ *
+ *
+ *
Edm.Binary
+ *
{@link EdmType#BINARY}
+ *
byte[], Byte[]
+ *
An array of bytes up to 64 KB in size.
+ *
+ *
+ *
Edm.Boolean
+ *
{@link EdmType#BOOLEAN}
+ *
boolean, Boolean
+ *
A Boolean value.
+ *
+ *
+ *
Edm.Byte
+ *
{@link EdmType#BYTE}
+ *
boolean, Boolean
+ *
A Boolean value.
+ *
+ *
+ *
Edm.DateTime
+ *
{@link EdmType#DATE_TIME}
+ *
Date
+ *
A 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, Double
+ *
A 64-bit double-precision floating point value.
+ *
+ *
+ *
Edm.Guid
+ *
{@link EdmType#GUID}
+ *
UUID
+ *
A 128-bit globally unique identifier.
+ *
+ *
+ *
Edm.Int32
+ *
{@link EdmType#INT32}
+ *
int, Integer
+ *
A 32-bit integer value.
+ *
+ *
+ *
Edm.Int64
+ *
{@link EdmType#INT64}
+ *
long, Long
+ *
A 64-bit integer value.
+ *
+ *
+ *
Edm.String
+ *
{@link EdmType#STRING}
+ *
String
+ *
A UTF-16-encoded value. String values may be up to 64 KB in size.
+ *
+ * @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 000000000000..04f065a4407d
--- /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 000000000000..3fbf519729c4
--- /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/main/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriter.java
new file mode 100644
index 000000000000..c5803bfb5411
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriter.java
@@ -0,0 +1,307 @@
+package com.microsoft.windowsazure.services.table.implementation;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import javax.inject.Inject;
+import javax.xml.stream.XMLStreamConstants;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter;
+import com.microsoft.windowsazure.services.core.utils.DateFactory;
+import com.microsoft.windowsazure.services.table.EdmValueConverter;
+import com.microsoft.windowsazure.services.table.models.Entity;
+import com.microsoft.windowsazure.services.table.models.Property;
+import com.microsoft.windowsazure.services.table.models.TableEntry;
+
+public class AtomReaderWriter {
+ private final XMLStreamFactory xmlStreamFactory;
+ private final DateFactory dateFactory;
+ private final ISO8601DateConverter iso8601DateConverter;
+ private final EdmValueConverter edmValueConverter;
+
+ @Inject
+ public AtomReaderWriter(XMLStreamFactory xmlStreamFactory, DateFactory dateFactory,
+ ISO8601DateConverter iso8601DateConverter, EdmValueConverter edmValueConverter) {
+ this.xmlStreamFactory = xmlStreamFactory;
+ this.dateFactory = dateFactory;
+ this.iso8601DateConverter = iso8601DateConverter;
+ this.edmValueConverter = edmValueConverter;
+ }
+
+ public InputStream generateTableEntry(String table) {
+ final String tableTemp = table;
+ return generateEntry(new PropertiesWriter() {
+ @Override
+ public void write(XMLStreamWriter writer) throws XMLStreamException {
+ writer.writeStartElement("d:TableName");
+ writer.writeCharacters(tableTemp);
+ writer.writeEndElement(); // d:TableName
+ }
+ });
+ }
+
+ public InputStream generateEntityEntry(Entity entity) {
+ final Entity entityTemp = entity;
+ return generateEntry(new PropertiesWriter() {
+ @Override
+ public void write(XMLStreamWriter writer) throws XMLStreamException {
+ for (Entry entry : entityTemp.getProperties().entrySet()) {
+ writer.writeStartElement("d:" + entry.getKey());
+
+ String edmType = entry.getValue().getEdmType();
+ if (edmType != null) {
+ writer.writeAttribute("m:type", edmType);
+ }
+
+ String value = edmValueConverter.serialize(edmType, entry.getValue().getValue());
+ if (value != null) {
+ writer.writeCharacters(value);
+ }
+
+ writer.writeEndElement(); // property name
+
+ }
+ }
+ });
+ }
+
+ public List parseTableEntries(InputStream stream) {
+ try {
+ XMLStreamReader xmlr = xmlStreamFactory.getReader(stream);
+
+ expect(xmlr, XMLStreamConstants.START_DOCUMENT);
+ expect(xmlr, XMLStreamConstants.START_ELEMENT, "feed");
+
+ List result = new ArrayList();
+ while (!isEndElement(xmlr, "feed")) {
+ // Process "entry" elements only
+ if (isStartElement(xmlr, "entry")) {
+ result.add(parseTableEntry(xmlr));
+ }
+ else {
+ nextSignificant(xmlr);
+ }
+ }
+
+ expect(xmlr, XMLStreamConstants.END_ELEMENT, "feed");
+ expect(xmlr, XMLStreamConstants.END_DOCUMENT);
+
+ return result;
+ }
+ catch (XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public TableEntry parseTableEntry(InputStream stream) {
+ try {
+ XMLStreamReader xmlr = xmlStreamFactory.getReader(stream);
+
+ expect(xmlr, XMLStreamConstants.START_DOCUMENT);
+ TableEntry result = parseTableEntry(xmlr);
+ expect(xmlr, XMLStreamConstants.END_DOCUMENT);
+
+ return result;
+ }
+ catch (XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public List parseEntityEntries(InputStream stream) {
+ try {
+ XMLStreamReader xmlr = xmlStreamFactory.getReader(stream);
+
+ expect(xmlr, XMLStreamConstants.START_DOCUMENT);
+ expect(xmlr, XMLStreamConstants.START_ELEMENT, "feed");
+
+ List result = new ArrayList();
+ while (!isEndElement(xmlr, "feed")) {
+ // Process "entry" elements only
+ if (isStartElement(xmlr, "entry")) {
+ result.add(parseEntityEntry(xmlr));
+ }
+ else {
+ nextSignificant(xmlr);
+ }
+ }
+
+ expect(xmlr, XMLStreamConstants.END_ELEMENT, "feed");
+ expect(xmlr, XMLStreamConstants.END_DOCUMENT);
+
+ return result;
+ }
+ catch (XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public Entity parseEntityEntry(InputStream stream) {
+ try {
+ XMLStreamReader xmlr = xmlStreamFactory.getReader(stream);
+
+ expect(xmlr, XMLStreamConstants.START_DOCUMENT);
+ Entity result = parseEntityEntry(xmlr);
+ expect(xmlr, XMLStreamConstants.END_DOCUMENT);
+
+ return result;
+ }
+ catch (XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private interface PropertiesWriter {
+ void write(XMLStreamWriter writer) throws XMLStreamException;
+ }
+
+ private InputStream generateEntry(PropertiesWriter propertiesWriter) {
+ try {
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ XMLStreamWriter writer = xmlStreamFactory.getWriter(stream);
+ writer.writeStartDocument("utf-8", "1.0");
+
+ writer.writeStartElement("entry");
+ writer.writeAttribute("xmlns:d", "http://schemas.microsoft.com/ado/2007/08/dataservices");
+ writer.writeAttribute("xmlns:m", "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata");
+ writer.writeAttribute("xmlns", "http://www.w3.org/2005/Atom");
+
+ writer.writeStartElement("title");
+ writer.writeEndElement(); // title
+
+ writer.writeStartElement("updated");
+ writer.writeCharacters(iso8601DateConverter.format(dateFactory.getDate()));
+ writer.writeEndElement(); // updated
+
+ writer.writeStartElement("author");
+ writer.writeStartElement("name");
+ writer.writeEndElement(); // name
+ writer.writeEndElement(); // author
+
+ writer.writeStartElement("id");
+ writer.writeEndElement(); // id
+
+ writer.writeStartElement("content");
+ writer.writeAttribute("type", "application/xml");
+
+ writer.writeStartElement("m:properties");
+ propertiesWriter.write(writer);
+ writer.writeEndElement(); // m:properties
+
+ writer.writeEndElement(); // content
+
+ writer.writeEndElement(); // entry
+
+ writer.writeEndDocument();
+ writer.close();
+
+ return new ByteArrayInputStream(stream.toByteArray());
+ }
+ catch (XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private TableEntry parseTableEntry(XMLStreamReader xmlr) throws XMLStreamException {
+ TableEntry result = new TableEntry();
+
+ expect(xmlr, XMLStreamConstants.START_ELEMENT, "entry");
+
+ while (!isEndElement(xmlr, "entry")) {
+ if (isStartElement(xmlr, "properties")) {
+ Map properties = parseEntryProperties(xmlr);
+
+ result.setName((String) properties.get("TableName").getValue());
+ }
+ else {
+ nextSignificant(xmlr);
+ }
+ }
+
+ expect(xmlr, XMLStreamConstants.END_ELEMENT, "entry");
+
+ return result;
+ }
+
+ private Entity parseEntityEntry(XMLStreamReader xmlr) throws XMLStreamException {
+ Entity result = new Entity();
+
+ result.setEtag(xmlr.getAttributeValue(null, "etag"));
+ expect(xmlr, XMLStreamConstants.START_ELEMENT, "entry");
+
+ while (!isEndElement(xmlr, "entry")) {
+ if (isStartElement(xmlr, "properties")) {
+ result.setProperties(parseEntryProperties(xmlr));
+ }
+ else {
+ nextSignificant(xmlr);
+ }
+ }
+
+ expect(xmlr, XMLStreamConstants.END_ELEMENT, "entry");
+
+ return result;
+ }
+
+ private Map parseEntryProperties(XMLStreamReader xmlr) throws XMLStreamException {
+ Map result = new HashMap();
+
+ expect(xmlr, XMLStreamConstants.START_ELEMENT, "properties");
+
+ while (!isEndElement(xmlr, "properties")) {
+ String name = xmlr.getLocalName();
+ String edmType = xmlr.getAttributeValue(null, "type");
+
+ xmlr.next();
+ String serializedValue = xmlr.getText();
+ Object value = edmValueConverter.deserialize(edmType, serializedValue);
+
+ result.put(name, new Property().setEdmType(edmType).setValue(value));
+
+ nextSignificant(xmlr);
+ expect(xmlr, XMLStreamConstants.END_ELEMENT, name);
+ }
+
+ expect(xmlr, XMLStreamConstants.END_ELEMENT, "properties");
+
+ return result;
+ }
+
+ private void nextSignificant(XMLStreamReader xmlr) throws XMLStreamException {
+ if (!xmlr.hasNext())
+ return;
+ xmlr.next();
+
+ while (xmlr.isCharacters()) {
+ if (!xmlr.hasNext())
+ return;
+ xmlr.next();
+ }
+ }
+
+ private boolean isStartElement(XMLStreamReader xmlr, String localName) {
+ return xmlr.isStartElement() && localName.equals(xmlr.getLocalName());
+ }
+
+ private boolean isEndElement(XMLStreamReader xmlr, String localName) {
+ return xmlr.isEndElement() && localName.equals(xmlr.getLocalName());
+ }
+
+ private void expect(XMLStreamReader xmlr, int eventType) throws XMLStreamException {
+ expect(xmlr, eventType, null);
+ }
+
+ private void expect(XMLStreamReader xmlr, int eventType, String localName) throws XMLStreamException {
+ xmlr.require(eventType, null, localName);
+ nextSignificant(xmlr);
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultEdmValueConterter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultEdmValueConterter.java
new file mode 100644
index 000000000000..b062b3394ab9
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultEdmValueConterter.java
@@ -0,0 +1,65 @@
+package com.microsoft.windowsazure.services.table.implementation;
+
+import java.text.ParseException;
+import java.util.Date;
+
+import javax.inject.Inject;
+
+import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter;
+import com.microsoft.windowsazure.services.table.EdmValueConverter;
+import com.microsoft.windowsazure.services.table.models.EdmType;
+
+public class DefaultEdmValueConterter implements EdmValueConverter {
+
+ private final ISO8601DateConverter iso8601DateConverter;
+
+ @Inject
+ public DefaultEdmValueConterter(ISO8601DateConverter iso8601DateConverter) {
+ this.iso8601DateConverter = iso8601DateConverter;
+ }
+
+ @Override
+ public String serialize(String edmType, Object value) {
+ if (value == null)
+ return null;
+
+ String serializedValue;
+ if (value instanceof Date) {
+ serializedValue = iso8601DateConverter.format((Date) value);
+ }
+ else {
+ serializedValue = value.toString();
+ }
+
+ return serializedValue;
+ }
+
+ @Override
+ public Object deserialize(String edmType, String value) {
+ if (edmType == null)
+ return value;
+
+ if (EdmType.DATETIME.equals(edmType)) {
+ try {
+ return iso8601DateConverter.parse(value);
+ }
+ catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ else if (EdmType.BOOLEAN.equals(edmType)) {
+ return Boolean.parseBoolean(value);
+ }
+ else if (EdmType.DOUBLE.equals(edmType)) {
+ return Double.parseDouble(value);
+ }
+ else if (EdmType.INT32.equals(edmType)) {
+ return Integer.parseInt(value);
+ }
+ else if (EdmType.INT64.equals(edmType)) {
+ return Long.parseLong(value);
+ }
+
+ return value;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultXMLStreamFactory.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultXMLStreamFactory.java
new file mode 100644
index 000000000000..8504c88fa349
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultXMLStreamFactory.java
@@ -0,0 +1,40 @@
+package com.microsoft.windowsazure.services.table.implementation;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+public class DefaultXMLStreamFactory implements XMLStreamFactory {
+ private final XMLOutputFactory xmlOutputFactory;
+ private final XMLInputFactory xmlInputFactory;
+
+ public DefaultXMLStreamFactory() {
+ this.xmlOutputFactory = XMLOutputFactory.newInstance();
+ this.xmlInputFactory = XMLInputFactory.newInstance();
+ }
+
+ @Override
+ public XMLStreamWriter getWriter(OutputStream stream) {
+ try {
+ return xmlOutputFactory.createXMLStreamWriter(stream, "UTF-8");
+ }
+ catch (XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public XMLStreamReader getReader(InputStream stream) {
+ try {
+ return xmlInputFactory.createXMLStreamReader(stream);
+ }
+ catch (XMLStreamException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/HttpReaderWriter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/HttpReaderWriter.java
new file mode 100644
index 000000000000..458acf6e0cd9
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/HttpReaderWriter.java
@@ -0,0 +1,168 @@
+package com.microsoft.windowsazure.services.table.implementation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Reader;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.util.Enumeration;
+
+import javax.activation.DataSource;
+import javax.inject.Inject;
+import javax.mail.Header;
+import javax.mail.MessagingException;
+import javax.mail.internet.InternetHeaders;
+
+import com.sun.mail.util.LineInputStream;
+
+public class HttpReaderWriter {
+
+ @Inject
+ public HttpReaderWriter() {
+ }
+
+ public StatusLine parseStatusLine(DataSource ds) {
+ try {
+ LineInputStream stream = new LineInputStream(ds.getInputStream());
+ String line = stream.readLine();
+ StringReader lineReader = new StringReader(line);
+
+ expect(lineReader, "HTTP/1.1");
+ expect(lineReader, " ");
+ String statusString = extractInput(lineReader, ' ');
+ String reason = extractInput(lineReader, -1);
+
+ return new StatusLine().setStatus(Integer.parseInt(statusString)).setReason(reason);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public InternetHeaders parseHeaders(DataSource ds) {
+ try {
+ return new InternetHeaders(ds.getInputStream());
+ }
+ catch (MessagingException e) {
+ throw new RuntimeException(e);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public InputStream parseEntity(DataSource ds) {
+ try {
+ return ds.getInputStream();
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void appendMethod(OutputStream stream, String verb, URI uri) {
+ try {
+ String method = String.format("%s %s %s\r\n", verb, uri, "HTTP/1.1");
+ stream.write(method.getBytes("UTF-8"));
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void appendHeaders(OutputStream stream, InternetHeaders headers) {
+ try {
+ // Headers
+ Enumeration e = headers.getAllHeaders();
+ while (e.hasMoreElements()) {
+ Header header = e.nextElement();
+
+ String headerLine = String.format("%s: %s\r\n", header.getName(), header.getValue());
+ stream.write(headerLine.getBytes("UTF-8"));
+ }
+
+ // Empty line
+ stream.write("\r\n".getBytes("UTF-8"));
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(e);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void appendEntity(OutputStream stream, InputStream entity) {
+ try {
+ byte[] buffer = new byte[1024];
+ while (true) {
+ int n = entity.read(buffer);
+ if (n == -1)
+ break;
+ stream.write(buffer, 0, n);
+ }
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void expect(Reader reader, String string) {
+ try {
+ for (int i = 0; i < string.length(); i++) {
+ int ch = reader.read();
+ if (ch < 0)
+ throw new RuntimeException(String.format("Expected '%s', found '%s' instead", string,
+ string.substring(0, i) + ch));
+ }
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private String extractInput(Reader reader, int delimiter) {
+ try {
+ StringBuilder sb = new StringBuilder();
+ while (true) {
+ int ch = reader.read();
+ if (ch == -1 || ch == delimiter)
+ break;
+
+ sb.append((char) ch);
+ }
+ return sb.toString();
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public class StatusLine {
+ private int status;
+ private String reason;
+
+ public int getStatus() {
+ return status;
+ }
+
+ public StatusLine setStatus(int status) {
+ this.status = status;
+ return this;
+ }
+
+ public String getReason() {
+ return reason;
+ }
+
+ public StatusLine setReason(String reason) {
+ this.reason = reason;
+ return this;
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/InputStreamDataSource.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/InputStreamDataSource.java
new file mode 100644
index 000000000000..22c099a52fee
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/InputStreamDataSource.java
@@ -0,0 +1,38 @@
+package com.microsoft.windowsazure.services.table.implementation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.activation.DataSource;
+
+public class InputStreamDataSource implements DataSource {
+ private final InputStream stream;
+ private final String contentType;
+
+ public InputStreamDataSource(InputStream stream, String contentType) {
+ this.stream = stream;
+ this.contentType = contentType;
+
+ }
+
+ @Override
+ public String getContentType() {
+ return contentType;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return stream;
+ }
+
+ @Override
+ public String getName() {
+ return null;
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/MimeReaderWriter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/MimeReaderWriter.java
new file mode 100644
index 000000000000..9d2282ab08e6
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/MimeReaderWriter.java
@@ -0,0 +1,147 @@
+package com.microsoft.windowsazure.services.table.implementation;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.activation.DataHandler;
+import javax.activation.DataSource;
+import javax.inject.Inject;
+import javax.mail.BodyPart;
+import javax.mail.MessagingException;
+import javax.mail.MultipartDataSource;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.internet.MimePartDataSource;
+
+public class MimeReaderWriter {
+
+ @Inject
+ public MimeReaderWriter() {
+ }
+
+ public MimeMultipart getMimeMultipart(List bodyPartContents) {
+ try {
+ return getMimeMultipartCore(bodyPartContents);
+ }
+ catch (MessagingException e) {
+ throw new RuntimeException(e);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private MimeMultipart getMimeMultipartCore(List bodyPartContents) throws MessagingException,
+ IOException {
+ // Create unique part boundary strings
+ String batchId = String.format("batch_%s", UUID.randomUUID().toString());
+ String changeSet = String.format("changeset_%s", UUID.randomUUID().toString());
+
+ //
+ // Build inner list of change sets containing the list of body part content
+ //
+ MimeMultipart changeSets = new MimeMultipart(new SetBoundaryMultipartDataSource(changeSet));
+
+ for (DataSource bodyPart : bodyPartContents) {
+ MimeBodyPart mimeBodyPart = new MimeBodyPart();
+
+ mimeBodyPart.setDataHandler(new DataHandler(bodyPart));
+ mimeBodyPart.setHeader("Content-Type", bodyPart.getContentType());
+ mimeBodyPart.setHeader("Content-Transfer-Encoding", "binary");
+
+ changeSets.addBodyPart(mimeBodyPart);
+ }
+
+ //
+ // Build outer "batch" body part
+ //
+ MimeBodyPart batchbody = new MimeBodyPart();
+ batchbody.setContent(changeSets);
+ //Note: Both content type and encoding need to be set *after* setting content, because
+ // MimeBodyPart implementation replaces them when calling "setContent".
+ batchbody.setHeader("Content-Type", changeSets.getContentType());
+
+ //
+ // Build outer "batch" multipart
+ //
+ MimeMultipart batch = new MimeMultipart(new SetBoundaryMultipartDataSource(batchId));
+ batch.addBodyPart(batchbody);
+ return batch;
+ }
+
+ /**
+ * The only purpose of this class is to force the boundary of a MimeMultipart instance.
+ * This is done by simple passing an instance of this class to the constructor of MimeMultipart.
+ */
+ private class SetBoundaryMultipartDataSource implements MultipartDataSource {
+
+ private final String boundary;
+
+ public SetBoundaryMultipartDataSource(String boundary) {
+ this.boundary = boundary;
+ }
+
+ @Override
+ public String getContentType() {
+ return "multipart/mixed; boundary=" + boundary;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return null;
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return 0;
+ }
+
+ @Override
+ public BodyPart getBodyPart(int index) throws MessagingException {
+ return null;
+ }
+ }
+
+ public List parseParts(final InputStream entityInputStream, final String contentType) {
+ try {
+ return parsePartsCore(entityInputStream, contentType);
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ catch (MessagingException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private List parsePartsCore(InputStream entityInputStream, String contentType)
+ throws MessagingException, IOException {
+ DataSource ds = new InputStreamDataSource(entityInputStream, contentType);
+ MimeMultipart batch = new MimeMultipart(ds);
+ MimeBodyPart batchBody = (MimeBodyPart) batch.getBodyPart(0);
+
+ MimeMultipart changeSets = new MimeMultipart(new MimePartDataSource(batchBody));
+
+ List result = new ArrayList();
+ for (int i = 0; i < changeSets.getCount(); i++) {
+ BodyPart part = changeSets.getBodyPart(i);
+
+ result.add(new InputStreamDataSource(part.getInputStream(), part.getContentType()));
+ }
+ return result;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyFilter.java
new file mode 100644
index 000000000000..79305ce758c8
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyFilter.java
@@ -0,0 +1,90 @@
+/**
+ * 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.implementation;
+
+import java.util.List;
+
+import javax.inject.Named;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.microsoft.windowsazure.services.blob.implementation.SharedKeyUtils;
+import com.microsoft.windowsazure.services.blob.implementation.SharedKeyUtils.QueryParam;
+import com.microsoft.windowsazure.services.table.TableConfiguration;
+import com.sun.jersey.api.client.ClientRequest;
+
+public class SharedKeyFilter extends com.microsoft.windowsazure.services.blob.implementation.SharedKeyFilter {
+ private static Log log = LogFactory.getLog(SharedKeyFilter.class);
+
+ public SharedKeyFilter(@Named(TableConfiguration.ACCOUNT_NAME) String accountName,
+ @Named(TableConfiguration.ACCOUNT_KEY) String accountKey) {
+ super(accountName, accountKey);
+ }
+
+ /*
+ * StringToSign = VERB + "\n" +
+ * Content-MD5 + "\n" +
+ * Content-Type + "\n" +
+ * Date + "\n" +
+ * CanonicalizedResource;
+ */
+ @Override
+ public void sign(ClientRequest cr) {
+ // gather signed material
+ addOptionalDateHeader(cr);
+
+ // build signed string
+ String stringToSign = cr.getMethod() + "\n" + getHeader(cr, "Content-MD5") + "\n"
+ + getHeader(cr, "Content-Type") + "\n" + getHeader(cr, "Date") + "\n";
+
+ stringToSign += getCanonicalizedResource(cr);
+
+ if (log.isDebugEnabled()) {
+ log.debug(String.format("String to sign: \"%s\"", stringToSign));
+ }
+ //TODO: Remove or comment the following line
+ //System.out.println(String.format("String to sign: \"%s\"", stringToSign));
+
+ String signature = this.getSigner().sign(stringToSign);
+ cr.getHeaders().putSingle("Authorization", "SharedKey " + this.getAccountName() + ":" + signature);
+ }
+
+ /**
+ * This format supports Shared Key and Shared Key Lite for all versions of the Table service, and Shared Key Lite
+ * for the 2009-09-19 version of the Blob and Queue services. This format is identical to that used with previous
+ * versions of the storage services. Construct the CanonicalizedResource string in this format as follows:
+ *
+ * 1. Beginning with an empty string (""), append a forward slash (/), followed by the name of the account that owns
+ * the resource being accessed.
+ *
+ * 2. Append the resource's encoded URI path. If the request URI addresses a component of the resource, append the
+ * appropriate query string. The query string should include the question mark and the comp parameter (for example,
+ * ?comp=metadata). No other parameters should be included on the query string.
+ */
+ private String getCanonicalizedResource(ClientRequest cr) {
+ String result = "/" + this.getAccountName();
+
+ result += cr.getURI().getPath();
+
+ List queryParams = SharedKeyUtils.getQueryParams(cr.getURI().getQuery());
+ for (QueryParam p : queryParams) {
+ if ("comp".equals(p.getName())) {
+ result += "?" + p.getName() + "=" + p.getValues().get(0);
+ }
+ }
+ return result;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyLiteFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyLiteFilter.java
new file mode 100644
index 000000000000..094fe429145a
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyLiteFilter.java
@@ -0,0 +1,26 @@
+/**
+ * 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.implementation;
+
+import javax.inject.Named;
+
+import com.microsoft.windowsazure.services.table.TableConfiguration;
+
+public class SharedKeyLiteFilter extends com.microsoft.windowsazure.services.blob.implementation.SharedKeyLiteFilter {
+ public SharedKeyLiteFilter(@Named(TableConfiguration.ACCOUNT_NAME) String accountName,
+ @Named(TableConfiguration.ACCOUNT_KEY) String accountKey) {
+ super(accountName, accountKey);
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableExceptionProcessor.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableExceptionProcessor.java
new file mode 100644
index 000000000000..c8bf695b7cdd
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableExceptionProcessor.java
@@ -0,0 +1,491 @@
+/**
+ * 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.implementation;
+
+import javax.inject.Inject;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import com.microsoft.windowsazure.services.core.ServiceException;
+import com.microsoft.windowsazure.services.core.ServiceFilter;
+import com.microsoft.windowsazure.services.core.utils.ServiceExceptionFactory;
+import com.microsoft.windowsazure.services.table.TableContract;
+import com.microsoft.windowsazure.services.table.models.BatchOperations;
+import com.microsoft.windowsazure.services.table.models.BatchResult;
+import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions;
+import com.microsoft.windowsazure.services.table.models.Entity;
+import com.microsoft.windowsazure.services.table.models.GetEntityResult;
+import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult;
+import com.microsoft.windowsazure.services.table.models.GetTableResult;
+import com.microsoft.windowsazure.services.table.models.InsertEntityResult;
+import com.microsoft.windowsazure.services.table.models.ListTablesOptions;
+import com.microsoft.windowsazure.services.table.models.QueryEntitiesOptions;
+import com.microsoft.windowsazure.services.table.models.QueryEntitiesResult;
+import com.microsoft.windowsazure.services.table.models.QueryTablesOptions;
+import com.microsoft.windowsazure.services.table.models.QueryTablesResult;
+import com.microsoft.windowsazure.services.table.models.ServiceProperties;
+import com.microsoft.windowsazure.services.table.models.TableServiceOptions;
+import com.microsoft.windowsazure.services.table.models.UpdateEntityResult;
+import com.sun.jersey.api.client.ClientHandlerException;
+import com.sun.jersey.api.client.UniformInterfaceException;
+
+public class TableExceptionProcessor implements TableContract {
+ private static Log log = LogFactory.getLog(TableExceptionProcessor.class);
+ private final TableContract service;
+
+ @Inject
+ public TableExceptionProcessor(TableRestProxy service) {
+ this.service = service;
+ }
+
+ public TableExceptionProcessor(TableContract service) {
+ this.service = service;
+ }
+
+ @Override
+ public TableContract withFilter(ServiceFilter filter) {
+ return new TableExceptionProcessor(service.withFilter(filter));
+ }
+
+ private ServiceException processCatch(ServiceException e) {
+ log.warn(e.getMessage(), e.getCause());
+ return ServiceExceptionFactory.process("table", e);
+ }
+
+ @Override
+ public GetServicePropertiesResult getServiceProperties() throws ServiceException {
+ try {
+ return service.getServiceProperties();
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public GetServicePropertiesResult getServiceProperties(TableServiceOptions options) throws ServiceException {
+ try {
+ return service.getServiceProperties(options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void setServiceProperties(ServiceProperties serviceProperties) throws ServiceException {
+ try {
+ service.setServiceProperties(serviceProperties);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void setServiceProperties(ServiceProperties serviceProperties, TableServiceOptions options)
+ throws ServiceException {
+ try {
+ service.setServiceProperties(serviceProperties, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void createTable(String table) throws ServiceException {
+ try {
+ service.createTable(table);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void createTable(String table, TableServiceOptions options) throws ServiceException {
+ try {
+ service.createTable(table, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public GetTableResult getTable(String table) throws ServiceException {
+ try {
+ return service.getTable(table);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public GetTableResult getTable(String table, TableServiceOptions options) throws ServiceException {
+ try {
+ return service.getTable(table, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void deleteTable(String table) throws ServiceException {
+ try {
+ service.deleteTable(table);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void deleteTable(String table, TableServiceOptions options) throws ServiceException {
+ try {
+ service.deleteTable(table, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public QueryTablesResult queryTables() throws ServiceException {
+ try {
+ return service.queryTables();
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException {
+ try {
+ return service.queryTables(options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public QueryTablesResult listTables() throws ServiceException {
+ try {
+ return service.listTables();
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public QueryTablesResult listTables(ListTablesOptions options) throws ServiceException {
+ try {
+ return service.listTables(options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public InsertEntityResult insertEntity(String table, Entity entity) throws ServiceException {
+ try {
+ return service.insertEntity(table, entity);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public InsertEntityResult insertEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ try {
+ return service.insertEntity(table, entity, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult updateEntity(String table, Entity entity) throws ServiceException {
+ try {
+ return service.updateEntity(table, entity);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ try {
+ return service.updateEntity(table, entity, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult mergeEntity(String table, Entity entity) throws ServiceException {
+ try {
+ return service.mergeEntity(table, entity);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult mergeEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ try {
+ return service.mergeEntity(table, entity, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult insertOrReplaceEntity(String table, Entity entity) throws ServiceException {
+ try {
+ return service.insertOrReplaceEntity(table, entity);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult insertOrReplaceEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ try {
+ return service.insertOrReplaceEntity(table, entity, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult insertOrMergeEntity(String table, Entity entity) throws ServiceException {
+ try {
+ return service.insertOrMergeEntity(table, entity);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public UpdateEntityResult insertOrMergeEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ try {
+ return service.insertOrMergeEntity(table, entity, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException {
+ try {
+ service.deleteEntity(table, partitionKey, rowKey);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options)
+ throws ServiceException {
+ try {
+ service.deleteEntity(table, partitionKey, rowKey, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public GetEntityResult getEntity(String table, String partitionKey, String rowKey) throws ServiceException {
+ try {
+ return service.getEntity(table, partitionKey, rowKey);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public GetEntityResult getEntity(String table, String partitionKey, String rowKey, TableServiceOptions options)
+ throws ServiceException {
+ try {
+ return service.getEntity(table, partitionKey, rowKey, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public QueryEntitiesResult queryEntities(String table) throws ServiceException {
+ try {
+ return service.queryEntities(table);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions options) throws ServiceException {
+ try {
+ return service.queryEntities(table, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public BatchResult batch(BatchOperations operations) throws ServiceException {
+ try {
+ return service.batch(operations);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+
+ @Override
+ public BatchResult batch(BatchOperations operations, TableServiceOptions options) throws ServiceException {
+ try {
+ return service.batch(operations, options);
+ }
+ catch (UniformInterfaceException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ catch (ClientHandlerException e) {
+ throw processCatch(new ServiceException(e));
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableRestProxy.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableRestProxy.java
new file mode 100644
index 000000000000..096c5b4ec312
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableRestProxy.java
@@ -0,0 +1,767 @@
+/**
+ * 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.implementation;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+
+import javax.activation.DataSource;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.mail.Header;
+import javax.mail.internet.InternetHeaders;
+import javax.mail.internet.MimeMultipart;
+
+import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter;
+import com.microsoft.windowsazure.services.blob.implementation.RFC1123DateConverter;
+import com.microsoft.windowsazure.services.core.ServiceException;
+import com.microsoft.windowsazure.services.core.ServiceFilter;
+import com.microsoft.windowsazure.services.core.utils.CommaStringBuilder;
+import com.microsoft.windowsazure.services.core.utils.DateFactory;
+import com.microsoft.windowsazure.services.core.utils.ServiceExceptionFactory;
+import com.microsoft.windowsazure.services.core.utils.pipeline.ClientFilterAdapter;
+import com.microsoft.windowsazure.services.core.utils.pipeline.HttpURLConnectionClient;
+import com.microsoft.windowsazure.services.core.utils.pipeline.PipelineHelpers;
+import com.microsoft.windowsazure.services.table.TableConfiguration;
+import com.microsoft.windowsazure.services.table.TableContract;
+import com.microsoft.windowsazure.services.table.implementation.HttpReaderWriter.StatusLine;
+import com.microsoft.windowsazure.services.table.models.BatchOperations;
+import com.microsoft.windowsazure.services.table.models.BatchOperations.DeleteEntityOperation;
+import com.microsoft.windowsazure.services.table.models.BatchOperations.InsertEntityOperation;
+import com.microsoft.windowsazure.services.table.models.BatchOperations.InsertOrMergeEntityOperation;
+import com.microsoft.windowsazure.services.table.models.BatchOperations.InsertOrReplaceEntityOperation;
+import com.microsoft.windowsazure.services.table.models.BatchOperations.MergeEntityOperation;
+import com.microsoft.windowsazure.services.table.models.BatchOperations.Operation;
+import com.microsoft.windowsazure.services.table.models.BatchOperations.UpdateEntityOperation;
+import com.microsoft.windowsazure.services.table.models.BatchResult;
+import com.microsoft.windowsazure.services.table.models.BatchResult.DeleteEntity;
+import com.microsoft.windowsazure.services.table.models.BatchResult.Entry;
+import com.microsoft.windowsazure.services.table.models.BatchResult.Error;
+import com.microsoft.windowsazure.services.table.models.BatchResult.InsertEntity;
+import com.microsoft.windowsazure.services.table.models.BatchResult.UpdateEntity;
+import com.microsoft.windowsazure.services.table.models.BinaryFilter;
+import com.microsoft.windowsazure.services.table.models.ConstantFilter;
+import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions;
+import com.microsoft.windowsazure.services.table.models.Entity;
+import com.microsoft.windowsazure.services.table.models.Filter;
+import com.microsoft.windowsazure.services.table.models.GetEntityResult;
+import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult;
+import com.microsoft.windowsazure.services.table.models.GetTableResult;
+import com.microsoft.windowsazure.services.table.models.InsertEntityResult;
+import com.microsoft.windowsazure.services.table.models.ListTablesOptions;
+import com.microsoft.windowsazure.services.table.models.LitteralFilter;
+import com.microsoft.windowsazure.services.table.models.Query;
+import com.microsoft.windowsazure.services.table.models.QueryEntitiesOptions;
+import com.microsoft.windowsazure.services.table.models.QueryEntitiesResult;
+import com.microsoft.windowsazure.services.table.models.QueryTablesOptions;
+import com.microsoft.windowsazure.services.table.models.QueryTablesResult;
+import com.microsoft.windowsazure.services.table.models.RawStringFilter;
+import com.microsoft.windowsazure.services.table.models.ServiceProperties;
+import com.microsoft.windowsazure.services.table.models.TableServiceOptions;
+import com.microsoft.windowsazure.services.table.models.UnaryFilter;
+import com.microsoft.windowsazure.services.table.models.UpdateEntityResult;
+import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.client.UniformInterfaceException;
+import com.sun.jersey.api.client.WebResource;
+import com.sun.jersey.api.client.WebResource.Builder;
+import com.sun.jersey.core.header.InBoundHeaders;
+
+public class TableRestProxy implements TableContract {
+ private static final String API_VERSION = "2011-08-18";
+ private final HttpURLConnectionClient channel;
+ private final String url;
+ private final RFC1123DateConverter dateMapper;
+ private final ISO8601DateConverter iso8601DateConverter;
+ private final DateFactory dateFactory;
+ private final ServiceFilter[] filters;
+ private final SharedKeyFilter filter;
+ private final AtomReaderWriter atomReaderWriter;
+ private final MimeReaderWriter mimeReaderWriter;
+ private final HttpReaderWriter httpReaderWriter;
+
+ @Inject
+ public TableRestProxy(HttpURLConnectionClient channel, @Named(TableConfiguration.URI) String url,
+ SharedKeyFilter filter, DateFactory dateFactory, ISO8601DateConverter iso8601DateConverter,
+ AtomReaderWriter atomReaderWriter, MimeReaderWriter mimeReaderWriter, HttpReaderWriter httpReaderWriter) {
+
+ this.channel = channel;
+ this.url = url;
+ this.filter = filter;
+ this.dateMapper = new RFC1123DateConverter();
+ this.iso8601DateConverter = iso8601DateConverter;
+ this.filters = new ServiceFilter[0];
+ this.dateFactory = dateFactory;
+ this.atomReaderWriter = atomReaderWriter;
+ this.mimeReaderWriter = mimeReaderWriter;
+ this.httpReaderWriter = httpReaderWriter;
+ channel.addFilter(filter);
+ }
+
+ public TableRestProxy(HttpURLConnectionClient channel, ServiceFilter[] filters, String url, SharedKeyFilter filter,
+ DateFactory dateFactory, AtomReaderWriter atomReaderWriter, MimeReaderWriter mimeReaderWriter,
+ HttpReaderWriter httpReaderWriter, RFC1123DateConverter dateMapper,
+ ISO8601DateConverter iso8601DateConverter) {
+
+ this.channel = channel;
+ this.filters = filters;
+ this.url = url;
+ this.filter = filter;
+ this.dateFactory = dateFactory;
+ this.atomReaderWriter = atomReaderWriter;
+ this.mimeReaderWriter = mimeReaderWriter;
+ this.httpReaderWriter = httpReaderWriter;
+ this.dateMapper = dateMapper;
+ this.iso8601DateConverter = iso8601DateConverter;
+ }
+
+ @Override
+ public TableContract withFilter(ServiceFilter filter) {
+ ServiceFilter[] newFilters = Arrays.copyOf(filters, filters.length + 1);
+ newFilters[filters.length] = filter;
+ return new TableRestProxy(this.channel, newFilters, this.url, this.filter, this.dateFactory,
+ this.atomReaderWriter, this.mimeReaderWriter, this.httpReaderWriter, this.dateMapper,
+ this.iso8601DateConverter);
+ }
+
+ private void ThrowIfError(ClientResponse r) {
+ PipelineHelpers.ThrowIfError(r);
+ }
+
+ private String encodeODataURIValue(String value) {
+ //TODO: Unclear if OData value in URI's need to be encoded or not
+ return value;
+ }
+
+ private List encodeODataURIValues(List values) {
+ List list = new ArrayList();
+ for (String value : values) {
+ list.add(encodeODataURIValue(value));
+ }
+ return list;
+ }
+
+ private String getEntityPath(String table, String partitionKey, String rowKey) {
+ return table + "(" + "PartitionKey='" + partitionKey + "',RowKey='" + rowKey + "')";
+ }
+
+ private WebResource addOptionalQueryParam(WebResource webResource, String key, Object value) {
+ return PipelineHelpers.addOptionalQueryParam(webResource, key, value);
+ }
+
+ private WebResource addOptionalQuery(WebResource webResource, Query query) {
+ if (query == null)
+ return webResource;
+
+ if (query.getSelectFields() != null && query.getSelectFields().size() > 0) {
+ webResource = addOptionalQueryParam(webResource, "$select",
+ CommaStringBuilder.join(encodeODataURIValues(query.getSelectFields())));
+ }
+
+ if (query.getTop() != null) {
+ webResource = addOptionalQueryParam(webResource, "$top", encodeODataURIValue(query.getTop().toString()));
+ }
+
+ if (query.getFilter() != null) {
+ webResource = addOptionalQueryParam(webResource, "$filter", buildFilterExpression(query.getFilter()));
+ }
+
+ if (query.getOrderByFields() != null) {
+ webResource = addOptionalQueryParam(webResource, "$orderby",
+ CommaStringBuilder.join(encodeODataURIValues(query.getOrderByFields())));
+ }
+
+ return webResource;
+ }
+
+ private String buildFilterExpression(Filter filter) {
+ StringBuilder sb = new StringBuilder();
+ buildFilterExpression(filter, sb);
+ return sb.toString();
+ }
+
+ private void buildFilterExpression(Filter filter, StringBuilder sb) {
+ if (filter == null)
+ return;
+
+ if (filter instanceof LitteralFilter) {
+ sb.append(((LitteralFilter) filter).getLitteral());
+ }
+ else if (filter instanceof ConstantFilter) {
+ sb.append("'");
+ sb.append(((ConstantFilter) filter).getValue());
+ sb.append("'");
+ }
+ else if (filter instanceof UnaryFilter) {
+ sb.append(((UnaryFilter) filter).getOperator());
+ sb.append("(");
+ buildFilterExpression(((UnaryFilter) filter).getOperand(), sb);
+ sb.append(")");
+ }
+ else if (filter instanceof BinaryFilter) {
+ sb.append("(");
+ buildFilterExpression(((BinaryFilter) filter).getLeft(), sb);
+ sb.append(" ");
+ sb.append(((BinaryFilter) filter).getOperator());
+ sb.append(" ");
+ buildFilterExpression(((BinaryFilter) filter).getRight(), sb);
+ sb.append(")");
+ }
+ else if (filter instanceof RawStringFilter) {
+ sb.append(((RawStringFilter) filter).getRawString());
+ }
+ }
+
+ private Builder addOptionalHeader(Builder builder, String name, Object value) {
+ return PipelineHelpers.addOptionalHeader(builder, name, value);
+ }
+
+ private WebResource.Builder addTableRequestHeaders(WebResource.Builder builder) {
+ builder = addOptionalHeader(builder, "x-ms-version", API_VERSION);
+ builder = addOptionalHeader(builder, "DataServiceVersion", "1.0;NetFx");
+ builder = addOptionalHeader(builder, "MaxDataServiceVersion", "2.0;NetFx");
+ builder = addOptionalHeader(builder, "Accept", "application/atom+xml,application/xml");
+ builder = addOptionalHeader(builder, "Accept-Charset", "UTF-8");
+ return builder;
+ }
+
+ private WebResource.Builder addIfMatchHeader(WebResource.Builder builder, String eTag) {
+ builder = addOptionalHeader(builder, "If-Match", eTag == null ? "*" : eTag);
+ return builder;
+ }
+
+ private WebResource getResource(TableServiceOptions options) {
+ WebResource webResource = channel.resource(url).path("/");
+ webResource = addOptionalQueryParam(webResource, "timeout", options.getTimeout());
+ for (ServiceFilter filter : filters) {
+ webResource.addFilter(new ClientFilterAdapter(filter));
+ }
+
+ return webResource;
+ }
+
+ @Override
+ public GetServicePropertiesResult getServiceProperties() throws ServiceException {
+ return getServiceProperties(new TableServiceOptions());
+ }
+
+ @Override
+ public GetServicePropertiesResult getServiceProperties(TableServiceOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path("/").queryParam("resType", "service")
+ .queryParam("comp", "properties");
+
+ WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION);
+
+ GetServicePropertiesResult result = new GetServicePropertiesResult();
+ result.setValue(builder.get(ServiceProperties.class));
+ return result;
+ }
+
+ @Override
+ public void setServiceProperties(ServiceProperties serviceProperties) throws ServiceException {
+ setServiceProperties(serviceProperties, new TableServiceOptions());
+ }
+
+ @Override
+ public void setServiceProperties(ServiceProperties serviceProperties, TableServiceOptions options)
+ throws ServiceException {
+ WebResource webResource = getResource(options).path("/").queryParam("resType", "service")
+ .queryParam("comp", "properties");
+
+ WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION);
+
+ builder.put(serviceProperties);
+ }
+
+ @Override
+ public GetTableResult getTable(String table) throws ServiceException {
+ return getTable(table, new TableServiceOptions());
+ }
+
+ @Override
+ public GetTableResult getTable(String table, TableServiceOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path("Tables" + "('" + table + "')");
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+
+ ClientResponse response = builder.get(ClientResponse.class);
+ ThrowIfError(response);
+
+ GetTableResult result = new GetTableResult();
+ result.setTableEntry(atomReaderWriter.parseTableEntry(response.getEntityInputStream()));
+ return result;
+ }
+
+ @Override
+ public QueryTablesResult listTables() throws ServiceException {
+ return listTables(new ListTablesOptions());
+ }
+
+ @Override
+ public QueryTablesResult listTables(ListTablesOptions options) throws ServiceException {
+ // Append Max char to end '{' is 1 + 'z' in AsciiTable ==> uppperBound is prefix + '{'
+ Filter filter = Filter.and(Filter.ge(Filter.litteral("TableName"), Filter.constant(options.getPrefix())),
+ Filter.le(Filter.litteral("TableName"), Filter.constant(options.getPrefix() + "{")));
+
+ QueryTablesOptions queryTableOptions = new QueryTablesOptions();
+ queryTableOptions.setTimeout(options.getTimeout());
+ queryTableOptions.setQuery(new Query().setFilter(filter));
+ return queryTables(queryTableOptions);
+ }
+
+ @Override
+ public QueryTablesResult queryTables() throws ServiceException {
+ return queryTables(new QueryTablesOptions());
+ }
+
+ @Override
+ public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path("Tables");
+ webResource = addOptionalQuery(webResource, options.getQuery());
+ webResource = addOptionalQueryParam(webResource, "NextTableName", options.getNextTableName());
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+
+ ClientResponse response = builder.get(ClientResponse.class);
+ ThrowIfError(response);
+
+ QueryTablesResult result = new QueryTablesResult();
+ result.setNextTableName(response.getHeaders().getFirst("x-ms-continuation-NextTableName"));
+ result.setTables(atomReaderWriter.parseTableEntries(response.getEntityInputStream()));
+
+ return result;
+ }
+
+ @Override
+ public void createTable(String table) throws ServiceException {
+ createTable(table, new TableServiceOptions());
+
+ }
+
+ @Override
+ public void createTable(String table, TableServiceOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path("Tables");
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+
+ builder.entity(atomReaderWriter.generateTableEntry(table), "application/atom+xml");
+
+ ClientResponse response = builder.post(ClientResponse.class);
+ ThrowIfError(response);
+ }
+
+ @Override
+ public void deleteTable(String table) throws ServiceException {
+ deleteTable(table, new TableServiceOptions());
+ }
+
+ @Override
+ public void deleteTable(String table, TableServiceOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path("Tables" + "('" + table + "')");
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+ builder = addOptionalHeader(builder, "Content-Type", "application/atom+xml");
+
+ ClientResponse response = builder.delete(ClientResponse.class);
+ ThrowIfError(response);
+ }
+
+ @Override
+ public InsertEntityResult insertEntity(String table, Entity entity) throws ServiceException {
+ return insertEntity(table, entity, new TableServiceOptions());
+ }
+
+ @Override
+ public InsertEntityResult insertEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ WebResource webResource = getResource(options).path(table);
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+
+ builder = builder.entity(atomReaderWriter.generateEntityEntry(entity), "application/atom+xml");
+
+ ClientResponse response = builder.post(ClientResponse.class);
+ ThrowIfError(response);
+
+ InsertEntityResult result = new InsertEntityResult();
+ result.setEntity(atomReaderWriter.parseEntityEntry(response.getEntityInputStream()));
+
+ return result;
+ }
+
+ @Override
+ public UpdateEntityResult updateEntity(String table, Entity entity) throws ServiceException {
+ return updateEntity(table, entity, new TableServiceOptions());
+ }
+
+ @Override
+ public UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ return putOrMergeEntityCore(table, entity, "PUT", true/*includeEtag*/, options);
+ }
+
+ @Override
+ public UpdateEntityResult mergeEntity(String table, Entity entity) throws ServiceException {
+ return updateEntity(table, entity, new TableServiceOptions());
+ }
+
+ @Override
+ public UpdateEntityResult mergeEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ return putOrMergeEntityCore(table, entity, "MERGE", true/*includeEtag*/, options);
+ }
+
+ @Override
+ public UpdateEntityResult insertOrReplaceEntity(String table, Entity entity) throws ServiceException {
+ return insertOrReplaceEntity(table, entity, new TableServiceOptions());
+ }
+
+ @Override
+ public UpdateEntityResult insertOrReplaceEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ return putOrMergeEntityCore(table, entity, "PUT", false/*includeEtag*/, options);
+ }
+
+ @Override
+ public UpdateEntityResult insertOrMergeEntity(String table, Entity entity) throws ServiceException {
+ return insertOrReplaceEntity(table, entity, new TableServiceOptions());
+ }
+
+ @Override
+ public UpdateEntityResult insertOrMergeEntity(String table, Entity entity, TableServiceOptions options)
+ throws ServiceException {
+ return putOrMergeEntityCore(table, entity, "MERGE", false/*includeEtag*/, options);
+ }
+
+ private UpdateEntityResult putOrMergeEntityCore(String table, Entity entity, String verb, boolean includeEtag,
+ TableServiceOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path(
+ getEntityPath(table, entity.getPartitionKey(), entity.getRowKey()));
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+ if (includeEtag) {
+ builder = addIfMatchHeader(builder, entity.getEtag());
+ }
+
+ builder = builder.entity(atomReaderWriter.generateEntityEntry(entity), "application/atom+xml");
+
+ ClientResponse response = builder.method(verb, ClientResponse.class);
+ ThrowIfError(response);
+
+ UpdateEntityResult result = new UpdateEntityResult();
+ result.setEtag(response.getHeaders().getFirst("ETag"));
+
+ return result;
+ }
+
+ @Override
+ public void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException {
+ deleteEntity(table, partitionKey, rowKey, new DeleteEntityOptions());
+ }
+
+ @Override
+ public void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options)
+ throws ServiceException {
+ WebResource webResource = getResource(options).path(getEntityPath(table, partitionKey, rowKey));
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+ builder = addIfMatchHeader(builder, options.getEtag());
+
+ ClientResponse response = builder.delete(ClientResponse.class);
+ ThrowIfError(response);
+ }
+
+ @Override
+ public GetEntityResult getEntity(String table, String partitionKey, String rowKey) throws ServiceException {
+ return getEntity(table, partitionKey, rowKey, new TableServiceOptions());
+ }
+
+ @Override
+ public GetEntityResult getEntity(String table, String partitionKey, String rowKey, TableServiceOptions options)
+ throws ServiceException {
+ WebResource webResource = getResource(options).path(getEntityPath(table, partitionKey, rowKey));
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+
+ ClientResponse response = builder.get(ClientResponse.class);
+ ThrowIfError(response);
+
+ GetEntityResult result = new GetEntityResult();
+ result.setEntity(atomReaderWriter.parseEntityEntry(response.getEntityInputStream()));
+
+ return result;
+ }
+
+ @Override
+ public QueryEntitiesResult queryEntities(String table) throws ServiceException {
+ return queryEntities(table, new QueryEntitiesOptions());
+ }
+
+ @Override
+ public QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path(table);
+ webResource = addOptionalQuery(webResource, options.getQuery());
+ webResource = addOptionalQueryParam(webResource, "NextPartitionKey",
+ encodeODataURIValue(options.getNextPartitionKey()));
+ webResource = addOptionalQueryParam(webResource, "NextRowKey", encodeODataURIValue(options.getNextRowKey()));
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+
+ ClientResponse response = builder.get(ClientResponse.class);
+ ThrowIfError(response);
+
+ QueryEntitiesResult result = new QueryEntitiesResult();
+ result.setNextPartitionKey(response.getHeaders().getFirst("x-ms-continuation-NextPartitionKey"));
+ result.setNextRowKey(response.getHeaders().getFirst("x-ms-continuation-NextRowKey"));
+ result.setEntities(atomReaderWriter.parseEntityEntries(response.getEntityInputStream()));
+
+ return result;
+ }
+
+ @Override
+ public BatchResult batch(BatchOperations operations) throws ServiceException {
+ return batch(operations, new TableServiceOptions());
+ }
+
+ @Override
+ public BatchResult batch(BatchOperations operations, TableServiceOptions options) throws ServiceException {
+ WebResource webResource = getResource(options).path("$batch");
+
+ WebResource.Builder builder = webResource.getRequestBuilder();
+ builder = addTableRequestHeaders(builder);
+
+ MimeMultipart entity = createBatchRequestBody(operations);
+ builder = builder.type(entity.getContentType());
+
+ ClientResponse response = builder.post(ClientResponse.class, entity);
+ ThrowIfError(response);
+
+ BatchResult result = new BatchResult();
+ result.setEntries(parseBatchResponse(response, operations));
+
+ return result;
+ }
+
+ private MimeMultipart createBatchRequestBody(BatchOperations operations) {
+ List bodyPartContents = new ArrayList();
+ int contentId = 1;
+ for (Operation operation : operations.getOperations()) {
+
+ DataSource bodyPartContent = null;
+ if (operation instanceof InsertEntityOperation) {
+ InsertEntityOperation op = (InsertEntityOperation) operation;
+ bodyPartContent = createBatchInsertOrUpdateEntityPart(op.getTable(), op.getEntity(), "POST",
+ false/*includeEtag*/, contentId);
+ contentId++;
+ }
+ else if (operation instanceof UpdateEntityOperation) {
+ UpdateEntityOperation op = (UpdateEntityOperation) operation;
+ bodyPartContent = createBatchInsertOrUpdateEntityPart(op.getTable(), op.getEntity(), "PUT",
+ true/*includeEtag*/, contentId);
+ contentId++;
+ }
+ else if (operation instanceof MergeEntityOperation) {
+ MergeEntityOperation op = (MergeEntityOperation) operation;
+ bodyPartContent = createBatchInsertOrUpdateEntityPart(op.getTable(), op.getEntity(), "MERGE",
+ true/*includeEtag*/, contentId);
+ contentId++;
+ }
+ else if (operation instanceof InsertOrReplaceEntityOperation) {
+ InsertOrReplaceEntityOperation op = (InsertOrReplaceEntityOperation) operation;
+ bodyPartContent = createBatchInsertOrUpdateEntityPart(op.getTable(), op.getEntity(), "PUT",
+ false/*includeEtag*/, contentId);
+ contentId++;
+ }
+ else if (operation instanceof InsertOrMergeEntityOperation) {
+ InsertOrMergeEntityOperation op = (InsertOrMergeEntityOperation) operation;
+ bodyPartContent = createBatchInsertOrUpdateEntityPart(op.getTable(), op.getEntity(), "MERGE",
+ false/*includeEtag*/, contentId);
+ contentId++;
+ }
+ else if (operation instanceof DeleteEntityOperation) {
+ DeleteEntityOperation op = (DeleteEntityOperation) operation;
+ bodyPartContent = createBatchDeleteEntityPart(op.getTable(), op.getPartitionKey(), op.getRowKey(),
+ op.getEtag(), contentId);
+ contentId++;
+ }
+
+ if (bodyPartContent != null) {
+ bodyPartContents.add(bodyPartContent);
+ }
+ }
+
+ return mimeReaderWriter.getMimeMultipart(bodyPartContents);
+ }
+
+ private DataSource createBatchInsertOrUpdateEntityPart(String table, Entity entity, String verb,
+ boolean includeEtag, int contentId) {
+
+ URI path;
+ if ("POST".equals(verb)) {
+ path = channel.resource(url).path(table).getURI();
+ }
+ else {
+ path = channel.resource(url).path(getEntityPath(table, entity.getPartitionKey(), entity.getRowKey()))
+ .getURI();
+ }
+
+ //
+ // Stream content into byte[] so that we have the length
+ //
+ InputStream stream = atomReaderWriter.generateEntityEntry(entity);
+ byte[] bytes = inputStreamToByteArray(stream);
+
+ //
+ // Create body of MIME part as the HTTP request
+ //
+ InternetHeaders headers = new InternetHeaders();
+ headers.addHeader("Content-ID", Integer.toString(contentId));
+ headers.addHeader("Content-Type", "application/atom+xml;type=entry");
+ headers.addHeader("Content-Length", Integer.toString(bytes.length));
+ if (includeEtag) {
+ headers.addHeader("If-Match", entity.getEtag());
+ }
+
+ //TODO: Review code to make sure encoding is correct
+ ByteArrayOutputStream httpRequest = new ByteArrayOutputStream();
+ httpReaderWriter.appendMethod(httpRequest, verb, path);
+ httpReaderWriter.appendHeaders(httpRequest, headers);
+ httpReaderWriter.appendEntity(httpRequest, new ByteArrayInputStream(bytes));
+
+ DataSource bodyPartContent = new InputStreamDataSource(new ByteArrayInputStream(httpRequest.toByteArray()),
+ "application/http");
+ return bodyPartContent;
+ }
+
+ private DataSource createBatchDeleteEntityPart(String table, String partitionKey, String rowKey, String etag,
+ int contentId) {
+
+ URI path = channel.resource(url).path(getEntityPath(table, partitionKey, rowKey)).getURI();
+
+ //
+ // Create body of MIME part as the HTTP request
+ //
+ InternetHeaders headers = new InternetHeaders();
+ headers.addHeader("Content-ID", Integer.toString(contentId));
+ headers.addHeader("If-Match", etag == null ? "*" : etag);
+
+ //TODO: Review code to make sure encoding is correct
+ ByteArrayOutputStream httpRequest = new ByteArrayOutputStream();
+ httpReaderWriter.appendMethod(httpRequest, "DELETE", path);
+ httpReaderWriter.appendHeaders(httpRequest, headers);
+ httpReaderWriter.appendEntity(httpRequest, new ByteArrayInputStream(new byte[0]));
+
+ DataSource bodyPartContent = new InputStreamDataSource(new ByteArrayInputStream(httpRequest.toByteArray()),
+ "application/http");
+ return bodyPartContent;
+ }
+
+ private List parseBatchResponse(ClientResponse response, BatchOperations operations) {
+ List parts = mimeReaderWriter.parseParts(response.getEntityInputStream(), response.getHeaders()
+ .getFirst("Content-Type"));
+
+ if (parts.size() != operations.getOperations().size()) {
+ throw new UniformInterfaceException(String.format(
+ "Batch response from server does not contain the correct amount "
+ + "of parts (expecting %d, received %d instead)", parts.size(), operations.getOperations()
+ .size()), response);
+ }
+
+ List result = new ArrayList();
+ for (int i = 0; i < parts.size(); i++) {
+ DataSource ds = parts.get(i);
+ Operation operation = operations.getOperations().get(i);
+
+ StatusLine status = httpReaderWriter.parseStatusLine(ds);
+ InternetHeaders headers = httpReaderWriter.parseHeaders(ds);
+ InputStream content = httpReaderWriter.parseEntity(ds);
+
+ if (status.getStatus() >= 400) {
+ // Create dummy client response with status, headers and content
+ InBoundHeaders inBoundHeaders = new InBoundHeaders();
+
+ Enumeration e = headers.getAllHeaders();
+ while (e.hasMoreElements()) {
+ Header header = e.nextElement();
+ inBoundHeaders.putSingle(header.getName(), header.getValue());
+ }
+
+ ClientResponse dummyResponse = new ClientResponse(status.getStatus(), inBoundHeaders, content, null);
+
+ // Wrap into a ServiceException
+ UniformInterfaceException exception = new UniformInterfaceException(dummyResponse);
+ ServiceException serviceException = new ServiceException(exception);
+ serviceException = ServiceExceptionFactory.process("table", serviceException);
+ Error error = new Error().setError(serviceException);
+
+ result.add(error);
+ }
+ else if (operation instanceof InsertEntityOperation) {
+ InsertEntity opResult = new InsertEntity().setEntity(atomReaderWriter.parseEntityEntry(content));
+ result.add(opResult);
+ }
+ else if ((operation instanceof UpdateEntityOperation) || (operation instanceof MergeEntityOperation)
+ || (operation instanceof InsertOrReplaceEntityOperation)
+ || (operation instanceof InsertOrMergeEntityOperation)) {
+ UpdateEntity opResult = new UpdateEntity().setEtag(headers.getHeader("ETag", null));
+ result.add(opResult);
+ }
+ else if (operation instanceof DeleteEntityOperation) {
+ DeleteEntity opResult = new DeleteEntity();
+ result.add(opResult);
+ }
+ }
+
+ return result;
+ }
+
+ private byte[] inputStreamToByteArray(InputStream inputStream) {
+ try {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ byte[] buffer = new byte[1024];
+ try {
+ while (true) {
+ int n = inputStream.read(buffer);
+ if (n == -1)
+ break;
+ outputStream.write(buffer, 0, n);
+ }
+ }
+ finally {
+ inputStream.close();
+ }
+ return outputStream.toByteArray();
+ }
+ catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/XMLStreamFactory.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/XMLStreamFactory.java
new file mode 100644
index 000000000000..0e97c0193ab5
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/XMLStreamFactory.java
@@ -0,0 +1,13 @@
+package com.microsoft.windowsazure.services.table.implementation;
+
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamWriter;
+
+public interface XMLStreamFactory {
+ XMLStreamWriter getWriter(OutputStream stream);
+
+ XMLStreamReader getReader(InputStream stream);
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchOperations.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchOperations.java
new file mode 100644
index 000000000000..f1df873ac4da
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchOperations.java
@@ -0,0 +1,208 @@
+package com.microsoft.windowsazure.services.table.models;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BatchOperations {
+ private List operations = new ArrayList();
+
+ public List getOperations() {
+ return operations;
+ }
+
+ public void setOperations(List operations) {
+ this.operations = operations;
+ }
+
+ public BatchOperations addInsertEntity(String table, Entity entity) {
+ this.operations.add(new InsertEntityOperation().setTable(table).setEntity(entity));
+ return this;
+ }
+
+ public BatchOperations addUpdateEntity(String table, Entity entity) {
+ this.operations.add(new UpdateEntityOperation().setTable(table).setEntity(entity));
+ return this;
+ }
+
+ public BatchOperations addMergeEntity(String table, Entity entity) {
+ this.operations.add(new MergeEntityOperation().setTable(table).setEntity(entity));
+ return this;
+ }
+
+ public BatchOperations addInsertOrReplaceEntity(String table, Entity entity) {
+ this.operations.add(new InsertOrReplaceEntityOperation().setTable(table).setEntity(entity));
+ return this;
+ }
+
+ public BatchOperations addInsertOrMergeEntity(String table, Entity entity) {
+ this.operations.add(new InsertOrMergeEntityOperation().setTable(table).setEntity(entity));
+ return this;
+ }
+
+ public BatchOperations addDeleteEntity(String table, String partitionKey, String rowKey, String etag) {
+ this.operations.add(new DeleteEntityOperation().setTable(table).setPartitionKey(partitionKey).setRowKey(rowKey)
+ .setEtag(etag));
+ return this;
+ }
+
+ public static abstract class Operation {
+ }
+
+ public static class InsertEntityOperation extends Operation {
+ private String table;
+ private Entity entity;
+
+ public String getTable() {
+ return table;
+ }
+
+ public InsertEntityOperation setTable(String table) {
+ this.table = table;
+ return this;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public InsertEntityOperation setEntity(Entity entity) {
+ this.entity = entity;
+ return this;
+ }
+ }
+
+ public static class UpdateEntityOperation extends Operation {
+ private String table;
+ private Entity entity;
+
+ public String getTable() {
+ return table;
+ }
+
+ public UpdateEntityOperation setTable(String table) {
+ this.table = table;
+ return this;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public UpdateEntityOperation setEntity(Entity entity) {
+ this.entity = entity;
+ return this;
+ }
+ }
+
+ public static class MergeEntityOperation extends Operation {
+ private String table;
+ private Entity entity;
+
+ public String getTable() {
+ return table;
+ }
+
+ public MergeEntityOperation setTable(String table) {
+ this.table = table;
+ return this;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public MergeEntityOperation setEntity(Entity entity) {
+ this.entity = entity;
+ return this;
+ }
+ }
+
+ public static class InsertOrReplaceEntityOperation extends Operation {
+ private String table;
+ private Entity entity;
+
+ public String getTable() {
+ return table;
+ }
+
+ public InsertOrReplaceEntityOperation setTable(String table) {
+ this.table = table;
+ return this;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public InsertOrReplaceEntityOperation setEntity(Entity entity) {
+ this.entity = entity;
+ return this;
+ }
+ }
+
+ public static class InsertOrMergeEntityOperation extends Operation {
+ private String table;
+ private Entity entity;
+
+ public String getTable() {
+ return table;
+ }
+
+ public InsertOrMergeEntityOperation setTable(String table) {
+ this.table = table;
+ return this;
+ }
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public InsertOrMergeEntityOperation setEntity(Entity entity) {
+ this.entity = entity;
+ return this;
+ }
+ }
+
+ public static class DeleteEntityOperation extends Operation {
+ private String table;
+ private String partitionKey;
+ private String rowKey;
+ private String etag;
+
+ public String getTable() {
+ return table;
+ }
+
+ public DeleteEntityOperation setTable(String table) {
+ this.table = table;
+ return this;
+ }
+
+ public String getPartitionKey() {
+ return partitionKey;
+ }
+
+ public DeleteEntityOperation setPartitionKey(String partitionKey) {
+ this.partitionKey = partitionKey;
+ return this;
+ }
+
+ public String getRowKey() {
+ return rowKey;
+ }
+
+ public DeleteEntityOperation setRowKey(String rowKey) {
+ this.rowKey = rowKey;
+ return this;
+ }
+
+ public String getEtag() {
+ return etag;
+ }
+
+ public DeleteEntityOperation setEtag(String etag) {
+ this.etag = etag;
+ return this;
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchResult.java
new file mode 100644
index 000000000000..30a0b3beebd9
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchResult.java
@@ -0,0 +1,65 @@
+package com.microsoft.windowsazure.services.table.models;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.microsoft.windowsazure.services.core.ServiceException;
+
+public class BatchResult {
+ private List entries = new ArrayList();
+
+ public List getEntries() {
+ return entries;
+ }
+
+ public BatchResult setEntries(List entries) {
+ this.entries = entries;
+ return this;
+ }
+
+ public static abstract class Entry {
+ }
+
+ public static class InsertEntity extends Entry {
+ private Entity entity;
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public InsertEntity setEntity(Entity entity) {
+ this.entity = entity;
+ return this;
+ }
+ }
+
+ public static class UpdateEntity extends Entry {
+ private String etag;
+
+ public String getEtag() {
+ return etag;
+ }
+
+ public UpdateEntity setEtag(String etag) {
+ this.etag = etag;
+ return this;
+ }
+ }
+
+ public static class DeleteEntity extends Entry {
+
+ }
+
+ public static class Error extends Entry {
+ private ServiceException error;
+
+ public ServiceException getError() {
+ return error;
+ }
+
+ public Error setError(ServiceException error) {
+ this.error = error;
+ return this;
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilter.java
new file mode 100644
index 000000000000..462be9a865c6
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilter.java
@@ -0,0 +1,34 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class BinaryFilter extends Filter {
+ private String operator;
+ private Filter left;
+ private Filter right;
+
+ public String getOperator() {
+ return operator;
+ }
+
+ public BinaryFilter setOperator(String operator) {
+ this.operator = operator;
+ return this;
+ }
+
+ public Filter getLeft() {
+ return left;
+ }
+
+ public BinaryFilter setLeft(Filter left) {
+ this.left = left;
+ return this;
+ }
+
+ public Filter getRight() {
+ return right;
+ }
+
+ public BinaryFilter setRight(Filter right) {
+ this.right = right;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilter.java
new file mode 100644
index 000000000000..9dd16b1a1daa
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilter.java
@@ -0,0 +1,14 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class ConstantFilter extends Filter {
+ private Object value;
+
+ public Object getValue() {
+ return value;
+ }
+
+ public ConstantFilter setValue(Object value) {
+ this.value = value;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/DeleteEntityOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/DeleteEntityOptions.java
new file mode 100644
index 000000000000..441f3020822b
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/DeleteEntityOptions.java
@@ -0,0 +1,21 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class DeleteEntityOptions extends TableServiceOptions {
+ private String etag;
+
+ @Override
+ public DeleteEntityOptions setTimeout(Integer timeout) {
+ super.setTimeout(timeout);
+ return this;
+ }
+
+ public String getEtag() {
+ return etag;
+ }
+
+ public DeleteEntityOptions setEtag(String etag) {
+ this.etag = etag;
+ return this;
+ }
+
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/EdmType.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/EdmType.java
new file mode 100644
index 000000000000..5a82435b9337
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/EdmType.java
@@ -0,0 +1,12 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class EdmType {
+ public static final String DATETIME = "Edm.DateTime";
+ public static final String BINARY = "Edm.Binary";
+ public static final String BOOLEAN = "Edm.Boolean";
+ public static final String DOUBLE = "Edm.Double";
+ public static final String GUID = "Edm.Guid";
+ public static final String INT32 = "Edm.Int32";
+ public static final String INT64 = "Edm.Int64";
+ public static final String STRING = "Edm.String";
+}
\ No newline at end of file
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Entity.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Entity.java
new file mode 100644
index 000000000000..ce1d6dff3933
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Entity.java
@@ -0,0 +1,77 @@
+package com.microsoft.windowsazure.services.table.models;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+public class Entity {
+ private String etag;
+ private Map properties = new HashMap();
+
+ public String getEtag() {
+ return etag;
+ }
+
+ public Entity setEtag(String etag) {
+ this.etag = etag;
+ return this;
+ }
+
+ public String getPartitionKey() {
+ Property p = getProperty("PartitionKey");
+ return p == null ? null : (String) p.getValue();
+ }
+
+ public Entity setPartitionKey(String partitionKey) {
+ setProperty("PartitionKey", null, partitionKey);
+ return this;
+ }
+
+ public String getRowKey() {
+ Property p = getProperty("RowKey");
+ return p == null ? null : (String) p.getValue();
+ }
+
+ public Entity setRowKey(String rowKey) {
+ setProperty("RowKey", null, rowKey);
+ return this;
+ }
+
+ public Date getTimestamp() {
+ Property p = getProperty("Timestamp");
+ return p == null ? null : (Date) p.getValue();
+ }
+
+ public Entity setTimestamp(Date timestamp) {
+ setProperty("Timestamp", null, timestamp);
+ return this;
+ }
+
+ public Map getProperties() {
+ return properties;
+ }
+
+ public Entity setProperties(Map properties) {
+ this.properties = properties;
+ return this;
+ }
+
+ public Property getProperty(String name) {
+ return properties.get(name);
+ }
+
+ public Entity setProperty(String name, Property property) {
+ this.properties.put(name, property);
+ return this;
+ }
+
+ public Entity setProperty(String name, String edmType, Object value) {
+ setProperty(name, new Property().setEdmType(edmType).setValue(value));
+ return this;
+ }
+
+ public Object getPropertyValue(String name) {
+ Property p = getProperty(name);
+ return p == null ? null : p.getValue();
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Filter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Filter.java
new file mode 100644
index 000000000000..f0a54f58b09b
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Filter.java
@@ -0,0 +1,51 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class Filter {
+ public static UnaryFilter not(Filter operand) {
+ return new UnaryFilter().setOperator("not").setOperand(operand);
+ }
+
+ public static BinaryFilter and(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("and").setLeft(left).setRight(right);
+ }
+
+ public static BinaryFilter or(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("or").setLeft(left).setRight(right);
+ }
+
+ public static BinaryFilter eq(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("eq").setLeft(left).setRight(right);
+ }
+
+ public static BinaryFilter ne(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("ne").setLeft(left).setRight(right);
+ }
+
+ public static BinaryFilter ge(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("ge").setLeft(left).setRight(right);
+ }
+
+ public static BinaryFilter gt(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("gt").setLeft(left).setRight(right);
+ }
+
+ public static BinaryFilter lt(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("lt").setLeft(left).setRight(right);
+ }
+
+ public static BinaryFilter le(Filter left, Filter right) {
+ return new BinaryFilter().setOperator("le").setLeft(left).setRight(right);
+ }
+
+ public static ConstantFilter constant(Object value) {
+ return new ConstantFilter().setValue(value);
+ }
+
+ public static LitteralFilter litteral(String value) {
+ return new LitteralFilter().setLitteral(value);
+ }
+
+ public static RawStringFilter rawString(String value) {
+ return new RawStringFilter().setRawString(value);
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetEntityResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetEntityResult.java
new file mode 100644
index 000000000000..34195d479504
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetEntityResult.java
@@ -0,0 +1,13 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class GetEntityResult {
+ private Entity entity;
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public void setEntity(Entity entity) {
+ this.entity = entity;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetServicePropertiesResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetServicePropertiesResult.java
new file mode 100644
index 000000000000..30ab7196d448
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetServicePropertiesResult.java
@@ -0,0 +1,27 @@
+/**
+ * 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.models;
+
+public class GetServicePropertiesResult {
+ private ServiceProperties value;
+
+ public ServiceProperties getValue() {
+ return value;
+ }
+
+ public void setValue(ServiceProperties value) {
+ this.value = value;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetTableResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetTableResult.java
new file mode 100644
index 000000000000..de199b24f075
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetTableResult.java
@@ -0,0 +1,13 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class GetTableResult {
+ private TableEntry tableEntry;
+
+ public TableEntry getTableEntry() {
+ return tableEntry;
+ }
+
+ public void setTableEntry(TableEntry tableEntry) {
+ this.tableEntry = tableEntry;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/InsertEntityResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/InsertEntityResult.java
new file mode 100644
index 000000000000..666fc9634da3
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/InsertEntityResult.java
@@ -0,0 +1,13 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class InsertEntityResult {
+ private Entity entity;
+
+ public Entity getEntity() {
+ return entity;
+ }
+
+ public void setEntity(Entity entity) {
+ this.entity = entity;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ListTablesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ListTablesOptions.java
new file mode 100644
index 000000000000..25f042b564f0
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ListTablesOptions.java
@@ -0,0 +1,20 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class ListTablesOptions extends TableServiceOptions {
+ private String prefix;
+
+ @Override
+ public ListTablesOptions setTimeout(Integer timeout) {
+ super.setTimeout(timeout);
+ return this;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public ListTablesOptions setPrefix(String prefix) {
+ this.prefix = prefix;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilter.java
new file mode 100644
index 000000000000..d67f47162c4d
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilter.java
@@ -0,0 +1,14 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class LitteralFilter extends Filter {
+ private String litteral;
+
+ public String getLitteral() {
+ return litteral;
+ }
+
+ public LitteralFilter setLitteral(String litteral) {
+ this.litteral = litteral;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Property.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Property.java
new file mode 100644
index 000000000000..1e90530e04f0
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Property.java
@@ -0,0 +1,24 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class Property {
+ private String edmType;
+ private Object value;
+
+ public String getEdmType() {
+ return edmType;
+ }
+
+ public Property setEdmType(String edmType) {
+ this.edmType = edmType;
+ return this;
+ }
+
+ public Object getValue() {
+ return value;
+ }
+
+ public Property setValue(Object value) {
+ this.value = value;
+ return this;
+ }
+}
\ No newline at end of file
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Query.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Query.java
new file mode 100644
index 000000000000..57e7f8c30b66
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Query.java
@@ -0,0 +1,67 @@
+package com.microsoft.windowsazure.services.table.models;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class Query {
+ private List selectFields = new ArrayList();
+ private String from;
+ private Filter filter;
+ private List orderByFields = new ArrayList();
+ private Integer top;
+
+ public List getSelectFields() {
+ return selectFields;
+ }
+
+ public Query setSelectFields(List selectFields) {
+ this.selectFields = selectFields;
+ return this;
+ }
+
+ public Query addSelectField(String selectField) {
+ this.selectFields.add(selectField);
+ return this;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public Query setFrom(String from) {
+ this.from = from;
+ return this;
+ }
+
+ public Filter getFilter() {
+ return filter;
+ }
+
+ public Query setFilter(Filter filter) {
+ this.filter = filter;
+ return this;
+ }
+
+ public List getOrderByFields() {
+ return orderByFields;
+ }
+
+ public Query setOrderByFields(List orderByFields) {
+ this.orderByFields = orderByFields;
+ return this;
+ }
+
+ public Query addOrderByField(String orderByField) {
+ this.orderByFields.add(orderByField);
+ return this;
+ }
+
+ public Integer getTop() {
+ return top;
+ }
+
+ public Query setTop(Integer top) {
+ this.top = top;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesOptions.java
new file mode 100644
index 000000000000..1de1cd47426b
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesOptions.java
@@ -0,0 +1,40 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class QueryEntitiesOptions extends TableServiceOptions {
+ private Query query;
+ public String nextPartitionKey;
+ public String nextRowKey;
+
+ @Override
+ public QueryEntitiesOptions setTimeout(Integer timeout) {
+ super.setTimeout(timeout);
+ return this;
+ }
+
+ public Query getQuery() {
+ return query;
+ }
+
+ public QueryEntitiesOptions setQuery(Query query) {
+ this.query = query;
+ return this;
+ }
+
+ public String getNextPartitionKey() {
+ return nextPartitionKey;
+ }
+
+ public QueryEntitiesOptions setNextPartitionKey(String nextPartitionKey) {
+ this.nextPartitionKey = nextPartitionKey;
+ return this;
+ }
+
+ public String getNextRowKey() {
+ return nextRowKey;
+ }
+
+ public QueryEntitiesOptions setNextRowKey(String nextRowKey) {
+ this.nextRowKey = nextRowKey;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesResult.java
new file mode 100644
index 000000000000..3977ccc9b97d
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesResult.java
@@ -0,0 +1,34 @@
+package com.microsoft.windowsazure.services.table.models;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class QueryEntitiesResult {
+ private String nextPartitionKey;
+ private String nextRowKey;
+ private List entities = new ArrayList();
+
+ public List getEntities() {
+ return entities;
+ }
+
+ public void setEntities(List entities) {
+ this.entities = entities;
+ }
+
+ public String getNextPartitionKey() {
+ return nextPartitionKey;
+ }
+
+ public void setNextPartitionKey(String nextPartitionKey) {
+ this.nextPartitionKey = nextPartitionKey;
+ }
+
+ public String getNextRowKey() {
+ return nextRowKey;
+ }
+
+ public void setNextRowKey(String nextRowKey) {
+ this.nextRowKey = nextRowKey;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesOptions.java
new file mode 100644
index 000000000000..d402cd8527cc
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesOptions.java
@@ -0,0 +1,30 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class QueryTablesOptions extends TableServiceOptions {
+ private String nextTableName;
+ private Query query;
+
+ @Override
+ public QueryTablesOptions setTimeout(Integer timeout) {
+ super.setTimeout(timeout);
+ return this;
+ }
+
+ public Query getQuery() {
+ return query;
+ }
+
+ public QueryTablesOptions setQuery(Query query) {
+ this.query = query;
+ return this;
+ }
+
+ public String getNextTableName() {
+ return nextTableName;
+ }
+
+ public QueryTablesOptions setNextTableName(String nextTableName) {
+ this.nextTableName = nextTableName;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesResult.java
new file mode 100644
index 000000000000..dfd1c1b7f4aa
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesResult.java
@@ -0,0 +1,24 @@
+package com.microsoft.windowsazure.services.table.models;
+
+import java.util.List;
+
+public class QueryTablesResult {
+ private String nextTableName;
+ private List tables;
+
+ public String getNextTableName() {
+ return nextTableName;
+ }
+
+ public void setNextTableName(String nextTableName) {
+ this.nextTableName = nextTableName;
+ }
+
+ public List getTables() {
+ return tables;
+ }
+
+ public void setTables(List tables) {
+ this.tables = tables;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/RawStringFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/RawStringFilter.java
new file mode 100644
index 000000000000..ffc8e38aef00
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/RawStringFilter.java
@@ -0,0 +1,14 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class RawStringFilter extends Filter {
+ private String rawString;
+
+ public String getRawString() {
+ return rawString;
+ }
+
+ public RawStringFilter setRawString(String rawString) {
+ this.rawString = rawString;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ServiceProperties.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ServiceProperties.java
new file mode 100644
index 000000000000..2e068e4fe9df
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ServiceProperties.java
@@ -0,0 +1,161 @@
+/**
+ * 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.models;
+
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+
+@XmlRootElement(name = "StorageServiceProperties")
+public class ServiceProperties {
+ private Logging logging = new Logging();
+ private Metrics metrics = new Metrics();
+
+ @XmlElement(name = "Logging")
+ public Logging getLogging() {
+ return logging;
+ }
+
+ public void setLogging(Logging logging) {
+ this.logging = logging;
+ }
+
+ @XmlElement(name = "Metrics")
+ public Metrics getMetrics() {
+ return metrics;
+ }
+
+ public void setMetrics(Metrics metrics) {
+ this.metrics = metrics;
+ }
+
+ public static class Logging {
+ private String version;
+ private Boolean delete;
+ private Boolean read;
+ private Boolean write;
+ private RetentionPolicy retentionPolicy;
+
+ @XmlElement(name = "RetentionPolicy")
+ public RetentionPolicy getRetentionPolicy() {
+ return retentionPolicy;
+ }
+
+ public void setRetentionPolicy(RetentionPolicy retentionPolicy) {
+ this.retentionPolicy = retentionPolicy;
+ }
+
+ @XmlElement(name = "Write")
+ public boolean isWrite() {
+ return write;
+ }
+
+ public void setWrite(boolean write) {
+ this.write = write;
+ }
+
+ @XmlElement(name = "Read")
+ public boolean isRead() {
+ return read;
+ }
+
+ public void setRead(boolean read) {
+ this.read = read;
+ }
+
+ @XmlElement(name = "Delete")
+ public boolean isDelete() {
+ return delete;
+ }
+
+ public void setDelete(boolean delete) {
+ this.delete = delete;
+ }
+
+ @XmlElement(name = "Version")
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+ }
+
+ public static class Metrics {
+ private String version;
+ private boolean enabled;
+ private Boolean includeAPIs;
+ private RetentionPolicy retentionPolicy;
+
+ @XmlElement(name = "RetentionPolicy")
+ public RetentionPolicy getRetentionPolicy() {
+ return retentionPolicy;
+ }
+
+ public void setRetentionPolicy(RetentionPolicy retentionPolicy) {
+ this.retentionPolicy = retentionPolicy;
+ }
+
+ @XmlElement(name = "IncludeAPIs")
+ public Boolean isIncludeAPIs() {
+ return includeAPIs;
+ }
+
+ public void setIncludeAPIs(Boolean includeAPIs) {
+ this.includeAPIs = includeAPIs;
+ }
+
+ @XmlElement(name = "Enabled")
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ @XmlElement(name = "Version")
+ public String getVersion() {
+ return version;
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+ }
+
+ public static class RetentionPolicy {
+ private boolean enabled;
+ private Integer days; // nullable, because optional if "enabled" is false
+
+ @XmlElement(name = "Days")
+ public Integer getDays() {
+ return days;
+ }
+
+ public void setDays(Integer days) {
+ this.days = days;
+ }
+
+ @XmlElement(name = "Enabled")
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableEntry.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableEntry.java
new file mode 100644
index 000000000000..917cecc3409c
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableEntry.java
@@ -0,0 +1,13 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class TableEntry {
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableServiceOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableServiceOptions.java
new file mode 100644
index 000000000000..e42240885c15
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableServiceOptions.java
@@ -0,0 +1,29 @@
+/**
+ * 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.models;
+
+public class TableServiceOptions {
+ // Nullable because it is optional
+ private Integer timeout;
+
+ public Integer getTimeout() {
+ return timeout;
+ }
+
+ public TableServiceOptions setTimeout(Integer timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilter.java
new file mode 100644
index 000000000000..42d4b7a870fc
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilter.java
@@ -0,0 +1,24 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class UnaryFilter extends Filter {
+ private String operator;
+ private Filter operand;
+
+ public String getOperator() {
+ return operator;
+ }
+
+ public UnaryFilter setOperator(String operator) {
+ this.operator = operator;
+ return this;
+ }
+
+ public Filter getOperand() {
+ return operand;
+ }
+
+ public UnaryFilter setOperand(Filter operand) {
+ this.operand = operand;
+ return this;
+ }
+}
diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UpdateEntityResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UpdateEntityResult.java
new file mode 100644
index 000000000000..2db782e63693
--- /dev/null
+++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UpdateEntityResult.java
@@ -0,0 +1,13 @@
+package com.microsoft.windowsazure.services.table.models;
+
+public class UpdateEntityResult {
+ private String etag;
+
+ public String getEtag() {
+ return etag;
+ }
+
+ public void setEtag(String etag) {
+ this.etag = etag;
+ }
+}
diff --git a/microsoft-azure-api/src/main/resources/META-INF/services/com.microsoft.windowsazure.services.core.Builder$Exports b/microsoft-azure-api/src/main/resources/META-INF/services/com.microsoft.windowsazure.services.core.Builder$Exports
index cc7087393981..07fdb1c4e7b6 100644
--- a/microsoft-azure-api/src/main/resources/META-INF/services/com.microsoft.windowsazure.services.core.Builder$Exports
+++ b/microsoft-azure-api/src/main/resources/META-INF/services/com.microsoft.windowsazure.services.core.Builder$Exports
@@ -1,5 +1,6 @@
com.microsoft.windowsazure.services.blob.Exports
com.microsoft.windowsazure.services.queue.Exports
+com.microsoft.windowsazure.services.table.Exports
com.microsoft.windowsazure.services.serviceBus.Exports
com.microsoft.windowsazure.services.serviceBus.implementation.Exports
com.microsoft.windowsazure.services.core.utils.Exports
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/configuration/builder/DefaultBuilderTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/configuration/builder/DefaultBuilderTest.java
index dc57798032bf..664627699737 100644
--- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/configuration/builder/DefaultBuilderTest.java
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/configuration/builder/DefaultBuilderTest.java
@@ -96,6 +96,7 @@ public void multipleCtorWithNoInjectShouldFail() throws Exception {
// Assert
Assert.assertTrue("Exception must occur", false);
+ Assert.assertNull("Result should be null", result);
}
@Test
@@ -111,6 +112,7 @@ public void multipleCtorWithMultipleInjectShouldFail() throws Exception {
// Assert
Assert.assertTrue("Exception must occur", false);
+ Assert.assertNull("Result should be null", result);
}
@Test
@@ -155,7 +157,7 @@ public void namedParametersFallBackToNonProfileBasedKeys() throws Exception {
// Assert
Assert.assertEquals("fallback", result1.getHello());
Assert.assertEquals("fallback", result2.getHello());
- Assert.assertEquals("fallback", result2.getHello());
+ Assert.assertEquals("fallback", result3.getHello());
}
@Test
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/BlobServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/BlobServiceIntegrationTest.java
index 2e5d4b4d2d0e..57536855c2b5 100644
--- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/BlobServiceIntegrationTest.java
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/BlobServiceIntegrationTest.java
@@ -390,14 +390,20 @@ public void listContainersWithPrefixWorks() throws Exception {
assertEquals(3, results.getMaxResults());
// Act
- ListContainersResult results2 = service.listContainers(new ListContainersOptions().setMarker(results
- .getNextMarker()));
+ ListContainersResult results2 = service.listContainers(new ListContainersOptions().setPrefix(
+ testContainersPrefix).setMarker(results.getNextMarker()));
// Assert
assertNotNull(results2);
- assertEquals(testContainers.length - 3, results2.getContainers().size());
assertNotNull(results2.getNextMarker());
assertEquals(0, results2.getMaxResults());
+
+ // Act
+ ListContainersResult results3 = service.listContainers(new ListContainersOptions()
+ .setPrefix(testContainersPrefix));
+
+ // Assert
+ assertEquals(results.getContainers().size() + results2.getContainers().size(), results3.getContainers().size());
}
@Test
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverterTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverterTests.java
new file mode 100644
index 000000000000..6f514dad26d3
--- /dev/null
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverterTests.java
@@ -0,0 +1,51 @@
+/**
+ * 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.blob.implementation;
+
+import static org.junit.Assert.*;
+
+import java.util.Date;
+
+import org.junit.Test;
+
+import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter;
+
+public class ISO8601DateConverterTests {
+ @Test
+ public void shortFormatWorks() throws Exception {
+ // Arrange
+ ISO8601DateConverter converter = new ISO8601DateConverter();
+ String value = "2012-01-12T00:35:58Z";
+
+ // Act
+ Date result = converter.parse(value);
+
+ // Assert
+ assertNotNull(result);
+ }
+
+ @Test
+ public void longFormatWorks() throws Exception {
+ // Arrange
+ ISO8601DateConverter converter = new ISO8601DateConverter();
+ String value = "2012-01-12T00:35:58.1234567Z";
+
+ // Act
+ Date result = converter.parse(value);
+
+ // Assert
+ assertNotNull(result);
+ }
+}
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/BrokerPropertiesMapperTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/BrokerPropertiesMapperTest.java
index 849fd72e606a..0efb1b72e03d 100644
--- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/BrokerPropertiesMapperTest.java
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/BrokerPropertiesMapperTest.java
@@ -98,7 +98,7 @@ public void deserializingAllPossibleValues() {
assertEquals("corid", properties.getCorrelationId());
assertEquals("sesid", properties.getSessionId());
assertEquals(5, (int) properties.getDeliveryCount());
- assertTrue(Math.abs(lockedUntilDelta) < 500);
+ assertTrue(Math.abs(lockedUntilDelta) < 2000);
assertEquals("loctok", properties.getLockToken());
assertEquals("mesid", properties.getMessageId());
assertEquals("lab", properties.getLabel());
@@ -106,7 +106,7 @@ public void deserializingAllPossibleValues() {
assertEquals(7, (long) properties.getSequenceNumber());
assertEquals(8.123, properties.getTimeToLive(), .001);
assertEquals("to", properties.getTo());
- assertTrue(Math.abs(schedTimeDelta) < 500);
+ assertTrue(Math.abs(schedTimeDelta) < 2000);
assertEquals("reptosesid", properties.getReplyToSessionId());
assertEquals("mesloc", properties.getMessageLocation());
assertEquals("locloc", properties.getLockLocation());
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/IntegrationTestBase.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/IntegrationTestBase.java
index 100f527ae41e..13956e3de931 100644
--- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/IntegrationTestBase.java
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/IntegrationTestBase.java
@@ -19,6 +19,7 @@
import org.junit.Before;
import org.junit.BeforeClass;
+import com.microsoft.windowsazure.services.core.Configuration;
import com.microsoft.windowsazure.services.serviceBus.models.QueueInfo;
import com.microsoft.windowsazure.services.serviceBus.models.ReceiveMessageOptions;
import com.microsoft.windowsazure.services.serviceBus.models.TopicInfo;
@@ -33,7 +34,8 @@ public static void initializeSystem() {
public void initialize() throws Exception {
boolean testAlphaExists = false;
- ServiceBusContract service = ServiceBusService.create();
+ Configuration config = createConfiguration();
+ ServiceBusContract service = ServiceBusService.create(config);
for (QueueInfo queue : iterateQueues(service)) {
String queueName = queue.getPath();
if (queueName.startsWith("Test") || queueName.startsWith("test")) {
@@ -59,4 +61,21 @@ public void initialize() throws Exception {
service.createQueue(new QueueInfo("TestAlpha"));
}
}
+
+ protected static Configuration createConfiguration() throws Exception {
+ Configuration config = Configuration.load();
+ overrideWithEnv(config, ServiceBusConfiguration.URI);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_URI);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_NAME);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_PASSWORD);
+ return config;
+ }
+
+ private static void overrideWithEnv(Configuration config, String key) {
+ String value = System.getenv(key);
+ if (value == null)
+ return;
+
+ config.setProperty(key, value);
+ }
}
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/ServiceBusIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/ServiceBusIntegrationTest.java
index 91a94b9fa529..8b3d3ce8addd 100644
--- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/ServiceBusIntegrationTest.java
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/ServiceBusIntegrationTest.java
@@ -63,11 +63,7 @@ public class ServiceBusIntegrationTest extends IntegrationTestBase {
@Before
public void createService() throws Exception {
// reinitialize configuration from known state
- Configuration config = Configuration.load();
- overrideWithEnv(config, ServiceBusConfiguration.URI);
- overrideWithEnv(config, ServiceBusConfiguration.WRAP_URI);
- overrideWithEnv(config, ServiceBusConfiguration.WRAP_NAME);
- overrideWithEnv(config, ServiceBusConfiguration.WRAP_PASSWORD);
+ Configuration config = createConfiguration();
// add LoggingFilter to any pipeline that is created
Registry builder = (Registry) config.getBuilder();
@@ -84,14 +80,6 @@ public Client alter(Client instance, Builder builder, Map proper
service = ServiceBusService.create();
}
- private static void overrideWithEnv(Configuration config, String key) {
- String value = System.getenv(key);
- if (value == null)
- return;
-
- config.setProperty(key, value);
- }
-
@Test
public void fetchQueueAndListQueuesWorks() throws Exception {
// Arrange
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapRestProxyIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapRestProxyIntegrationTest.java
index 9e58ad475a5a..154ab259ff2c 100644
--- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapRestProxyIntegrationTest.java
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapRestProxyIntegrationTest.java
@@ -29,6 +29,10 @@ public class WrapRestProxyIntegrationTest {
public void serviceCanBeCalledToCreateAccessToken() throws Exception {
// Arrange
Configuration config = Configuration.getInstance();
+ overrideWithEnv(config, ServiceBusConfiguration.URI);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_URI);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_NAME);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_PASSWORD);
WrapContract contract = new WrapRestProxy(config.create(Client.class));
// Act
@@ -44,4 +48,12 @@ public void serviceCanBeCalledToCreateAccessToken() throws Exception {
assertNotNull(result);
assertNotNull(result.getAccessToken());
}
+
+ private static void overrideWithEnv(Configuration config, String key) {
+ String value = System.getenv(key);
+ if (value == null)
+ return;
+
+ config.setProperty(key, value);
+ }
}
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapTokenManagerIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapTokenManagerIntegrationTest.java
index 40b13af6992a..1b49d7af4765 100644
--- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapTokenManagerIntegrationTest.java
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/serviceBus/implementation/WrapTokenManagerIntegrationTest.java
@@ -28,6 +28,10 @@ public class WrapTokenManagerIntegrationTest {
public void wrapClientWillAcquireAccessToken() throws Exception {
// Arrange
Configuration config = Configuration.getInstance();
+ overrideWithEnv(config, ServiceBusConfiguration.URI);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_URI);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_NAME);
+ overrideWithEnv(config, ServiceBusConfiguration.WRAP_PASSWORD);
WrapTokenManager client = config.create("serviceBus", WrapTokenManager.class);
// Act
@@ -37,4 +41,12 @@ public void wrapClientWillAcquireAccessToken() throws Exception {
// Assert
Assert.assertNotNull(accessToken);
}
+
+ private static void overrideWithEnv(Configuration config, String key) {
+ String value = System.getenv(key);
+ if (value == null)
+ return;
+
+ config.setProperty(key, value);
+ }
}
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/IntegrationTestBase.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/IntegrationTestBase.java
new file mode 100644
index 000000000000..8b7b18382d59
--- /dev/null
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/IntegrationTestBase.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;
+
+import com.microsoft.windowsazure.services.core.Configuration;
+
+public abstract class IntegrationTestBase {
+ protected static Configuration createConfiguration() {
+ Configuration config = Configuration.getInstance();
+ overrideWithEnv(config, TableConfiguration.ACCOUNT_NAME);
+ overrideWithEnv(config, TableConfiguration.ACCOUNT_KEY);
+ overrideWithEnv(config, TableConfiguration.URI);
+ return config;
+ }
+
+ private static void overrideWithEnv(Configuration config, String key) {
+ String value = System.getenv(key);
+ if (value == null)
+ return;
+
+ config.setProperty(key, value);
+ }
+
+ protected static boolean isRunningWithEmulator(Configuration config) {
+ String accountName = "devstoreaccount1";
+ String accountKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==";
+
+ return accountName.equals(config.getProperty(TableConfiguration.ACCOUNT_NAME))
+ && accountKey.equals(config.getProperty(TableConfiguration.ACCOUNT_KEY));
+ }
+}
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java
new file mode 100644
index 000000000000..c8143349741d
--- /dev/null
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java
@@ -0,0 +1,913 @@
+/**
+ * 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;
+
+import static org.junit.Assert.*;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.microsoft.windowsazure.services.core.Configuration;
+import com.microsoft.windowsazure.services.core.ExponentialRetryPolicy;
+import com.microsoft.windowsazure.services.core.RetryPolicyFilter;
+import com.microsoft.windowsazure.services.core.ServiceException;
+import com.microsoft.windowsazure.services.table.models.BatchOperations;
+import com.microsoft.windowsazure.services.table.models.BatchResult;
+import com.microsoft.windowsazure.services.table.models.BatchResult.DeleteEntity;
+import com.microsoft.windowsazure.services.table.models.BatchResult.InsertEntity;
+import com.microsoft.windowsazure.services.table.models.BatchResult.UpdateEntity;
+import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions;
+import com.microsoft.windowsazure.services.table.models.EdmType;
+import com.microsoft.windowsazure.services.table.models.Entity;
+import com.microsoft.windowsazure.services.table.models.Filter;
+import com.microsoft.windowsazure.services.table.models.GetEntityResult;
+import com.microsoft.windowsazure.services.table.models.GetTableResult;
+import com.microsoft.windowsazure.services.table.models.InsertEntityResult;
+import com.microsoft.windowsazure.services.table.models.ListTablesOptions;
+import com.microsoft.windowsazure.services.table.models.Query;
+import com.microsoft.windowsazure.services.table.models.QueryEntitiesOptions;
+import com.microsoft.windowsazure.services.table.models.QueryEntitiesResult;
+import com.microsoft.windowsazure.services.table.models.QueryTablesResult;
+import com.microsoft.windowsazure.services.table.models.ServiceProperties;
+import com.microsoft.windowsazure.services.table.models.TableEntry;
+
+public class TableServiceIntegrationTest extends IntegrationTestBase {
+ private static final String testTablesPrefix = "sdktest";
+ private static final String createableTablesPrefix = "csdktest";
+ private static String TEST_TABLE_1;
+ private static String TEST_TABLE_2;
+ private static String TEST_TABLE_3;
+ private static String TEST_TABLE_4;
+ private static String TEST_TABLE_5;
+ private static String TEST_TABLE_6;
+ private static String TEST_TABLE_7;
+ private static String TEST_TABLE_8;
+ private static String CREATABLE_TABLE_1;
+ private static String CREATABLE_TABLE_2;
+ //private static String CREATABLE_TABLE_3;
+ private static String[] creatableTables;
+ private static String[] testTables;
+
+ @BeforeClass
+ public static void setup() throws Exception {
+ //System.setProperty("http.proxyHost", "127.0.0.1");
+ //System.setProperty("http.proxyPort", "8888");
+
+ // Setup container names array (list of container names used by
+ // integration tests)
+ testTables = new String[10];
+ for (int i = 0; i < testTables.length; i++) {
+ testTables[i] = String.format("%s%d", testTablesPrefix, i + 1);
+ }
+
+ creatableTables = new String[10];
+ for (int i = 0; i < creatableTables.length; i++) {
+ creatableTables[i] = String.format("%s%d", createableTablesPrefix, i + 1);
+ }
+
+ TEST_TABLE_1 = testTables[0];
+ TEST_TABLE_2 = testTables[1];
+ TEST_TABLE_3 = testTables[2];
+ TEST_TABLE_4 = testTables[3];
+ TEST_TABLE_5 = testTables[4];
+ TEST_TABLE_6 = testTables[5];
+ TEST_TABLE_7 = testTables[6];
+ TEST_TABLE_8 = testTables[7];
+
+ CREATABLE_TABLE_1 = creatableTables[0];
+ CREATABLE_TABLE_2 = creatableTables[1];
+ //CREATABLE_TABLE_3 = creatableTables[2];
+
+ // Create all test containers and their content
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ deleteAllTables(service, testTables);
+ deleteAllTables(service, creatableTables);
+ createTables(service, testTablesPrefix, testTables);
+ }
+
+ @AfterClass
+ public static void cleanup() throws Exception {
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ deleteTables(service, testTablesPrefix, testTables);
+ deleteTables(service, createableTablesPrefix, creatableTables);
+ }
+
+ private static void createTables(TableContract service, String prefix, String[] list) throws Exception {
+ // Retry creating every table as long as we get "409 - Table being deleted" error
+ service = service.withFilter(new RetryPolicyFilter(new ExponentialRetryPolicy(new int[] { 409 })));
+
+ Set containers = listTables(service, prefix);
+ for (String item : list) {
+ if (!containers.contains(item)) {
+ service.createTable(item);
+ }
+ }
+ }
+
+ private static void deleteTables(TableContract service, String prefix, String[] list) throws Exception {
+ Set containers = listTables(service, prefix);
+ for (String item : list) {
+ if (containers.contains(item)) {
+ service.deleteTable(item);
+ }
+ }
+ }
+
+ private static void deleteAllTables(TableContract service, String[] list) throws Exception {
+ for (String item : list) {
+ try {
+ service.deleteTable(item);
+ }
+ catch (ServiceException e) {
+ // Ignore
+ }
+ }
+ }
+
+ private static Set listTables(TableContract service, String prefix) throws Exception {
+ HashSet result = new HashSet();
+ QueryTablesResult list = service.listTables(new ListTablesOptions().setPrefix(prefix));
+ for (TableEntry item : list.getTables()) {
+ result.add(item.getName());
+ }
+ return result;
+ }
+
+ @Test
+ public void getServicePropertiesWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Don't run this test with emulator, as v1.6 doesn't support this method
+ if (isRunningWithEmulator(config)) {
+ return;
+ }
+
+ // Act
+ ServiceProperties props = service.getServiceProperties().getValue();
+
+ // Assert
+ assertNotNull(props);
+ assertNotNull(props.getLogging());
+ assertNotNull(props.getLogging().getRetentionPolicy());
+ assertNotNull(props.getLogging().getVersion());
+ assertNotNull(props.getMetrics().getRetentionPolicy());
+ assertNotNull(props.getMetrics().getVersion());
+ }
+
+ @Test
+ public void setServicePropertiesWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Don't run this test with emulator, as v1.6 doesn't support this method
+ if (isRunningWithEmulator(config)) {
+ return;
+ }
+
+ // Act
+ ServiceProperties props = service.getServiceProperties().getValue();
+
+ props.getLogging().setRead(true);
+ service.setServiceProperties(props);
+
+ props = service.getServiceProperties().getValue();
+
+ // Assert
+ assertNotNull(props);
+ assertNotNull(props.getLogging());
+ assertNotNull(props.getLogging().getRetentionPolicy());
+ assertNotNull(props.getLogging().getVersion());
+ assertTrue(props.getLogging().isRead());
+ assertNotNull(props.getMetrics().getRetentionPolicy());
+ assertNotNull(props.getMetrics().getVersion());
+ }
+
+ @Test
+ public void createTablesWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Act
+ Exception error;
+ try {
+ service.getTable(CREATABLE_TABLE_1);
+ error = null;
+ }
+ catch (Exception e) {
+ error = e;
+ }
+ service.createTable(CREATABLE_TABLE_1);
+ GetTableResult result = service.getTable(CREATABLE_TABLE_1);
+
+ // Assert
+ assertNotNull(error);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void deleteTablesWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Act
+ service.createTable(CREATABLE_TABLE_2);
+ GetTableResult result = service.getTable(CREATABLE_TABLE_2);
+
+ service.deleteTable(CREATABLE_TABLE_2);
+ Exception error;
+ try {
+ service.getTable(CREATABLE_TABLE_2);
+ error = null;
+ }
+ catch (Exception e) {
+ error = e;
+ }
+
+ // Assert
+ assertNotNull(error);
+ assertNotNull(result);
+ }
+
+ @Test
+ public void queryTablesWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Act
+ QueryTablesResult result = service.queryTables();
+
+ // Assert
+ assertNotNull(result);
+ }
+
+ @Test
+ public void listTablesWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Act
+ QueryTablesResult result = service.listTables();
+
+ // Assert
+ assertNotNull(result);
+ }
+
+ @Test
+ public void queryTablesWithPrefixWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Act
+ QueryTablesResult result = service.listTables(new ListTablesOptions().setPrefix(testTablesPrefix));
+
+ // Assert
+ assertNotNull(result);
+ }
+
+ @Test
+ public void getTableWorks() throws Exception {
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+
+ // Act
+ GetTableResult result = service.getTable(TEST_TABLE_1);
+
+ // Assert
+ assertNotNull(result);
+ }
+
+ @Test
+ public void insertEntityWorks() throws Exception {
+ System.out.println("insertEntityWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("insertEntityWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity);
+
+ // Assert
+ assertNotNull(result);
+ assertNotNull(result.getEntity());
+
+ assertEquals("001", result.getEntity().getPartitionKey());
+ assertEquals("insertEntityWorks", result.getEntity().getRowKey());
+ assertNotNull(result.getEntity().getTimestamp());
+ assertNotNull(result.getEntity().getEtag());
+
+ assertNotNull(result.getEntity().getProperty("test"));
+ assertEquals(true, result.getEntity().getProperty("test").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test2"));
+ assertEquals("value", result.getEntity().getProperty("test2").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test3"));
+ assertEquals(3, result.getEntity().getProperty("test3").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test4"));
+ assertEquals(12345678901L, result.getEntity().getProperty("test4").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test5"));
+ assertTrue(result.getEntity().getProperty("test5").getValue() instanceof Date);
+ }
+
+ @Test
+ public void updateEntityWorks() throws Exception {
+ System.out.println("updateEntityWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("updateEntityWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity);
+ result.getEntity().setProperty("test4", EdmType.INT32, 5);
+ service.updateEntity(TEST_TABLE_2, result.getEntity());
+
+ // Assert
+ }
+
+ @Test
+ public void insertOrReplaceEntityWorks() throws Exception {
+ System.out.println("insertOrReplaceEntityWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("insertOrReplaceEntityWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ service.insertOrReplaceEntity(TEST_TABLE_2, entity);
+ entity.setProperty("test4", EdmType.INT32, 5);
+ entity.setProperty("test6", EdmType.INT32, 6);
+ service.insertOrReplaceEntity(TEST_TABLE_2, entity);
+
+ // Assert
+ }
+
+ @Test
+ public void insertOrMergeEntityWorks() throws Exception {
+ System.out.println("insertOrMergeEntityWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("insertOrMergeEntityWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ service.insertOrMergeEntity(TEST_TABLE_2, entity);
+ entity.setProperty("test4", EdmType.INT32, 5);
+ entity.setProperty("test6", EdmType.INT32, 6);
+ service.insertOrMergeEntity(TEST_TABLE_2, entity);
+
+ // Assert
+ }
+
+ @Test
+ public void mergeEntityWorks() throws Exception {
+ System.out.println("mergeEntityWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("mergeEntityWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity);
+
+ result.getEntity().setProperty("test4", EdmType.INT32, 5);
+ result.getEntity().setProperty("test6", EdmType.INT32, 6);
+ service.mergeEntity(TEST_TABLE_2, result.getEntity());
+
+ // Assert
+ }
+
+ @Test
+ public void deleteEntityWorks() throws Exception {
+ System.out.println("deleteEntityWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("deleteEntityWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity);
+
+ service.deleteEntity(TEST_TABLE_2, result.getEntity().getPartitionKey(), result.getEntity().getRowKey());
+
+ // Assert
+ }
+
+ @Test
+ public void deleteEntityWithETagWorks() throws Exception {
+ System.out.println("deleteEntityWithETagWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("deleteEntityWithETagWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity);
+
+ service.deleteEntity(TEST_TABLE_2, result.getEntity().getPartitionKey(), result.getEntity().getRowKey(),
+ new DeleteEntityOptions().setEtag(result.getEntity().getEtag()));
+
+ // Assert
+ }
+
+ @Test
+ public void getEntityWorks() throws Exception {
+ System.out.println("getEntityWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("getEntityWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ InsertEntityResult insertResult = service.insertEntity(TEST_TABLE_2, entity);
+ GetEntityResult result = service.getEntity(TEST_TABLE_2, insertResult.getEntity().getPartitionKey(),
+ insertResult.getEntity().getRowKey());
+
+ // Assert
+ assertNotNull(result);
+ assertNotNull(result.getEntity());
+
+ assertEquals("001", result.getEntity().getPartitionKey());
+ assertEquals("getEntityWorks", result.getEntity().getRowKey());
+ assertNotNull(result.getEntity().getTimestamp());
+ assertNotNull(result.getEntity().getEtag());
+
+ assertNotNull(result.getEntity().getProperty("test"));
+ assertEquals(true, result.getEntity().getProperty("test").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test2"));
+ assertEquals("value", result.getEntity().getProperty("test2").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test3"));
+ assertEquals(3, result.getEntity().getProperty("test3").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test4"));
+ assertEquals(12345678901L, result.getEntity().getProperty("test4").getValue());
+
+ assertNotNull(result.getEntity().getProperty("test5"));
+ assertTrue(result.getEntity().getProperty("test5").getValue() instanceof Date);
+ }
+
+ @Test
+ public void queryEntitiesWorks() throws Exception {
+ System.out.println("queryEntitiesWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("queryEntitiesWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ // Act
+ service.insertEntity(TEST_TABLE_3, entity);
+ QueryEntitiesResult result = service.queryEntities(TEST_TABLE_3);
+
+ // Assert
+ assertNotNull(result);
+ assertNotNull(result.getEntities());
+ assertEquals(1, result.getEntities().size());
+
+ assertNotNull(result.getEntities().get(0));
+
+ assertEquals("001", result.getEntities().get(0).getPartitionKey());
+ assertEquals("queryEntitiesWorks", result.getEntities().get(0).getRowKey());
+ assertNotNull(result.getEntities().get(0).getTimestamp());
+ assertNotNull(result.getEntities().get(0).getEtag());
+
+ assertNotNull(result.getEntities().get(0).getProperty("test"));
+ assertEquals(true, result.getEntities().get(0).getProperty("test").getValue());
+
+ assertNotNull(result.getEntities().get(0).getProperty("test2"));
+ assertEquals("value", result.getEntities().get(0).getProperty("test2").getValue());
+
+ assertNotNull(result.getEntities().get(0).getProperty("test3"));
+ assertEquals(3, result.getEntities().get(0).getProperty("test3").getValue());
+
+ assertNotNull(result.getEntities().get(0).getProperty("test4"));
+ assertEquals(12345678901L, result.getEntities().get(0).getProperty("test4").getValue());
+
+ assertNotNull(result.getEntities().get(0).getProperty("test5"));
+ assertTrue(result.getEntities().get(0).getProperty("test5").getValue() instanceof Date);
+ }
+
+ @Test
+ public void queryEntitiesWithPaginationWorks() throws Exception {
+ System.out.println("queryEntitiesWithPaginationWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_4;
+ int numberOfEntries = 20;
+ for (int i = 0; i < numberOfEntries; i++) {
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("queryEntitiesWithPaginationWorks-" + i)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ service.insertEntity(table, entity);
+ }
+
+ // Act
+ int entryCount = 0;
+ String nextPartitionKey = null;
+ String nextRowKey = null;
+ while (true) {
+ QueryEntitiesResult result = service.queryEntities(table,
+ new QueryEntitiesOptions().setNextPartitionKey(nextPartitionKey).setNextRowKey(nextRowKey));
+
+ entryCount += result.getEntities().size();
+
+ if (nextPartitionKey == null)
+ break;
+
+ nextPartitionKey = result.getNextPartitionKey();
+ nextRowKey = result.getNextRowKey();
+ }
+
+ // Assert
+ assertEquals(numberOfEntries, entryCount);
+ }
+
+ @Test
+ public void queryEntitiesWithFilterWorks() throws Exception {
+ System.out.println("queryEntitiesWithFilterWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_5;
+ int numberOfEntries = 5;
+ for (int i = 0; i < numberOfEntries; i++) {
+ Entity entity = new Entity().setPartitionKey("001").setRowKey("queryEntitiesWithFilterWorks-" + i)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ service.insertEntity(table, entity);
+ }
+
+ {
+ // Act
+ QueryEntitiesResult result = service.queryEntities(table,
+ new QueryEntitiesOptions().setQuery(new Query().setFilter(Filter.eq(Filter.litteral("RowKey"),
+ Filter.constant("queryEntitiesWithFilterWorks-3")))));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntities().size());
+ assertEquals("queryEntitiesWithFilterWorks-3", result.getEntities().get(0).getRowKey());
+ }
+
+ {
+ // Act
+ QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query()
+ .setFilter(Filter.rawString("RowKey eq 'queryEntitiesWithFilterWorks-3'"))));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntities().size());
+ assertEquals("queryEntitiesWithFilterWorks-3", result.getEntities().get(0).getRowKey());
+ }
+ }
+
+ @Test
+ public void batchInsertWorks() throws Exception {
+ System.out.println("batchInsertWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_6;
+ String partitionKey = "001";
+
+ // Act
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchInsertWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ BatchResult result = service.batch(new BatchOperations().addInsertEntity(table, entity));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntries().size());
+ assertEquals(InsertEntity.class, result.getEntries().get(0).getClass());
+ }
+
+ @Test
+ public void batchUpdateWorks() throws Exception {
+ System.out.println("batchUpdateWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_6;
+ String partitionKey = "001";
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchUpdateWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+ entity = service.insertEntity(table, entity).getEntity();
+
+ // Act
+ entity.setProperty("test", EdmType.BOOLEAN, false);
+ BatchResult result = service.batch(new BatchOperations().addUpdateEntity(table, entity));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntries().size());
+ assertEquals(UpdateEntity.class, result.getEntries().get(0).getClass());
+ }
+
+ @Test
+ public void batchMergeWorks() throws Exception {
+ System.out.println("batchMergeWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_6;
+ String partitionKey = "001";
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchMergeWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+ entity = service.insertEntity(table, entity).getEntity();
+
+ // Act
+ BatchResult result = service.batch(new BatchOperations().addMergeEntity(table, entity));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntries().size());
+ assertEquals(UpdateEntity.class, result.getEntries().get(0).getClass());
+ }
+
+ @Test
+ public void batchInsertOrReplaceWorks() throws Exception {
+ System.out.println("batchInsertOrReplaceWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_6;
+ String partitionKey = "001";
+
+ // Act
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchInsertOrReplaceWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ BatchResult result = service.batch(new BatchOperations().addInsertOrReplaceEntity(table, entity));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntries().size());
+ assertEquals(UpdateEntity.class, result.getEntries().get(0).getClass());
+ }
+
+ @Test
+ public void batchInsertOrMergeWorks() throws Exception {
+ System.out.println("batchInsertOrMergeWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_6;
+ String partitionKey = "001";
+
+ // Act
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchInsertOrMergeWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ BatchResult result = service.batch(new BatchOperations().addInsertOrMergeEntity(table, entity));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntries().size());
+ assertEquals(UpdateEntity.class, result.getEntries().get(0).getClass());
+ }
+
+ @Test
+ public void batchDeleteWorks() throws Exception {
+ System.out.println("batchDeleteWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_6;
+ String partitionKey = "001";
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchDeleteWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+ entity = service.insertEntity(table, entity).getEntity();
+
+ // Act
+ BatchResult result = service.batch(new BatchOperations().addDeleteEntity(table, entity.getPartitionKey(),
+ entity.getRowKey(), entity.getEtag()));
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(1, result.getEntries().size());
+ assertEquals(DeleteEntity.class, result.getEntries().get(0).getClass());
+ }
+
+ @Test
+ public void batchLotsOfInsertsWorks() throws Exception {
+ System.out.println("batchMultipleWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_7;
+ String partitionKey = "001";
+ int insertCount = 100;
+
+ // Act
+ BatchOperations batchOperations = new BatchOperations();
+ for (int i = 0; i < insertCount; i++) {
+
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchWorks-" + i)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ batchOperations.addInsertEntity(table, entity);
+ }
+ BatchResult result = service.batch(batchOperations);
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(insertCount, result.getEntries().size());
+ for (int i = 0; i < insertCount; i++) {
+ assertEquals(InsertEntity.class, result.getEntries().get(i).getClass());
+
+ Entity entity = ((InsertEntity) result.getEntries().get(i)).getEntity();
+
+ assertEquals("001", entity.getPartitionKey());
+ assertEquals("batchWorks-" + i, entity.getRowKey());
+ assertNotNull(entity.getTimestamp());
+ assertNotNull(entity.getEtag());
+
+ assertNotNull(entity.getProperty("test"));
+ assertEquals(true, entity.getProperty("test").getValue());
+
+ assertNotNull(entity.getProperty("test2"));
+ assertEquals("value", entity.getProperty("test2").getValue());
+
+ assertNotNull(entity.getProperty("test3"));
+ assertEquals(3, entity.getProperty("test3").getValue());
+
+ assertNotNull(entity.getProperty("test4"));
+ assertEquals(12345678901L, entity.getProperty("test4").getValue());
+
+ assertNotNull(entity.getProperty("test5"));
+ assertTrue(entity.getProperty("test5").getValue() instanceof Date);
+ }
+ }
+
+ @Test
+ public void batchAllOperationsTogetherWorks() throws Exception {
+ System.out.println("batchAllOperationsWorks()");
+
+ // Arrange
+ Configuration config = createConfiguration();
+ TableContract service = TableService.create(config);
+ String table = TEST_TABLE_8;
+ String partitionKey = "001";
+
+ // Insert a few entities to allow updating them in batch
+ Entity entity1 = new Entity().setPartitionKey(partitionKey).setRowKey("batchAllOperationsWorks-" + 1)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ entity1 = service.insertEntity(table, entity1).getEntity();
+
+ Entity entity2 = new Entity().setPartitionKey(partitionKey).setRowKey("batchAllOperationsWorks-" + 2)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ entity2 = service.insertEntity(table, entity2).getEntity();
+
+ Entity entity3 = new Entity().setPartitionKey(partitionKey).setRowKey("batchAllOperationsWorks-" + 3)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ entity3 = service.insertEntity(table, entity3).getEntity();
+
+ Entity entity4 = new Entity().setPartitionKey(partitionKey).setRowKey("batchAllOperationsWorks-" + 4)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+
+ entity4 = service.insertEntity(table, entity4).getEntity();
+
+ // Act
+ BatchOperations batchOperations = new BatchOperations();
+
+ Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchAllOperationsWorks")
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+ batchOperations.addInsertEntity(table, entity);
+
+ batchOperations.addDeleteEntity(table, entity1.getPartitionKey(), entity1.getRowKey(), entity1.getEtag());
+
+ batchOperations.addUpdateEntity(table, entity2.setProperty("test", EdmType.INT32, 5));
+ batchOperations.addMergeEntity(table, entity3.setProperty("test", EdmType.INT32, 5));
+ batchOperations.addInsertOrReplaceEntity(table, entity4.setProperty("test", EdmType.INT32, 5));
+
+ Entity entity5 = new Entity().setPartitionKey(partitionKey).setRowKey("batchAllOperationsWorks-" + 5)
+ .setProperty("test", EdmType.BOOLEAN, true).setProperty("test2", EdmType.STRING, "value")
+ .setProperty("test3", EdmType.INT32, 3).setProperty("test4", EdmType.INT64, 12345678901L)
+ .setProperty("test5", EdmType.DATETIME, new Date());
+ batchOperations.addInsertOrMergeEntity(table, entity5);
+
+ BatchResult result = service.batch(batchOperations);
+
+ // Assert
+ assertNotNull(result);
+ assertEquals(batchOperations.getOperations().size(), result.getEntries().size());
+ assertEquals(InsertEntity.class, result.getEntries().get(0).getClass());
+ assertEquals(DeleteEntity.class, result.getEntries().get(1).getClass());
+ assertEquals(UpdateEntity.class, result.getEntries().get(2).getClass());
+ assertEquals(UpdateEntity.class, result.getEntries().get(3).getClass());
+ assertEquals(UpdateEntity.class, result.getEntries().get(4).getClass());
+ assertEquals(UpdateEntity.class, result.getEntries().get(5).getClass());
+ }
+}
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 000000000000..671ac7c308a7
--- /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 000000000000..7dc7bd3c4dbd
--- /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 000000000000..ce90b73f1af4
--- /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("\\ // @ ? ", true);
+ }
+
+ @Test
+ public void specialCharsBatch() throws StorageException {
+ doEscapeTest("\\ // @ ? ", true);
+ }
+
+ @Test
+ public void unicode() throws StorageException {
+ doEscapeTest("\u00A9\u770b\u5168\u90e8", false);
+ doEscapeTest("char䏿–‡test", false);
+ doEscapeTest("charä¸Âæ–‡test", false);
+ doEscapeTest("ä¸–ç•Œä½ å¥½", false);
+ }
+
+ @Test
+ public void unicodeBatch() throws StorageException {
+ doEscapeTest("\u00A9\u770b\u5168\u90e8", true);
+ doEscapeTest("char䏿–‡test", true);
+ doEscapeTest("charä¸Âæ–‡test", true);
+ doEscapeTest("ä¸–ç•Œä½ å¥½", true);
+ }
+
+ @Test
+ public void unicodeInQuery() throws StorageException {
+ doQueryEscapeTest("char䏿–‡test");
+ doQueryEscapeTest("charä¸Âæ–‡test");
+ doQueryEscapeTest("ä¸–ç•Œä½ å¥½");
+ doQueryEscapeTest("\u00A9\u770b\u5168\u90e8");
+ }
+
+ @Test
+ public void whiteSpaceOnly() throws StorageException {
+ doEscapeTest(" ", false);
+ }
+
+ @Test
+ public void whiteSpaceOnlyBatch() throws StorageException {
+ doEscapeTest(" ", true);
+ }
+
+ @Test
+ public void whiteSpaceOnlyInQuery() throws StorageException {
+ doQueryEscapeTest(" ");
+ }
+
+ @Test
+ public void xmlTest() 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 000000000000..4beb6ebc4ed2
--- /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 000000000000..dd46b99bae99
--- /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 000000000000..e7380c67c6fe
--- /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 000000000000..b30f200fa61a
--- /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("-", "");
+ }
+}
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriterTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriterTests.java
new file mode 100644
index 000000000000..aa14a71f8597
--- /dev/null
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriterTests.java
@@ -0,0 +1,80 @@
+/**
+ * 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.implementation;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter;
+import com.microsoft.windowsazure.services.core.utils.DefaultDateFactory;
+import com.microsoft.windowsazure.services.table.IntegrationTestBase;
+import com.microsoft.windowsazure.services.table.models.TableEntry;
+
+public class AtomReaderWriterTests extends IntegrationTestBase {
+ @Test
+ public void parseTableEntriesWorks() throws Exception {
+ // Arrange
+ AtomReaderWriter atom = new AtomReaderWriter(new DefaultXMLStreamFactory(), new DefaultDateFactory(),
+ new ISO8601DateConverter(), new DefaultEdmValueConterter(new ISO8601DateConverter()));
+ String feed = "\r\n"
+ + "\r\n"
+ + " Tables\r\n"
+ + " http://rpaquaytest.table.core.windows.net/Tables\r\n"
+ + " 2012-01-10T21:23:30Z\r\n"
+ + " \r\n"
+ + " \r\n"
+ + " http://rpaquaytest.table.core.windows.net/Tables('sdktest1')\r\n"
+ + " \r\n"
+ + " 2012-01-10T21:23:30Z\r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " sdktest1\r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " http://rpaquaytest.table.core.windows.net/Tables('sdktest10')\r\n"
+ + " \r\n"
+ + " 2012-01-10T21:23:30Z\r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n" + " \r\n"
+ + " sdktest10\r\n" + " \r\n"
+ + " \r\n" + " \r\n" + "\r\n";
+ InputStream stream = new ByteArrayInputStream(feed.getBytes("UTF-8"));
+
+ // Act
+ List entries = atom.parseTableEntries(stream);
+
+ // Assert
+ assertNotNull(entries);
+ assertEquals(2, entries.size());
+ assertEquals("sdktest1", entries.get(0).getName());
+ assertEquals("sdktest10", entries.get(1).getName());
+ }
+}
diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/MimeMultipartTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/MimeMultipartTests.java
new file mode 100644
index 000000000000..c01fdeaebc3c
--- /dev/null
+++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/MimeMultipartTests.java
@@ -0,0 +1,336 @@
+/**
+ * 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.implementation;
+
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.StringReader;
+
+import javax.activation.DataSource;
+import javax.mail.BodyPart;
+import javax.mail.MessagingException;
+import javax.mail.MultipartDataSource;
+import javax.mail.internet.MimeBodyPart;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.util.ByteArrayDataSource;
+
+import org.junit.Test;
+
+import com.microsoft.windowsazure.services.table.IntegrationTestBase;
+
+public class MimeMultipartTests extends IntegrationTestBase {
+ @Test
+ public void parseMimeWorks() throws Exception {
+ //@formatter:off
+ String s = "--batchresponse_dc0fea8c-ed83-4aa8-ac9b-bf56a2d46dfb \r\n"
+ + "Content-Type: multipart/mixed; boundary=changesetresponse_8a28b620-b4bb-458c-a177-0959fb14c977\r\n"
+ + "\r\n"
+ + "--changesetresponse_8a28b620-b4bb-458c-a177-0959fb14c977\r\n"
+ + "Content-Type: application/http\r\n"
+ + "Content-Transfer-Encoding: binary\r\n"
+ + "\r\n"
+ + "HTTP/1.1 201 Created\r\n"
+ + "Content-ID: 1\r\n"
+ + "Content-Type: application/atom+xml;charset=utf-8\r\n"
+ + "Cache-Control: no-cache\r\n"
+ + "ETag: W/\"datetime'2009-04-30T20%3A44%3A09.5789464Z'\"\r\n"
+ + "Location: http://myaccount.tables.core.windows.net/Blogs(PartitionKey='Channel_19',RowKey='1')\r\n"
+ + "DataServiceVersion: 1.0;\r\n"
+ + "\r\n"
+ + "\r\n"
+ + "\r\n"
+ + " http://myaccount.tables.core.windows.net/Blogs(PartitionKey='Channel_19',RowKey='1')\r\n"
+ + " \r\n"
+ + " 2009-04-30T20:44:09Z\r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " Channel_19\r\n"
+ + " 1\r\n"
+ + " 2009-04-30T20:44:09.5789464Z\r\n"
+ + " .Net...\r\n"
+ + " 9\r\n"
+ + " \r\n"
+ + " \r\n"
+ + "\r\n"
+ + "\r\n"
+ + "--changesetresponse_8a28b620-b4bb-458c-a177-0959fb14c977\r\n"
+ + "Content-Type: application/http\r\n"
+ + "Content-Transfer-Encoding: binary\r\n"
+ + "\r\n"
+ + "HTTP/1.1 201 Created\r\n"
+ + "Content-ID: 2\r\n"
+ + "Content-Type: application/atom+xml;charset=utf-8\r\n"
+ + "Cache-Control: no-cache\r\n"
+ + "ETag: W/\"datetime'2009-04-30T20%3A44%3A09.5789464Z'\"\r\n"
+ + "Location: http://myaccount.tables.core.windows.net/Blogs(PartitionKey='Channel_19',RowKey='2')\r\n"
+ + "DataServiceVersion: 1.0;\r\n"
+ + "\r\n"
+ + "\r\n"
+ + "\r\n"
+ + " http://myaccount.tables.core.windows.net/Blogs(PartitionKey='Channel_19',RowKey='2')\r\n"
+ + " \r\n"
+ + " 2009-04-30T20:44:09Z\r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " \r\n"
+ + " Channel_19\r\n"
+ + " 2\r\n"
+ + " 2009-04-30T20:44:09.5789464Z\r\n"
+ + " Azure...\r\n"
+ + " 9\r\n"
+ + " \r\n"
+ + " \r\n"
+ + "\r\n"
+ + "\r\n"
+ + "--changesetresponse_8a28b620-b4bb-458c-a177-0959fb14c977\r\n"
+ + "Content-Type: application/http\r\n"
+ + "Content-Transfer-Encoding: binary\r\n"
+ + "\r\n"
+ + "HTTP/1.1 204 No Content\r\n"
+ + "Content-ID: 3\r\n"
+ + "Cache-Control: no-cache\r\n"
+ + "ETag: W/\"datetime'2009-04-30T20%3A44%3A10.0019041Z'\"\r\n"
+ + "DataServiceVersion: 1.0;\r\n"
+ + "\r\n"
+ + "--changesetresponse_8a28b620-b4bb-458c-a177-0959fb14c977\r\n"
+ + "Content-Type: application/http\r\n"
+ + "Content-Transfer-Encoding: binary\r\n"
+ + "\r\n"
+ + "HTTP/1.1 204 No Content\r\n"
+ + "Content-ID: 4\r\n"
+ + "Cache-Control: no-cache\r\n"
+ + "DataServiceVersion: 1.0;\r\n"
+ + "\r\n"
+ + "--changesetresponse_8a28b620-b4bb-458c-a177-0959fb14c977--\r\n"
+ + "--batchresponse_4c637ba4-b2f8-40f8-8856-c2d10d163a83--\r\n";
+ //@formatter:on
+
+ DataSource ds = new ByteArrayDataSource(s,
+ "multipart/mixed; boundary=batchresponse_dc0fea8c-ed83-4aa8-ac9b-bf56a2d46dfb");
+ MimeMultipart m = new MimeMultipart(ds);
+
+ assertEquals(1, m.getCount());
+ assertTrue(m.getBodyPart(0) instanceof MimeBodyPart);
+
+ MimeBodyPart part = (MimeBodyPart) m.getBodyPart(0);
+ String contentType = part.getHeader("Content-Type", ":");
+ assertEquals("multipart/mixed; boundary=changesetresponse_8a28b620-b4bb-458c-a177-0959fb14c977", contentType);
+
+ DataSource ds2 = new ByteArrayDataSource(part.getInputStream(), contentType);
+ MimeMultipart m2 = new MimeMultipart(ds2);
+
+ assertEquals(4, m2.getCount());
+ }
+
+ @Test
+ public void buildMimeWorks() throws Exception {
+ //@formatter:off
+ String changeset1 = "POST http://myaccount.tables.core.windows.net/Blogs HTTP/1.1\r\n" +
+ "Content-ID: 1\r\n" +
+ "Content-Type: application/atom+xml;type=entry\r\n" +
+ "Content-Length: ###\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ " \r\n" +
+ " 2009-04-30T20:45:13.7155321Z\r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " Channel_19\r\n" +
+ " 1\r\n" +
+ " 0001-01-01T00:00:00\r\n" +
+ " 9\r\n" +
+ " .NET...\r\n" +
+ " \r\n" +
+ " \r\n" +
+ "";
+ //@formatter:on
+
+ //
+ // Build inner list of change sets
+ //
+
+ MimeMultipart changeSets = new MimeMultipart(new SetBoundaryMultipartDataSource(
+ "changeset_8a28b620-b4bb-458c-a177-0959fb14c977"));
+
+ MimeBodyPart cs1 = new MimeBodyPart();
+ cs1.setContent(changeset1, "application/http");
+ changeSets.addBodyPart(cs1);
+
+ MimeBodyPart cs2 = new MimeBodyPart();
+ cs2.setContent(changeset1, "application/http");
+ changeSets.addBodyPart(cs2);
+
+ //
+ // Build outer "batch" body part
+ //
+ MimeBodyPart batchbody = new MimeBodyPart();
+ batchbody.setContent(changeSets);
+
+ //
+ // Build outer "batch" multipart
+ //
+ MimeMultipart batch = new MimeMultipart(new SetBoundaryMultipartDataSource(
+ "batch_a1e9d677-b28b-435e-a89e-87e6a768a431"));
+ batch.addBodyPart(batchbody);
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ batch.writeTo(stream);
+
+ String result = stream.toString("UTF-8");
+ //@formatter:off
+ String expectedResult =
+ "--batch_a1e9d677-b28b-435e-a89e-87e6a768a431\r\n" +
+ "\r\n" +
+ "--changeset_8a28b620-b4bb-458c-a177-0959fb14c977\r\n" +
+ "\r\n" +
+ "POST http://myaccount.tables.core.windows.net/Blogs HTTP/1.1\r\n" +
+ "Content-ID: 1\r\n" +
+ "Content-Type: application/atom+xml;type=entry\r\n" +
+ "Content-Length: ###\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ " \r\n" +
+ " 2009-04-30T20:45:13.7155321Z\r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " Channel_19\r\n" +
+ " 1\r\n" +
+ " 0001-01-01T00:00:00\r\n" +
+ " 9\r\n" +
+ " .NET...\r\n" +
+ " \r\n" +
+ " \r\n" +
+ "\r\n" +
+ "--changeset_8a28b620-b4bb-458c-a177-0959fb14c977\r\n" +
+ "\r\n" +
+ "POST http://myaccount.tables.core.windows.net/Blogs HTTP/1.1\r\n" +
+ "Content-ID: 1\r\n" +
+ "Content-Type: application/atom+xml;type=entry\r\n" +
+ "Content-Length: ###\r\n" +
+ "\r\n" +
+ "\r\n" +
+ "\r\n" +
+ " \r\n" +
+ " 2009-04-30T20:45:13.7155321Z\r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " \r\n" +
+ " Channel_19\r\n" +
+ " 1\r\n" +
+ " 0001-01-01T00:00:00\r\n" +
+ " 9\r\n" +
+ " .NET...\r\n" +
+ " \r\n" +
+ " \r\n" +
+ "\r\n" +
+ "--changeset_8a28b620-b4bb-458c-a177-0959fb14c977--\r\n" +
+ "\r\n" +
+ "--batch_a1e9d677-b28b-435e-a89e-87e6a768a431--\r\n";
+ //@formatter:on
+ StringReader reader1 = new StringReader(result);
+ StringReader reader2 = new StringReader(expectedResult);
+
+ for (int i = 0;; i++) {
+ int ch1 = reader1.read();
+ int ch2 = reader2.read();
+ if (ch1 == -1) {
+ assertEquals(-1, ch2);
+ break;
+ }
+ if (ch2 == -1) {
+ assertEquals(-1, ch1);
+ break;
+ }
+
+ if (ch1 != ch2) {
+ int min1 = Math.max(0, i - 20);
+ int max1 = Math.min(result.length(), i + 20);
+
+ int min2 = Math.max(0, i - 20);
+ int max2 = Math.min(expectedResult.length(), i + 20);
+
+ String closeBy1 = result.substring(min1, max1);
+ String closeBy2 = expectedResult.substring(min2, max2);
+
+ assertEquals("Message content are no equal starting at position " + i, closeBy2, closeBy1);
+ }
+ }
+ }
+
+ private class SetBoundaryMultipartDataSource implements MultipartDataSource {
+
+ private final String boundary;
+
+ public SetBoundaryMultipartDataSource(String boundary) {
+ this.boundary = boundary;
+ }
+
+ @Override
+ public String getContentType() {
+ return "multipart/mixed; boundary=" + boundary;
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public String getName() {
+ return null;
+ }
+
+ @Override
+ public OutputStream getOutputStream() throws IOException {
+ return null;
+ }
+
+ @Override
+ public int getCount() {
+ return 0;
+ }
+
+ @Override
+ public BodyPart getBodyPart(int index) throws MessagingException {
+ return null;
+ }
+ }
+}
diff --git a/microsoft-azure-api/src/test/resources/META-INF/com.microsoft.windowsazure.properties b/microsoft-azure-api/src/test/resources/META-INF/com.microsoft.windowsazure.properties
index e7bf06b034de..7739acd3e733 100644
--- a/microsoft-azure-api/src/test/resources/META-INF/com.microsoft.windowsazure.properties
+++ b/microsoft-azure-api/src/test/resources/META-INF/com.microsoft.windowsazure.properties
@@ -7,4 +7,7 @@ blob.accountKey=%BLOB_ACCOUNTKEY%
blob.uri=http://%BLOB_ACCOUNTNAME%.blob.core.windows.net
queue.accountName=%QUEUE_ACCOUNTNAME%
queue.accountKey=%QUEUE_ACCOUNTKEY%
-queue.uri=http://%QUEUE_ACCOUNTNAME%.queue.core.windows.net
\ No newline at end of file
+queue.uri=http://%QUEUE_ACCOUNTNAME%.queue.core.windows.net
+table.accountName=%TABLE_ACCOUNTNAME%
+table.accountKey=%TABLE_ACCOUNTKEY%
+table.uri=http://%TABLE_ACCOUNTNAME%.table.core.windows.net
\ No newline at end of file