From 6a25ef7d48334d8a891d54b30ae9f086e42737ac Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 7 Dec 2011 10:34:55 -0800 Subject: [PATCH 01/59] Initial implementation of table service --- .../windowsazure/services/table/Exports.java | 32 ++++ .../services/table/TableConfiguration.java | 21 +++ .../services/table/TableContract.java | 31 ++++ .../services/table/TableService.java | 38 +++++ .../table/implementation/SharedKeyFilter.java | 26 +++ .../implementation/SharedKeyLiteFilter.java | 26 +++ .../TableExceptionProcessor.java | 107 ++++++++++++ .../table/implementation/TableRestProxy.java | 141 +++++++++++++++ .../models/GetServicePropertiesResult.java | 27 +++ .../table/models/ServiceProperties.java | 161 ++++++++++++++++++ .../table/models/TableServiceOptions.java | 29 ++++ 11 files changed, 639 insertions(+) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyFilter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyLiteFilter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableExceptionProcessor.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableRestProxy.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetServicePropertiesResult.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ServiceProperties.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableServiceOptions.java diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java new file mode 100644 index 000000000000..484cd066f2a0 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java @@ -0,0 +1,32 @@ +/** + * 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.Builder; +import com.microsoft.windowsazure.services.table.implementation.SharedKeyFilter; +import com.microsoft.windowsazure.services.table.implementation.SharedKeyLiteFilter; +import com.microsoft.windowsazure.services.table.implementation.TableExceptionProcessor; +import com.microsoft.windowsazure.services.table.implementation.TableRestProxy; + +public class Exports implements Builder.Exports { + @Override + public void register(Builder.Registry registry) { + registry.add(TableContract.class, TableExceptionProcessor.class); + registry.add(TableExceptionProcessor.class); + registry.add(TableRestProxy.class); + registry.add(SharedKeyLiteFilter.class); + registry.add(SharedKeyFilter.class); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java new file mode 100644 index 000000000000..49c90e0ddf3d --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java @@ -0,0 +1,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 + * + * 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; + +public class TableConfiguration { + public final static String ACCOUNT_NAME = "table.accountName"; + public final static String ACCOUNT_KEY = "table.accountKey"; + public final static String URI = "table.uri"; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java new file mode 100644 index 000000000000..9a849047abc0 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.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; + +import com.microsoft.windowsazure.services.core.FilterableService; +import com.microsoft.windowsazure.services.core.ServiceException; +import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; +import com.microsoft.windowsazure.services.table.models.ServiceProperties; +import com.microsoft.windowsazure.services.table.models.TableServiceOptions; + +public interface TableContract extends FilterableService { + GetServicePropertiesResult getServiceProperties() throws ServiceException; + + GetServicePropertiesResult getServiceProperties(TableServiceOptions options) throws ServiceException; + + void setServiceProperties(ServiceProperties serviceProperties) throws ServiceException; + + void setServiceProperties(ServiceProperties serviceProperties, TableServiceOptions options) throws ServiceException; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java new file mode 100644 index 000000000000..2e732e4fc1d6 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java @@ -0,0 +1,38 @@ +/** + * 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 class TableService { + private TableService() { + } + + public static TableContract create() { + return create(null, Configuration.getInstance()); + } + + public static TableContract create(Configuration config) { + return create(null, config); + } + + public static TableContract create(String profile) { + return create(profile, Configuration.getInstance()); + } + + public static TableContract create(String profile, Configuration config) { + return config.create(profile, TableContract.class); + } +} 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..9cfbe21f43ff --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/SharedKeyFilter.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 SharedKeyFilter extends com.microsoft.windowsazure.services.blob.implementation.SharedKeyFilter { + public SharedKeyFilter(@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/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..6aadc0650f3c --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableExceptionProcessor.java @@ -0,0 +1,107 @@ +/** + * 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.GetServicePropertiesResult; +import com.microsoft.windowsazure.services.table.models.ServiceProperties; +import com.microsoft.windowsazure.services.table.models.TableServiceOptions; +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("blob", 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)); + } + } +} 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..5e965269030a --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableRestProxy.java @@ -0,0 +1,141 @@ +/** + * 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.Arrays; +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; +import javax.inject.Named; + +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.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.models.GetServicePropertiesResult; +import com.microsoft.windowsazure.services.table.models.ServiceProperties; +import com.microsoft.windowsazure.services.table.models.TableServiceOptions; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.WebResource.Builder; + +public class TableRestProxy implements TableContract { + private static final String API_VERSION = "2011-08-18"; + private final HttpURLConnectionClient channel; + private final String accountName; + private final String url; + private final RFC1123DateConverter dateMapper; + private final ServiceFilter[] filters; + private final SharedKeyFilter filter; + + @Inject + public TableRestProxy(HttpURLConnectionClient channel, @Named(TableConfiguration.ACCOUNT_NAME) String accountName, + @Named(TableConfiguration.URI) String url, SharedKeyFilter filter) { + + this.channel = channel; + this.accountName = accountName; + this.url = url; + this.filter = filter; + this.dateMapper = new RFC1123DateConverter(); + this.filters = new ServiceFilter[0]; + channel.addFilter(filter); + } + + public TableRestProxy(HttpURLConnectionClient channel, ServiceFilter[] filters, String accountName, String url, + SharedKeyFilter filter, RFC1123DateConverter dateMapper) { + + this.channel = channel; + this.filters = filters; + this.accountName = accountName; + this.url = url; + this.filter = filter; + this.dateMapper = dateMapper; + } + + @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.accountName, this.url, this.filter, this.dateMapper); + } + + private void ThrowIfError(ClientResponse r) { + PipelineHelpers.ThrowIfError(r); + } + + private WebResource addOptionalQueryParam(WebResource webResource, String key, Object value) { + return PipelineHelpers.addOptionalQueryParam(webResource, key, value); + } + + private WebResource addOptionalQueryParam(WebResource webResource, String key, int value, int defaultValue) { + return PipelineHelpers.addOptionalQueryParam(webResource, key, value, defaultValue); + } + + private Builder addOptionalMetadataHeader(Builder builder, Map metadata) { + return PipelineHelpers.addOptionalMetadataHeader(builder, metadata); + } + + private HashMap getMetadataFromHeaders(ClientResponse response) { + return PipelineHelpers.getMetadataFromHeaders(response); + } + + 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); + } +} 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/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/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; + } +} From cfb7c936128d18c13167038def909b6aae7f149a Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 7 Dec 2011 16:08:14 -0800 Subject: [PATCH 02/59] Integration tests --- .../blob/implementation/SharedKeyFilter.java | 35 ++++--- .../implementation/SharedKeyLiteFilter.java | 17 ++-- .../services/core/utils/pipeline/Exports.java | 23 +++-- .../services/table/TableContract.java | 6 ++ .../table/implementation/SharedKeyFilter.java | 63 +++++++++++++ .../TableExceptionProcessor.java | 28 ++++++ .../table/implementation/TableRestProxy.java | 22 +++++ .../table/models/QueryTablesOptions.java | 20 ++++ .../table/models/QueryTablesResult.java | 14 +++ ...windowsazure.services.core.Builder$Exports | 1 + .../services/table/IntegrationTestBase.java | 43 +++++++++ .../table/TableServiceIntegrationTest.java | 91 +++++++++++++++++++ .../com.microsoft.windowsazure.properties | 5 +- 13 files changed, 337 insertions(+), 31 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesOptions.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesResult.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/IntegrationTestBase.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java 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..9cbc9ae72bf2 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,7 +118,7 @@ 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()); @@ -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/utils/pipeline/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java index a562b7c2122e..f7aae2cd9188 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.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.core.utils.pipeline; @@ -22,11 +22,14 @@ import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; +import com.sun.jersey.api.client.filter.LoggingFilter; public class Exports implements Builder.Exports { + @Override public void register(Registry registry) { registry.add(new Builder.Factory() { + @Override public ClientConfig create(String profile, Builder builder, Map properties) { ClientConfig clientConfig = new DefaultClientConfig(); for (Entry entry : properties.entrySet()) { @@ -37,6 +40,7 @@ public ClientConfig create(String profile, Builder builder, Map }); registry.add(new Builder.Factory() { + @Override public Client create(String profile, Builder builder, Map properties) { ClientConfig clientConfig = (ClientConfig) properties.get("ClientConfig"); Client client = Client.create(clientConfig); @@ -45,10 +49,11 @@ public Client create(String profile, Builder builder, Map proper }); registry.add(new Builder.Factory() { + @Override public HttpURLConnectionClient create(String profile, Builder builder, Map properties) { ClientConfig clientConfig = (ClientConfig) properties.get("ClientConfig"); HttpURLConnectionClient client = HttpURLConnectionClient.create(clientConfig); - //client.addFilter(new LoggingFilter()); + client.addFilter(new LoggingFilter()); return client; } }); diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 9a849047abc0..6e759d01d433 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -17,6 +17,8 @@ import com.microsoft.windowsazure.services.core.FilterableService; import com.microsoft.windowsazure.services.core.ServiceException; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; +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; @@ -28,4 +30,8 @@ public interface TableContract extends FilterableService { void setServiceProperties(ServiceProperties serviceProperties) throws ServiceException; void setServiceProperties(ServiceProperties serviceProperties, TableServiceOptions options) throws ServiceException; + + QueryTablesResult queryTables() throws ServiceException; + + QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException; } 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 index 9cfbe21f43ff..ffa302d15640 100644 --- 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 @@ -14,13 +14,76 @@ */ 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)); + } + 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/TableExceptionProcessor.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableExceptionProcessor.java index 6aadc0650f3c..a771f2e57c70 100644 --- 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 @@ -24,6 +24,8 @@ import com.microsoft.windowsazure.services.core.utils.ServiceExceptionFactory; import com.microsoft.windowsazure.services.table.TableContract; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; +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.sun.jersey.api.client.ClientHandlerException; @@ -104,4 +106,30 @@ public void setServiceProperties(ServiceProperties serviceProperties, TableServi 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)); + } + } } 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 index 5e965269030a..9e1fdd24e27b 100644 --- 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 @@ -30,6 +30,8 @@ import com.microsoft.windowsazure.services.table.TableConfiguration; import com.microsoft.windowsazure.services.table.TableContract; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; +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.sun.jersey.api.client.ClientResponse; @@ -138,4 +140,24 @@ public void setServiceProperties(ServiceProperties serviceProperties, TableServi builder.put(serviceProperties); } + + @Override + public QueryTablesResult queryTables() throws ServiceException { + return queryTables(new QueryTablesOptions()); + } + + @Override + public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException { + WebResource webResource = getResource(options).path("Tables" + options.getQuery()); + + WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION); + + ClientResponse response = builder.get(ClientResponse.class); + ThrowIfError(response); + + QueryTablesResult result = new QueryTablesResult(); + result.setContinuationToken(response.getHeaders().getFirst("x-ms-continuation-NextTableName")); + + return result; + } } 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..e60e10bfe4ef --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesOptions.java @@ -0,0 +1,20 @@ +package com.microsoft.windowsazure.services.table.models; + +public class QueryTablesOptions extends TableServiceOptions { + private String query; + + @Override + public QueryTablesOptions setTimeout(Integer timeout) { + super.setTimeout(timeout); + return this; + } + + public String getQuery() { + return query; + } + + public QueryTablesOptions setQuery(String query) { + this.query = query; + 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..0215444e944b --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesResult.java @@ -0,0 +1,14 @@ +package com.microsoft.windowsazure.services.table.models; + +public class QueryTablesResult { + private String continuationToken; + + public String getContinuationToken() { + return continuationToken; + } + + public void setContinuationToken(String continuationToken) { + this.continuationToken = continuationToken; + } + +} 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/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..2c67bbce8c0e --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java @@ -0,0 +1,91 @@ +/** + * 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 org.junit.Test; + +import com.microsoft.windowsazure.services.core.Configuration; +import com.microsoft.windowsazure.services.table.models.QueryTablesResult; +import com.microsoft.windowsazure.services.table.models.ServiceProperties; + +public class TableServiceIntegrationTest extends IntegrationTestBase { + + @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 queryTablesWorks() throws Exception { + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + + // Act + QueryTablesResult result = service.queryTables(); + + // Assert + assertNotNull(result); + } +} 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 From bbcee3ed5b242de5dc372f6cbe373509c38c6ff9 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Mon, 9 Jan 2012 12:50:45 -0800 Subject: [PATCH 03/59] Cosmetic changes --- .../table/implementation/SharedKeyFilter.java | 1 + .../table/implementation/TableRestProxy.java | 23 ++++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) 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 index ffa302d15640..19a5cc737bc5 100644 --- 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 @@ -55,6 +55,7 @@ public void sign(ClientRequest 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); 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 index 9e1fdd24e27b..52b3eb6bbdac 100644 --- 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 @@ -15,8 +15,6 @@ package com.microsoft.windowsazure.services.table.implementation; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; import javax.inject.Inject; import javax.inject.Named; @@ -86,16 +84,8 @@ private WebResource addOptionalQueryParam(WebResource webResource, String key, O return PipelineHelpers.addOptionalQueryParam(webResource, key, value); } - private WebResource addOptionalQueryParam(WebResource webResource, String key, int value, int defaultValue) { - return PipelineHelpers.addOptionalQueryParam(webResource, key, value, defaultValue); - } - - private Builder addOptionalMetadataHeader(Builder builder, Map metadata) { - return PipelineHelpers.addOptionalMetadataHeader(builder, metadata); - } - - private HashMap getMetadataFromHeaders(ClientResponse response) { - return PipelineHelpers.getMetadataFromHeaders(response); + private Builder addOptionalHeader(Builder builder, String name, Object value) { + return PipelineHelpers.addOptionalHeader(builder, name, value); } private WebResource getResource(TableServiceOptions options) { @@ -148,7 +138,7 @@ public QueryTablesResult queryTables() throws ServiceException { @Override public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException { - WebResource webResource = getResource(options).path("Tables" + options.getQuery()); + WebResource webResource = getResource(options).path("Tables" + getTableQuery(options.getQuery())); WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION); @@ -160,4 +150,11 @@ public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceE return result; } + + private String getTableQuery(String query) { + if (query == null || query.length() == 0) + return ""; + + return "(" + query + ")"; + } } From 9432fac94bcf230779f2953ce323945337f0b6d9 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Tue, 10 Jan 2012 15:31:06 -0800 Subject: [PATCH 04/59] Implement table creation/deletion/querying --- .../windowsazure/services/blob/Exports.java | 18 +- .../blob/implementation/BlobRestProxy.java | 10 +- .../ContainerACLDateAdapter.java | 4 +- ...nverter.java => ISO8601DateConverter.java} | 2 +- .../blob/implementation/SharedKeyFilter.java | 2 +- .../core/utils/CommaStringBuilder.java | 47 ++++ .../HttpURLConnectionClientHandler.java | 4 +- .../core/utils/pipeline/PipelineHelpers.java | 23 -- .../windowsazure/services/table/Exports.java | 5 + .../services/table/TableContract.java | 18 ++ .../implementation/AtomReaderWriter.java | 143 ++++++++++++ .../DefaultXMLStreamFactory.java | 40 ++++ .../TableExceptionProcessor.java | 108 ++++++++- .../table/implementation/TableRestProxy.java | 213 ++++++++++++++++-- .../implementation/XMLStreamFactory.java | 13 ++ .../table/models/BinaryFilterExpression.java | 34 +++ .../models/ConstantFilterExpression.java | 14 ++ .../table/models/FilterExpression.java | 47 ++++ .../services/table/models/GetTableResult.java | 13 ++ .../table/models/ListTablesOptions.java | 20 ++ .../models/LitteralFilterExpression.java | 14 ++ .../services/table/models/QueryBuilder.java | 96 ++++++++ .../table/models/QueryTablesOptions.java | 6 +- .../table/models/QueryTablesResult.java | 10 + .../services/table/models/TableEntry.java | 13 ++ .../table/models/UnaryFilterExpression.java | 24 ++ .../services/table/AtomReaderWriterTests.java | 81 +++++++ .../table/TableServiceIntegrationTest.java | 173 ++++++++++++++ 28 files changed, 1135 insertions(+), 60 deletions(-) rename microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/{ContainerACLDateConverter.java => ISO8601DateConverter.java} (97%) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/CommaStringBuilder.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultXMLStreamFactory.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/XMLStreamFactory.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilterExpression.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilterExpression.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetTableResult.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ListTablesOptions.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilterExpression.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryBuilder.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/TableEntry.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilterExpression.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java 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/ISO8601DateConverter.java similarity index 97% rename from microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ContainerACLDateConverter.java rename to microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverter.java index 209a345d26a0..a5f02e025637 100644 --- 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/ISO8601DateConverter.java @@ -24,7 +24,7 @@ /* * "not quite" ISO 8601 date time conversion routines */ -public class ContainerACLDateConverter { +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'"; 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 9cbc9ae72bf2..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 @@ -122,7 +122,7 @@ 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); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/CommaStringBuilder.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/CommaStringBuilder.java new file mode 100644 index 000000000000..3623028e5ef1 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/CommaStringBuilder.java @@ -0,0 +1,47 @@ +package com.microsoft.windowsazure.services.core.utils; + +import java.util.List; + +public class CommaStringBuilder { + private final StringBuilder sb = new StringBuilder(); + + public void add(String representation) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(representation); + } + + public void addValue(boolean value, String representation) { + if (value) { + add(representation); + } + } + + public static String join(List values) { + CommaStringBuilder sb = new CommaStringBuilder(); + + for (String value : values) { + sb.add(value); + } + + return sb.toString(); + } + + public static String join(String... values) { + CommaStringBuilder sb = new CommaStringBuilder(); + + for (String value : values) { + sb.add(value); + } + + return sb.toString(); + } + + @Override + public String toString() { + if (sb.length() == 0) + return null; + return sb.toString(); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/HttpURLConnectionClientHandler.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/HttpURLConnectionClientHandler.java index 0610eb8d7bc9..20d9046c5358 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/HttpURLConnectionClientHandler.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/HttpURLConnectionClientHandler.java @@ -27,7 +27,7 @@ import javax.ws.rs.core.MultivaluedMap; -import com.microsoft.windowsazure.services.core.utils.pipeline.PipelineHelpers.EnumCommaStringBuilder; +import com.microsoft.windowsazure.services.core.utils.CommaStringBuilder; import com.sun.jersey.api.client.ClientHandlerException; import com.sun.jersey.api.client.ClientRequest; import com.sun.jersey.api.client.ClientResponse; @@ -269,7 +269,7 @@ private void setURLConnectionHeaders(MultivaluedMap headers, Htt urlConnection.setRequestProperty(e.getKey(), ClientRequest.getHeaderValue(vs.get(0))); } else { - EnumCommaStringBuilder sb = new EnumCommaStringBuilder(); + CommaStringBuilder sb = new CommaStringBuilder(); for (Object v : e.getValue()) { sb.add(ClientRequest.getHeaderValue(v)); } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java index 95fce9e9dbeb..9cebe855ee2a 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java @@ -33,29 +33,6 @@ public static void ThrowIfError(ClientResponse r) { } } - public static class EnumCommaStringBuilder { - private final StringBuilder sb = new StringBuilder(); - - public void add(String representation) { - if (sb.length() > 0) { - sb.append(","); - } - sb.append(representation); - } - - public void addValue(boolean value, String representation) { - if (value) { - add(representation); - } - } - - public String getValue() { - if (sb.length() == 0) - return null; - return sb.toString(); - } - } - public static WebResource addOptionalQueryParam(WebResource webResource, String key, Object value) { if (value != null) { webResource = webResource.queryParam(key, value.toString()); diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java index 484cd066f2a0..c36841928b06 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java @@ -15,10 +15,13 @@ package com.microsoft.windowsazure.services.table; import com.microsoft.windowsazure.services.core.Builder; +import com.microsoft.windowsazure.services.table.implementation.AtomReaderWriter; +import com.microsoft.windowsazure.services.table.implementation.DefaultXMLStreamFactory; import com.microsoft.windowsazure.services.table.implementation.SharedKeyFilter; import com.microsoft.windowsazure.services.table.implementation.SharedKeyLiteFilter; import com.microsoft.windowsazure.services.table.implementation.TableExceptionProcessor; import com.microsoft.windowsazure.services.table.implementation.TableRestProxy; +import com.microsoft.windowsazure.services.table.implementation.XMLStreamFactory; public class Exports implements Builder.Exports { @Override @@ -28,5 +31,7 @@ public void register(Builder.Registry registry) { registry.add(TableRestProxy.class); registry.add(SharedKeyLiteFilter.class); registry.add(SharedKeyFilter.class); + registry.add(XMLStreamFactory.class, DefaultXMLStreamFactory.class); + registry.add(AtomReaderWriter.class); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 6e759d01d433..4d9776bde42c 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -17,6 +17,8 @@ import com.microsoft.windowsazure.services.core.FilterableService; import com.microsoft.windowsazure.services.core.ServiceException; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; +import com.microsoft.windowsazure.services.table.models.GetTableResult; +import com.microsoft.windowsazure.services.table.models.ListTablesOptions; import com.microsoft.windowsazure.services.table.models.QueryTablesOptions; import com.microsoft.windowsazure.services.table.models.QueryTablesResult; import com.microsoft.windowsazure.services.table.models.ServiceProperties; @@ -31,6 +33,22 @@ public interface TableContract extends FilterableService { void setServiceProperties(ServiceProperties serviceProperties, TableServiceOptions options) throws ServiceException; + void createTable(String table) throws ServiceException; + + void createTable(String table, TableServiceOptions options) throws ServiceException; + + void deleteTable(String table) throws ServiceException; + + void deleteTable(String table, TableServiceOptions options) throws ServiceException; + + GetTableResult getTable(String table) throws ServiceException; + + GetTableResult getTable(String table, TableServiceOptions options) throws ServiceException; + + QueryTablesResult listTables() throws ServiceException; + + QueryTablesResult listTables(ListTablesOptions options) throws ServiceException; + QueryTablesResult queryTables() throws ServiceException; QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException; 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..bd8439e5ee43 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriter.java @@ -0,0 +1,143 @@ +package com.microsoft.windowsazure.services.table.implementation; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +import javax.inject.Inject; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter; +import com.microsoft.windowsazure.services.core.utils.DateFactory; +import com.microsoft.windowsazure.services.table.models.TableEntry; + +public class AtomReaderWriter { + private final XMLStreamFactory xmlStreamFactory; + private final DateFactory dateFactory; + private final ISO8601DateConverter iso8601DateConverter; + + @Inject + public AtomReaderWriter(XMLStreamFactory xmlStreamFactory, DateFactory dateFactory, + ISO8601DateConverter iso8601DateConverter) { + this.xmlStreamFactory = xmlStreamFactory; + this.dateFactory = dateFactory; + this.iso8601DateConverter = iso8601DateConverter; + } + + public InputStream getTableNameEntry(String table) { + String entity = String.format("" + + " " + " " + "<updated>%s</updated>" + "<author>" + + " <name/> " + "</author> " + " <id/> " + " <content type=\"application/xml\">" + + " <m:properties>" + " <d:TableName>%s</d:TableName>" + " </m:properties>" + + " </content> " + "</entry>", iso8601DateConverter.format(dateFactory.getDate()), table); + + try { + return new ByteArrayInputStream(entity.getBytes("UTF-8")); + } + catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public List<TableEntry> parseTableEntries(InputStream stream) { + try { + XMLStreamReader xmlr = xmlStreamFactory.getReader(stream); + + expect(xmlr, XMLStreamConstants.START_DOCUMENT); + expect(xmlr, XMLStreamConstants.START_ELEMENT, "feed"); + + List<TableEntry> result = new ArrayList<TableEntry>(); + 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); + } + } + + private TableEntry parseTableEntry(XMLStreamReader xmlr) throws XMLStreamException { + TableEntry result = new TableEntry(); + + expect(xmlr, XMLStreamConstants.START_ELEMENT, "entry"); + + while (!isEndElement(xmlr, "entry")) { + + if (isStartElement(xmlr, "TableName")) { + xmlr.next(); + result.setName(xmlr.getText()); + + nextSignificant(xmlr); + expect(xmlr, XMLStreamConstants.END_ELEMENT, "TableName"); + } + else { + nextSignificant(xmlr); + } + } + + expect(xmlr, XMLStreamConstants.END_ELEMENT, "entry"); + + 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/DefaultXMLStreamFactory.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultXMLStreamFactory.java new file mode 100644 index 000000000000..9c44869f23f9 --- /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); + } + 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/TableExceptionProcessor.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableExceptionProcessor.java index a771f2e57c70..48e2f4e675da 100644 --- 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 @@ -24,6 +24,8 @@ import com.microsoft.windowsazure.services.core.utils.ServiceExceptionFactory; import com.microsoft.windowsazure.services.table.TableContract; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; +import com.microsoft.windowsazure.services.table.models.GetTableResult; +import com.microsoft.windowsazure.services.table.models.ListTablesOptions; import com.microsoft.windowsazure.services.table.models.QueryTablesOptions; import com.microsoft.windowsazure.services.table.models.QueryTablesResult; import com.microsoft.windowsazure.services.table.models.ServiceProperties; @@ -51,7 +53,7 @@ public TableContract withFilter(ServiceFilter filter) { private ServiceException processCatch(ServiceException e) { log.warn(e.getMessage(), e.getCause()); - return ServiceExceptionFactory.process("blob", e); + return ServiceExceptionFactory.process("table", e); } @Override @@ -107,6 +109,84 @@ public void setServiceProperties(ServiceProperties serviceProperties, TableServi } } + @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 { @@ -132,4 +212,30 @@ public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceE 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)); + } + } } 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 index 52b3eb6bbdac..17326178a952 100644 --- 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 @@ -14,24 +14,37 @@ */ package com.microsoft.windowsazure.services.table.implementation; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import javax.inject.Inject; import javax.inject.Named; +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.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.models.BinaryFilterExpression; +import com.microsoft.windowsazure.services.table.models.ConstantFilterExpression; +import com.microsoft.windowsazure.services.table.models.FilterExpression; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; +import com.microsoft.windowsazure.services.table.models.GetTableResult; +import com.microsoft.windowsazure.services.table.models.ListTablesOptions; +import com.microsoft.windowsazure.services.table.models.LitteralFilterExpression; +import com.microsoft.windowsazure.services.table.models.QueryBuilder; 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.UnaryFilterExpression; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource.Builder; @@ -39,55 +52,155 @@ public class TableRestProxy implements TableContract { private static final String API_VERSION = "2011-08-18"; private final HttpURLConnectionClient channel; - private final String accountName; 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; @Inject - public TableRestProxy(HttpURLConnectionClient channel, @Named(TableConfiguration.ACCOUNT_NAME) String accountName, - @Named(TableConfiguration.URI) String url, SharedKeyFilter filter) { + public TableRestProxy(HttpURLConnectionClient channel, @Named(TableConfiguration.URI) String url, + SharedKeyFilter filter, DateFactory dateFactory, ISO8601DateConverter iso8601DateConverter, + AtomReaderWriter atomReaderWriter) { this.channel = channel; - this.accountName = accountName; this.url = url; this.filter = filter; this.dateMapper = new RFC1123DateConverter(); + this.iso8601DateConverter = iso8601DateConverter; this.filters = new ServiceFilter[0]; + this.dateFactory = dateFactory; + this.atomReaderWriter = atomReaderWriter; channel.addFilter(filter); } - public TableRestProxy(HttpURLConnectionClient channel, ServiceFilter[] filters, String accountName, String url, - SharedKeyFilter filter, RFC1123DateConverter dateMapper) { + public TableRestProxy(HttpURLConnectionClient channel, ServiceFilter[] filters, String url, SharedKeyFilter filter, + DateFactory dateFactory, AtomReaderWriter atomReaderWriter, RFC1123DateConverter dateMapper, + ISO8601DateConverter iso8601DateConverter) { this.channel = channel; this.filters = filters; - this.accountName = accountName; this.url = url; this.filter = filter; + this.dateFactory = dateFactory; + this.atomReaderWriter = atomReaderWriter; 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.accountName, this.url, this.filter, this.dateMapper); + return new TableRestProxy(this.channel, newFilters, this.url, this.filter, this.dateFactory, + this.atomReaderWriter, 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<String> encodeODataURIValues(List<String> values) { + List<String> list = new ArrayList<String>(); + for (String value : values) { + list.add(encodeODataURIValue(value)); + } + return list; + } + private WebResource addOptionalQueryParam(WebResource webResource, String key, Object value) { return PipelineHelpers.addOptionalQueryParam(webResource, key, value); } + private WebResource addOptionalQuery(WebResource webResource, QueryBuilder query) { + if (query == null) + return webResource; + + if (query.getFields() != null && query.getFields().size() > 0) { + webResource = addOptionalQueryParam(webResource, "$select", + CommaStringBuilder.join(encodeODataURIValues(query.getFields()))); + } + + 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.getOrderBy() != null) { + webResource = addOptionalQueryParam(webResource, "$orderby", + CommaStringBuilder.join(encodeODataURIValues(query.getOrderBy()))); + } + + if (query.getNextPartitionKey() != null) { + webResource = addOptionalQueryParam(webResource, "NextPartitionKey", + encodeODataURIValue(query.getNextPartitionKey())); + } + + if (query.getNextRowKey() != null) { + webResource = addOptionalQueryParam(webResource, "NextRowKey", encodeODataURIValue(query.getNextRowKey())); + } + + return webResource; + } + + private String buildFilterExpression(FilterExpression filter) { + StringBuilder sb = new StringBuilder(); + buildFilterExpression(filter, sb); + return sb.toString(); + } + + private void buildFilterExpression(FilterExpression filter, StringBuilder sb) { + if (filter == null) + return; + + if (filter instanceof LitteralFilterExpression) { + sb.append(((LitteralFilterExpression) filter).getLitteral()); + } + else if (filter instanceof ConstantFilterExpression) { + sb.append("'"); + sb.append(((ConstantFilterExpression) filter).getValue()); + sb.append("'"); + } + else if (filter instanceof UnaryFilterExpression) { + sb.append(((UnaryFilterExpression) filter).getOperator()); + sb.append("("); + buildFilterExpression(((UnaryFilterExpression) filter).getOperand(), sb); + sb.append(")"); + } + else if (filter instanceof BinaryFilterExpression) { + sb.append("("); + buildFilterExpression(((BinaryFilterExpression) filter).getLeft(), sb); + sb.append(" "); + sb.append(((BinaryFilterExpression) filter).getOperator()); + sb.append(" "); + buildFilterExpression(((BinaryFilterExpression) filter).getRight(), sb); + sb.append(")"); + } + } + 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 getResource(TableServiceOptions options) { WebResource webResource = channel.resource(url).path("/"); webResource = addOptionalQueryParam(webResource, "timeout", options.getTimeout()); @@ -131,6 +244,46 @@ public void setServiceProperties(ServiceProperties serviceProperties, TableServi 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.setTable(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 + '{' + FilterExpression filter = FilterExpression.and( + FilterExpression.ge(FilterExpression.litteral("TableName"), + FilterExpression.constant(options.getPrefix())), + FilterExpression.le(FilterExpression.litteral("TableName"), + FilterExpression.constant(options.getPrefix() + "{"))); + + QueryTablesOptions queryTableOptions = new QueryTablesOptions(); + queryTableOptions.setTimeout(options.getTimeout()); + queryTableOptions.setQuery(new QueryBuilder().setFilter(filter)); + return queryTables(queryTableOptions); + } + @Override public QueryTablesResult queryTables() throws ServiceException { return queryTables(new QueryTablesOptions()); @@ -138,23 +291,55 @@ public QueryTablesResult queryTables() throws ServiceException { @Override public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException { - WebResource webResource = getResource(options).path("Tables" + getTableQuery(options.getQuery())); + WebResource webResource = getResource(options).path("Tables"); + webResource = addOptionalQuery(webResource, options.getQuery()); - WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION); + WebResource.Builder builder = webResource.getRequestBuilder(); + builder = addTableRequestHeaders(builder); ClientResponse response = builder.get(ClientResponse.class); ThrowIfError(response); QueryTablesResult result = new QueryTablesResult(); result.setContinuationToken(response.getHeaders().getFirst("x-ms-continuation-NextTableName")); + result.setTables(atomReaderWriter.parseTableEntries(response.getEntityInputStream())); return result; } - private String getTableQuery(String query) { - if (query == null || query.length() == 0) - return ""; + @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"); - return "(" + query + ")"; + WebResource.Builder builder = webResource.getRequestBuilder(); + builder = addTableRequestHeaders(builder); + + builder.entity(atomReaderWriter.getTableNameEntry(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); } } 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/BinaryFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilterExpression.java new file mode 100644 index 000000000000..25547090206a --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilterExpression.java @@ -0,0 +1,34 @@ +package com.microsoft.windowsazure.services.table.models; + +public class BinaryFilterExpression extends FilterExpression { + private String operator; + private FilterExpression left; + private FilterExpression right; + + public String getOperator() { + return operator; + } + + public BinaryFilterExpression setOperator(String operator) { + this.operator = operator; + return this; + } + + public FilterExpression getLeft() { + return left; + } + + public BinaryFilterExpression setLeft(FilterExpression left) { + this.left = left; + return this; + } + + public FilterExpression getRight() { + return right; + } + + public BinaryFilterExpression setRight(FilterExpression right) { + this.right = right; + return this; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilterExpression.java new file mode 100644 index 000000000000..4c38168ca295 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilterExpression.java @@ -0,0 +1,14 @@ +package com.microsoft.windowsazure.services.table.models; + +public class ConstantFilterExpression extends FilterExpression { + private Object value; + + public Object getValue() { + return value; + } + + public ConstantFilterExpression setValue(Object value) { + this.value = value; + return this; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java new file mode 100644 index 000000000000..655a53838e75 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java @@ -0,0 +1,47 @@ +package com.microsoft.windowsazure.services.table.models; + +public class FilterExpression { + public static UnaryFilterExpression not(FilterExpression operand) { + return new UnaryFilterExpression().setOperator("not").setOperand(operand); + } + + public static BinaryFilterExpression and(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("and").setLeft(left).setRight(right); + } + + public static BinaryFilterExpression or(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("or").setLeft(left).setRight(right); + } + + public static BinaryFilterExpression eq(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("eq").setLeft(left).setRight(right); + } + + public static BinaryFilterExpression ne(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("ne").setLeft(left).setRight(right); + } + + public static BinaryFilterExpression ge(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("ge").setLeft(left).setRight(right); + } + + public static BinaryFilterExpression gt(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("gt").setLeft(left).setRight(right); + } + + public static BinaryFilterExpression lt(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("lt").setLeft(left).setRight(right); + } + + public static BinaryFilterExpression le(FilterExpression left, FilterExpression right) { + return new BinaryFilterExpression().setOperator("le").setLeft(left).setRight(right); + } + + public static ConstantFilterExpression constant(Object value) { + return new ConstantFilterExpression().setValue(value); + } + + public static LitteralFilterExpression litteral(String value) { + return new LitteralFilterExpression().setLitteral(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..2978c465cebd --- /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 table; + + public TableEntry getTable() { + return table; + } + + public void setTable(TableEntry table) { + this.table = table; + } +} 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/LitteralFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilterExpression.java new file mode 100644 index 000000000000..8c4d857624e3 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilterExpression.java @@ -0,0 +1,14 @@ +package com.microsoft.windowsazure.services.table.models; + +public class LitteralFilterExpression extends FilterExpression { + private String litteral; + + public String getLitteral() { + return litteral; + } + + public LitteralFilterExpression setLitteral(String litteral) { + this.litteral = litteral; + return this; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryBuilder.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryBuilder.java new file mode 100644 index 000000000000..7c915cf53cb4 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryBuilder.java @@ -0,0 +1,96 @@ +package com.microsoft.windowsazure.services.table.models; + +import java.util.List; + +public class QueryBuilder { + private List<String> fields; + private String from; + private FilterExpression filter; + private List<String> orderBy; + private Integer top; + private String partitionKey; + private String nextPartitionKey; + private String rowKey; + private String nextRowKey; + + public List<String> getFields() { + return fields; + } + + public QueryBuilder setFields(List<String> fields) { + this.fields = fields; + return this; + } + + public String getFrom() { + return from; + } + + public QueryBuilder setFrom(String from) { + this.from = from; + return this; + } + + public FilterExpression getFilter() { + return filter; + } + + public QueryBuilder setFilter(FilterExpression where) { + this.filter = where; + return this; + } + + public List<String> getOrderBy() { + return orderBy; + } + + public QueryBuilder setOrderBy(List<String> orderBy) { + this.orderBy = orderBy; + return this; + } + + public Integer getTop() { + return top; + } + + public QueryBuilder setTop(Integer top) { + this.top = top; + return this; + } + + public String getPartitionKey() { + return partitionKey; + } + + public QueryBuilder setPartitionKey(String partitionKey) { + this.partitionKey = partitionKey; + return this; + } + + public String getNextPartitionKey() { + return nextPartitionKey; + } + + public QueryBuilder setNextPartitionKey(String nextPartitionKey) { + this.nextPartitionKey = nextPartitionKey; + return this; + } + + public String getRowKey() { + return rowKey; + } + + public QueryBuilder setRowKey(String rowKey) { + this.rowKey = rowKey; + return this; + } + + public String getNextRowKey() { + return nextRowKey; + } + + public QueryBuilder setNextRowKey(String nextRowKey) { + this.nextRowKey = nextRowKey; + return this; + } +} 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 index e60e10bfe4ef..d4896fa67498 100644 --- 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 @@ -1,7 +1,7 @@ package com.microsoft.windowsazure.services.table.models; public class QueryTablesOptions extends TableServiceOptions { - private String query; + private QueryBuilder query; @Override public QueryTablesOptions setTimeout(Integer timeout) { @@ -9,11 +9,11 @@ public QueryTablesOptions setTimeout(Integer timeout) { return this; } - public String getQuery() { + public QueryBuilder getQuery() { return query; } - public QueryTablesOptions setQuery(String query) { + public QueryTablesOptions setQuery(QueryBuilder query) { this.query = query; 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 index 0215444e944b..3816ffe5899a 100644 --- 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 @@ -1,7 +1,10 @@ package com.microsoft.windowsazure.services.table.models; +import java.util.List; + public class QueryTablesResult { private String continuationToken; + private List<TableEntry> tables; public String getContinuationToken() { return continuationToken; @@ -11,4 +14,11 @@ public void setContinuationToken(String continuationToken) { this.continuationToken = continuationToken; } + public List<TableEntry> getTables() { + return tables; + } + + public void setTables(List<TableEntry> tables) { + this.tables = tables; + } } 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/UnaryFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilterExpression.java new file mode 100644 index 000000000000..3e0efd984bff --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilterExpression.java @@ -0,0 +1,24 @@ +package com.microsoft.windowsazure.services.table.models; + +public class UnaryFilterExpression extends FilterExpression { + private String operator; + private FilterExpression operand; + + public String getOperator() { + return operator; + } + + public UnaryFilterExpression setOperator(String operator) { + this.operator = operator; + return this; + } + + public FilterExpression getOperand() { + return operand; + } + + public UnaryFilterExpression setOperand(FilterExpression operand) { + this.operand = operand; + return this; + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java new file mode 100644 index 000000000000..3cf03c788639 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java @@ -0,0 +1,81 @@ +/** + * 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.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.implementation.AtomReaderWriter; +import com.microsoft.windowsazure.services.table.implementation.DefaultXMLStreamFactory; +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()); + String feed = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\r\n" + + "<feed xml:base=\"http://rpaquaytest.table.core.windows.net/\" xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns=\"http://www.w3.org/2005/Atom\">\r\n" + + " <title type=\"text\">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/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java index 2c67bbce8c0e..434b10be1be3 100644 --- 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 @@ -16,13 +16,98 @@ import static org.junit.Assert.*; +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.table.models.GetTableResult; +import com.microsoft.windowsazure.services.table.models.ListTablesOptions; 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 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 { + // 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]; + + 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); + + 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 { + 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 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 { @@ -76,6 +161,54 @@ public void setServicePropertiesWorks() throws Exception { 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 @@ -88,4 +221,44 @@ public void queryTablesWorks() throws Exception { // 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 + System.out.println("getTable() test"); + GetTableResult result = service.getTable(TEST_TABLE_1); + + // Assert + assertNotNull(result); + } } From f481f385a4b65bafede2accfa246283fc037ca07 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 12:56:16 -0800 Subject: [PATCH 05/59] Add support for "insertEntity" --- .../implementation/ISO8601DateConverter.java | 25 +-- .../services/table/EdmValueConverter.java | 7 + .../windowsazure/services/table/Exports.java | 2 + .../services/table/TableContract.java | 6 + .../implementation/AtomReaderWriter.java | 157 +++++++++++++++--- .../DefaultEdmValueConterter.java | 65 ++++++++ .../DefaultXMLStreamFactory.java | 2 +- .../table/implementation/SharedKeyFilter.java | 2 +- .../TableExceptionProcessor.java | 29 ++++ .../table/implementation/TableRestProxy.java | 30 +++- .../services/table/models/EdmType.java | 12 ++ .../services/table/models/Entity.java | 67 ++++++++ .../services/table/models/GetTableResult.java | 10 +- .../table/models/InsertEntityResult.java | 13 ++ .../services/table/models/Property.java | 24 +++ .../services/table/AtomReaderWriterTests.java | 3 +- .../table/TableServiceIntegrationTest.java | 51 +++++- 17 files changed, 450 insertions(+), 55 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/DefaultEdmValueConterter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/EdmType.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Entity.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/InsertEntityResult.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Property.java 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 index a5f02e025637..08d7d6ea9ce8 100644 --- 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 @@ -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; @@ -36,15 +36,6 @@ 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")); diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java new file mode 100644 index 000000000000..e7582255ef59 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java @@ -0,0 +1,7 @@ +package com.microsoft.windowsazure.services.table; + +public interface EdmValueConverter { + String serialize(String edmType, Object value); + + Object deserialize(String edmType, String value); +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java index c36841928b06..21ad0784df4b 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java @@ -16,6 +16,7 @@ import com.microsoft.windowsazure.services.core.Builder; import com.microsoft.windowsazure.services.table.implementation.AtomReaderWriter; +import com.microsoft.windowsazure.services.table.implementation.DefaultEdmValueConterter; import com.microsoft.windowsazure.services.table.implementation.DefaultXMLStreamFactory; import com.microsoft.windowsazure.services.table.implementation.SharedKeyFilter; import com.microsoft.windowsazure.services.table.implementation.SharedKeyLiteFilter; @@ -33,5 +34,6 @@ public void register(Builder.Registry registry) { registry.add(SharedKeyFilter.class); registry.add(XMLStreamFactory.class, DefaultXMLStreamFactory.class); registry.add(AtomReaderWriter.class); + registry.add(EdmValueConverter.class, DefaultEdmValueConterter.class); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 4d9776bde42c..8fc4caefeadc 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -16,8 +16,10 @@ import com.microsoft.windowsazure.services.core.FilterableService; import com.microsoft.windowsazure.services.core.ServiceException; +import com.microsoft.windowsazure.services.table.models.Entity; 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.QueryTablesOptions; import com.microsoft.windowsazure.services.table.models.QueryTablesResult; @@ -52,4 +54,8 @@ public interface TableContract extends FilterableService { QueryTablesResult queryTables() throws ServiceException; QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException; + + InsertEntityResult insertEntity(String table, Entity entity) throws ServiceException; + + InsertEntityResult insertEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; } 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 index bd8439e5ee43..c1943ebdcd1e 100644 --- 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 @@ -1,48 +1,77 @@ package com.microsoft.windowsazure.services.table.implementation; import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.InputStream; -import java.io.UnsupportedEncodingException; 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) { + ISO8601DateConverter iso8601DateConverter, EdmValueConverter edmValueConverter) { this.xmlStreamFactory = xmlStreamFactory; this.dateFactory = dateFactory; this.iso8601DateConverter = iso8601DateConverter; + this.edmValueConverter = edmValueConverter; } - public InputStream getTableNameEntry(String table) { - String entity = String.format("" - + " " + " " + "<updated>%s</updated>" + "<author>" - + " <name/> " + "</author> " + " <id/> " + " <content type=\"application/xml\">" - + " <m:properties>" + " <d:TableName>%s</d:TableName>" + " </m:properties>" - + " </content> " + "</entry>", iso8601DateConverter.format(dateFactory.getDate()), table); + 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 + } + }); + } - try { - return new ByteArrayInputStream(entity.getBytes("UTF-8")); - } - catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } + public InputStream generateEntityEntry(Entity entity) { + final Entity entityTemp = entity; + return generateEntry(new PropertiesWriter() { + @Override + public void write(XMLStreamWriter writer) throws XMLStreamException { + for (Entry<String, Property> 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<TableEntry> parseTableEntries(InputStream stream) { @@ -88,19 +117,105 @@ public TableEntry parseTableEntry(InputStream stream) { } } + public Entity parseEntityEntry(InputStream stream) { + try { + XMLStreamReader xmlr = xmlStreamFactory.getReader(stream); + + expect(xmlr, XMLStreamConstants.START_DOCUMENT); + Map<String, Property> properties = parseEntryProperties(xmlr); + expect(xmlr, XMLStreamConstants.END_DOCUMENT); + + return new Entity().setProperties(properties); + } + 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 { + Map<String, Property> properties = parseEntryProperties(xmlr); + TableEntry result = new TableEntry(); + result.setName((String) properties.get("TableName").getValue()); + return result; + } + + private Map<String, Property> parseEntryProperties(XMLStreamReader xmlr) throws XMLStreamException { + Map<String, Property> result = new HashMap<String, Property>(); expect(xmlr, XMLStreamConstants.START_ELEMENT, "entry"); while (!isEndElement(xmlr, "entry")) { - if (isStartElement(xmlr, "TableName")) { - xmlr.next(); - result.setName(xmlr.getText()); - + if (isStartElement(xmlr, "properties")) { nextSignificant(xmlr); - expect(xmlr, XMLStreamConstants.END_ELEMENT, "TableName"); + + 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"); } else { 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 index 9c44869f23f9..8504c88fa349 100644 --- 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 @@ -21,7 +21,7 @@ public DefaultXMLStreamFactory() { @Override public XMLStreamWriter getWriter(OutputStream stream) { try { - return xmlOutputFactory.createXMLStreamWriter(stream); + return xmlOutputFactory.createXMLStreamWriter(stream, "UTF-8"); } catch (XMLStreamException e) { throw new RuntimeException(e); 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 index 19a5cc737bc5..79305ce758c8 100644 --- 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 @@ -56,7 +56,7 @@ public void sign(ClientRequest cr) { 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)); + //System.out.println(String.format("String to sign: \"%s\"", stringToSign)); String signature = this.getSigner().sign(stringToSign); cr.getHeaders().putSingle("Authorization", "SharedKey " + this.getAccountName() + ":" + signature); 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 index 48e2f4e675da..64215de5c604 100644 --- 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 @@ -23,8 +23,10 @@ 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.Entity; 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.QueryTablesOptions; import com.microsoft.windowsazure.services.table.models.QueryTablesResult; @@ -238,4 +240,31 @@ public QueryTablesResult listTables(ListTablesOptions options) throws ServiceExc 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)); + } + } } 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 index 17326178a952..4a2dbc92c29f 100644 --- 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 @@ -34,9 +34,11 @@ import com.microsoft.windowsazure.services.table.TableContract; import com.microsoft.windowsazure.services.table.models.BinaryFilterExpression; import com.microsoft.windowsazure.services.table.models.ConstantFilterExpression; +import com.microsoft.windowsazure.services.table.models.Entity; import com.microsoft.windowsazure.services.table.models.FilterExpression; 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.LitteralFilterExpression; import com.microsoft.windowsazure.services.table.models.QueryBuilder; @@ -260,7 +262,7 @@ public GetTableResult getTable(String table, TableServiceOptions options) throws ThrowIfError(response); GetTableResult result = new GetTableResult(); - result.setTable(atomReaderWriter.parseTableEntry(response.getEntityInputStream())); + result.setTableEntry(atomReaderWriter.parseTableEntry(response.getEntityInputStream())); return result; } @@ -320,7 +322,7 @@ public void createTable(String table, TableServiceOptions options) throws Servic WebResource.Builder builder = webResource.getRequestBuilder(); builder = addTableRequestHeaders(builder); - builder.entity(atomReaderWriter.getTableNameEntry(table), "application/atom+xml"); + builder.entity(atomReaderWriter.generateTableEntry(table), "application/atom+xml"); ClientResponse response = builder.post(ClientResponse.class); ThrowIfError(response); @@ -342,4 +344,28 @@ public void deleteTable(String table, TableServiceOptions options) throws Servic 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; + } } 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..de13eee4740f --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Entity.java @@ -0,0 +1,67 @@ +package com.microsoft.windowsazure.services.table.models; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +public class Entity { + private Map<String, Property> properties = new HashMap<String, Property>(); + + 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<String, Property> getProperties() { + return properties; + } + + public Entity setProperties(Map<String, Property> 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/GetTableResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetTableResult.java index 2978c465cebd..de199b24f075 100644 --- 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 @@ -1,13 +1,13 @@ package com.microsoft.windowsazure.services.table.models; public class GetTableResult { - private TableEntry table; + private TableEntry tableEntry; - public TableEntry getTable() { - return table; + public TableEntry getTableEntry() { + return tableEntry; } - public void setTable(TableEntry table) { - this.table = table; + 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/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/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java index 3cf03c788639..9aebd223e75b 100644 --- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java @@ -25,6 +25,7 @@ import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter; import com.microsoft.windowsazure.services.core.utils.DefaultDateFactory; import com.microsoft.windowsazure.services.table.implementation.AtomReaderWriter; +import com.microsoft.windowsazure.services.table.implementation.DefaultEdmValueConterter; import com.microsoft.windowsazure.services.table.implementation.DefaultXMLStreamFactory; import com.microsoft.windowsazure.services.table.models.TableEntry; @@ -33,7 +34,7 @@ public class AtomReaderWriterTests extends IntegrationTestBase { public void parseTableEntriesWorks() throws Exception { // Arrange AtomReaderWriter atom = new AtomReaderWriter(new DefaultXMLStreamFactory(), new DefaultDateFactory(), - new ISO8601DateConverter()); + new ISO8601DateConverter(), new DefaultEdmValueConterter(new ISO8601DateConverter())); String feed = "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\r\n" + "<feed xml:base=\"http://rpaquaytest.table.core.windows.net/\" xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns=\"http://www.w3.org/2005/Atom\">\r\n" + " <title type=\"text\">Tables\r\n" 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 index 434b10be1be3..3ecfde9f2b5a 100644 --- 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 @@ -16,6 +16,7 @@ import static org.junit.Assert.*; +import java.util.Date; import java.util.HashSet; import java.util.Set; @@ -24,7 +25,10 @@ import org.junit.Test; import com.microsoft.windowsazure.services.core.Configuration; +import com.microsoft.windowsazure.services.table.models.EdmType; +import com.microsoft.windowsazure.services.table.models.Entity; 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.QueryTablesResult; import com.microsoft.windowsazure.services.table.models.ServiceProperties; @@ -35,11 +39,11 @@ public class TableServiceIntegrationTest extends IntegrationTestBase { 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_3; + //private static String TEST_TABLE_4; private static String CREATABLE_TABLE_1; private static String CREATABLE_TABLE_2; - private static String CREATABLE_TABLE_3; + //private static String CREATABLE_TABLE_3; private static String[] creatableTables; private static String[] testTables; @@ -59,12 +63,12 @@ public static void setup() throws Exception { TEST_TABLE_1 = testTables[0]; TEST_TABLE_2 = testTables[1]; - TEST_TABLE_3 = testTables[2]; - TEST_TABLE_4 = testTables[3]; + //TEST_TABLE_3 = testTables[2]; + //TEST_TABLE_4 = testTables[3]; CREATABLE_TABLE_1 = creatableTables[0]; CREATABLE_TABLE_2 = creatableTables[1]; - CREATABLE_TABLE_3 = creatableTables[2]; + //CREATABLE_TABLE_3 = creatableTables[2]; // Create all test containers and their content Configuration config = createConfiguration(); @@ -255,10 +259,43 @@ public void getTableWorks() throws Exception { TableContract service = TableService.create(config); // Act - System.out.println("getTable() test"); 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); + + // Act + InsertEntityResult result = service.insertEntity( + TEST_TABLE_2, + new Entity().setPartitionKey("001").setRowKey("002").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())); + + // Assert + assertNotNull(result); + assertNotNull(result.getEntity()); + assertEquals("001", result.getEntity().getPartitionKey()); + assertEquals("002", result.getEntity().getRowKey()); + assertNotNull(result.getEntity().getTimestamp()); + 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); + } } From 28a3cecdd169da24a88f2d4738ed0f38ae86cd75 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 13:41:50 -0800 Subject: [PATCH 06/59] Whitespaces --- .../services/table/TableServiceIntegrationTest.java | 6 ++++++ 1 file changed, 6 insertions(+) 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 index 3ecfde9f2b5a..f07d3a1bb468 100644 --- 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 @@ -284,17 +284,23 @@ public void insertEntityWorks() throws Exception { // Assert assertNotNull(result); assertNotNull(result.getEntity()); + assertEquals("001", result.getEntity().getPartitionKey()); assertEquals("002", result.getEntity().getRowKey()); assertNotNull(result.getEntity().getTimestamp()); + 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); } From b3fc5506d0e5d0f9e6bebe1122e7347191dc9a8f Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 14:31:19 -0800 Subject: [PATCH 07/59] Add support for "updateEntity" --- .../services/table/TableContract.java | 5 ++ .../implementation/AtomReaderWriter.java | 69 +++++++++++-------- .../TableExceptionProcessor.java | 28 ++++++++ .../table/implementation/TableRestProxy.java | 32 +++++++++ .../services/table/models/Entity.java | 10 +++ .../table/models/UpdateEntityResult.java | 13 ++++ .../table/TableServiceIntegrationTest.java | 23 +++++++ 7 files changed, 153 insertions(+), 27 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UpdateEntityResult.java diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 8fc4caefeadc..4b1611c61e86 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -25,6 +25,7 @@ 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; public interface TableContract extends FilterableService { GetServicePropertiesResult getServiceProperties() throws ServiceException; @@ -58,4 +59,8 @@ public interface TableContract extends FilterableService { InsertEntityResult insertEntity(String table, Entity entity) throws ServiceException; InsertEntityResult insertEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; + + UpdateEntityResult updateEntity(String table, Entity entity) throws ServiceException; + + UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; } 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 index c1943ebdcd1e..0f85fcede8cb 100644 --- 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 @@ -120,12 +120,26 @@ public TableEntry parseTableEntry(InputStream stream) { public Entity parseEntityEntry(InputStream stream) { try { XMLStreamReader xmlr = xmlStreamFactory.getReader(stream); + Entity result = new Entity(); expect(xmlr, XMLStreamConstants.START_DOCUMENT); - Map properties = parseEntryProperties(xmlr); + + 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"); expect(xmlr, XMLStreamConstants.END_DOCUMENT); - return new Entity().setProperties(properties); + return result; } catch (XMLStreamException e) { throw new RuntimeException(e); @@ -184,38 +198,15 @@ private InputStream generateEntry(PropertiesWriter propertiesWriter) { } private TableEntry parseTableEntry(XMLStreamReader xmlr) throws XMLStreamException { - Map properties = parseEntryProperties(xmlr); - TableEntry result = new TableEntry(); - result.setName((String) properties.get("TableName").getValue()); - return result; - } - - private Map parseEntryProperties(XMLStreamReader xmlr) throws XMLStreamException { - Map result = new HashMap(); expect(xmlr, XMLStreamConstants.START_ELEMENT, "entry"); while (!isEndElement(xmlr, "entry")) { - if (isStartElement(xmlr, "properties")) { - nextSignificant(xmlr); - - 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); + Map properties = parseEntryProperties(xmlr); - result.put(name, new Property().setEdmType(edmType).setValue(value)); - - nextSignificant(xmlr); - expect(xmlr, XMLStreamConstants.END_ELEMENT, name); - } - - expect(xmlr, XMLStreamConstants.END_ELEMENT, "properties"); + result.setName((String) properties.get("TableName").getValue()); } else { nextSignificant(xmlr); @@ -227,6 +218,30 @@ private Map parseEntryProperties(XMLStreamReader xmlr) throws 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; 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 index 64215de5c604..8737038654b6 100644 --- 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 @@ -32,6 +32,7 @@ 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; @@ -267,4 +268,31 @@ public InsertEntityResult insertEntity(String table, Entity entity, TableService 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)); + } + } } 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 index 4a2dbc92c29f..852545c8b775 100644 --- 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 @@ -47,6 +47,7 @@ import com.microsoft.windowsazure.services.table.models.ServiceProperties; import com.microsoft.windowsazure.services.table.models.TableServiceOptions; import com.microsoft.windowsazure.services.table.models.UnaryFilterExpression; +import com.microsoft.windowsazure.services.table.models.UpdateEntityResult; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource.Builder; @@ -203,6 +204,11 @@ private WebResource.Builder addTableRequestHeaders(WebResource.Builder builder) 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()); @@ -368,4 +374,30 @@ public InsertEntityResult insertEntity(String table, Entity entity, TableService 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 { + WebResource webResource = getResource(options).path( + table + "(" + "PartitionKey='" + entity.getPartitionKey() + "',RowKey='" + entity.getRowKey() + "')"); + + WebResource.Builder builder = webResource.getRequestBuilder(); + builder = addTableRequestHeaders(builder); + builder = addIfMatchHeader(builder, entity.getEtag()); + + builder = builder.entity(atomReaderWriter.generateEntityEntry(entity), "application/atom+xml"); + + ClientResponse response = builder.put(ClientResponse.class); + ThrowIfError(response); + + UpdateEntityResult result = new UpdateEntityResult(); + result.setEtag(response.getHeaders().getFirst("ETag")); + + return result; + } } 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 index de13eee4740f..ce1d6dff3933 100644 --- 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 @@ -5,8 +5,18 @@ 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(); 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/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java index f07d3a1bb468..254678ed9088 100644 --- 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 @@ -288,6 +288,7 @@ public void insertEntityWorks() throws Exception { assertEquals("001", result.getEntity().getPartitionKey()); assertEquals("002", result.getEntity().getRowKey()); assertNotNull(result.getEntity().getTimestamp()); + assertNotNull(result.getEntity().getEtag()); assertNotNull(result.getEntity().getProperty("test")); assertEquals(true, result.getEntity().getProperty("test").getValue()); @@ -304,4 +305,26 @@ public void insertEntityWorks() throws Exception { 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); + + // Act + InsertEntityResult result = service.insertEntity( + TEST_TABLE_2, + 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())); + + result.getEntity().setProperty("test4", EdmType.INT32, 5); + service.updateEntity(TEST_TABLE_2, result.getEntity()); + + // Assert + } } From 6696e2c75d249c8b934a1ea767193f0f283fe6c8 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 14:41:15 -0800 Subject: [PATCH 08/59] Add support for "DeleteEntity" --- .../services/table/TableContract.java | 6 +++ .../TableExceptionProcessor.java | 28 ++++++++++++ .../table/implementation/TableRestProxy.java | 25 ++++++++++- .../table/models/DeleteEntityOptions.java | 21 +++++++++ .../table/TableServiceIntegrationTest.java | 44 +++++++++++++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/DeleteEntityOptions.java diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 4b1611c61e86..c3a2b2724aef 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -16,6 +16,7 @@ import com.microsoft.windowsazure.services.core.FilterableService; import com.microsoft.windowsazure.services.core.ServiceException; +import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions; import com.microsoft.windowsazure.services.table.models.Entity; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; import com.microsoft.windowsazure.services.table.models.GetTableResult; @@ -63,4 +64,9 @@ public interface TableContract extends FilterableService { UpdateEntityResult updateEntity(String table, Entity entity) throws ServiceException; UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; + + void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException; + + void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options) + throws ServiceException; } 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 index 8737038654b6..a13a87175285 100644 --- 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 @@ -23,6 +23,7 @@ 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.DeleteEntityOptions; import com.microsoft.windowsazure.services.table.models.Entity; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; import com.microsoft.windowsazure.services.table.models.GetTableResult; @@ -295,4 +296,31 @@ public UpdateEntityResult updateEntity(String table, Entity entity, TableService 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)); + } + } } 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 index 852545c8b775..e54e867a89a7 100644 --- 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 @@ -34,6 +34,7 @@ import com.microsoft.windowsazure.services.table.TableContract; import com.microsoft.windowsazure.services.table.models.BinaryFilterExpression; import com.microsoft.windowsazure.services.table.models.ConstantFilterExpression; +import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions; import com.microsoft.windowsazure.services.table.models.Entity; import com.microsoft.windowsazure.services.table.models.FilterExpression; import com.microsoft.windowsazure.services.table.models.GetServicePropertiesResult; @@ -118,6 +119,10 @@ private List encodeODataURIValues(List values) { 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); } @@ -384,7 +389,7 @@ public UpdateEntityResult updateEntity(String table, Entity entity) throws Servi public UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException { WebResource webResource = getResource(options).path( - table + "(" + "PartitionKey='" + entity.getPartitionKey() + "',RowKey='" + entity.getRowKey() + "')"); + getEntityPath(table, entity.getPartitionKey(), entity.getRowKey())); WebResource.Builder builder = webResource.getRequestBuilder(); builder = addTableRequestHeaders(builder); @@ -400,4 +405,22 @@ public UpdateEntityResult updateEntity(String table, Entity entity, TableService 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); + } } 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/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java index 254678ed9088..98e6346cf89f 100644 --- 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 @@ -25,6 +25,7 @@ import org.junit.Test; import com.microsoft.windowsazure.services.core.Configuration; +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.GetTableResult; @@ -327,4 +328,47 @@ public void updateEntityWorks() throws Exception { // Assert } + + @Test + public void deleteEntityWorks() throws Exception { + System.out.println("deleteEntityWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + + // Act + InsertEntityResult result = service.insertEntity( + TEST_TABLE_2, + 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())); + + 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); + + // Act + InsertEntityResult result = service.insertEntity( + TEST_TABLE_2, + 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())); + + service.deleteEntity(TEST_TABLE_2, result.getEntity().getPartitionKey(), result.getEntity().getRowKey(), + new DeleteEntityOptions().setEtag(result.getEntity().getEtag())); + + // Assert + } } From 9cede50c1450d309629c9638e4c6d1f962cecf6f Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 14:58:07 -0800 Subject: [PATCH 09/59] Add support for "mergeEntity" --- .../services/table/TableContract.java | 4 +++ .../TableExceptionProcessor.java | 27 +++++++++++++++++++ .../table/implementation/TableRestProxy.java | 18 ++++++++++++- .../table/TableServiceIntegrationTest.java | 23 ++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index c3a2b2724aef..7e5d968d0874 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -65,6 +65,10 @@ public interface TableContract extends FilterableService { UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; + UpdateEntityResult mergeEntity(String table, Entity entity) throws ServiceException; + + UpdateEntityResult mergeEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; + void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException; void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options) 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 index a13a87175285..abde4ec8cbe5 100644 --- 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 @@ -297,6 +297,33 @@ public UpdateEntityResult updateEntity(String table, Entity entity, TableService } } + @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 void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException { try { 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 index e54e867a89a7..30178256e691 100644 --- 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 @@ -388,6 +388,22 @@ public UpdateEntityResult updateEntity(String table, Entity entity) throws Servi @Override public UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException { + return updateOrMergeEntityCore(table, entity, "PUT", 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 updateOrMergeEntityCore(table, entity, "MERGE", options); + } + + private UpdateEntityResult updateOrMergeEntityCore(String table, Entity entity, String verb, + TableServiceOptions options) throws ServiceException { WebResource webResource = getResource(options).path( getEntityPath(table, entity.getPartitionKey(), entity.getRowKey())); @@ -397,7 +413,7 @@ public UpdateEntityResult updateEntity(String table, Entity entity, TableService builder = builder.entity(atomReaderWriter.generateEntityEntry(entity), "application/atom+xml"); - ClientResponse response = builder.put(ClientResponse.class); + ClientResponse response = builder.method(verb, ClientResponse.class); ThrowIfError(response); UpdateEntityResult result = new UpdateEntityResult(); 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 index 98e6346cf89f..f5ab25bf9e37 100644 --- 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 @@ -329,6 +329,29 @@ public void updateEntityWorks() throws Exception { // Assert } + @Test + public void mergeEntityWorks() throws Exception { + System.out.println("mergeEntityWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + + // Act + InsertEntityResult result = service.insertEntity( + TEST_TABLE_2, + 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())); + + 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()"); From b9a00d4b7ae96a152cdbbf0b9af166d0e8cd5007 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 15:13:34 -0800 Subject: [PATCH 10/59] Add support for "insertOrReplaceEntity" --- .../services/table/TableContract.java | 5 ++++ .../TableExceptionProcessor.java | 27 +++++++++++++++++++ .../table/implementation/TableRestProxy.java | 21 ++++++++++++--- .../table/TableServiceIntegrationTest.java | 22 +++++++++++++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 7e5d968d0874..88526099a933 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -69,6 +69,11 @@ public interface TableContract extends FilterableService { UpdateEntityResult mergeEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; + UpdateEntityResult insertOrReplaceEntity(String table, Entity entity) throws ServiceException; + + UpdateEntityResult insertOrReplaceEntity(String table, Entity entity, TableServiceOptions options) + throws ServiceException; + void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException; void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options) 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 index abde4ec8cbe5..df7384900baf 100644 --- 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 @@ -324,6 +324,33 @@ public UpdateEntityResult mergeEntity(String table, Entity entity, TableServiceO } } + @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 void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException { try { 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 index 30178256e691..db969f5c182a 100644 --- 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 @@ -388,7 +388,7 @@ public UpdateEntityResult updateEntity(String table, Entity entity) throws Servi @Override public UpdateEntityResult updateEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException { - return updateOrMergeEntityCore(table, entity, "PUT", options); + return putOrMergeEntityCore(table, entity, "PUT", true/*includeEtag*/, options); } @Override @@ -399,17 +399,30 @@ public UpdateEntityResult mergeEntity(String table, Entity entity) throws Servic @Override public UpdateEntityResult mergeEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException { - return updateOrMergeEntityCore(table, entity, "MERGE", options); + return putOrMergeEntityCore(table, entity, "MERGE", true/*includeEtag*/, options); } - private UpdateEntityResult updateOrMergeEntityCore(String table, Entity entity, String verb, + @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); + } + + 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); - builder = addIfMatchHeader(builder, entity.getEtag()); + if (includeEtag) { + builder = addIfMatchHeader(builder, entity.getEtag()); + } builder = builder.entity(atomReaderWriter.generateEntityEntry(entity), "application/atom+xml"); 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 index f5ab25bf9e37..5962a2b35bc8 100644 --- 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 @@ -329,6 +329,28 @@ public void updateEntityWorks() throws Exception { // Assert } + @Test + public void insertOrReplaceEntityWorks() throws Exception { + System.out.println("insertOrReplaceEntityWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + + // Act + 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()); + + 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 mergeEntityWorks() throws Exception { System.out.println("mergeEntityWorks()"); From a4f49b043243a7fd73c1dff97a61db7ee09f1708 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 15:18:05 -0800 Subject: [PATCH 11/59] Add support for "insertOrMergeEntity" --- .../services/table/TableContract.java | 5 ++++ .../TableExceptionProcessor.java | 27 +++++++++++++++++++ .../table/implementation/TableRestProxy.java | 11 ++++++++ .../table/TableServiceIntegrationTest.java | 22 +++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 88526099a933..412c9afbdea5 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -74,6 +74,11 @@ public interface TableContract extends FilterableService { UpdateEntityResult insertOrReplaceEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException; + UpdateEntityResult insertOrMergeEntity(String table, Entity entity) throws ServiceException; + + UpdateEntityResult insertOrMergeEntity(String table, Entity entity, TableServiceOptions options) + throws ServiceException; + void deleteEntity(String table, String partitionKey, String rowKey) throws ServiceException; void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options) 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 index df7384900baf..fc1ac6473d7d 100644 --- 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 @@ -351,6 +351,33 @@ public UpdateEntityResult insertOrReplaceEntity(String table, Entity entity, Tab } } + @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 { 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 index db969f5c182a..603a0c04f1d5 100644 --- 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 @@ -413,6 +413,17 @@ public UpdateEntityResult insertOrReplaceEntity(String table, Entity entity, Tab 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( 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 index 5962a2b35bc8..35bac81f30cc 100644 --- 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 @@ -351,6 +351,28 @@ public void insertOrReplaceEntityWorks() throws Exception { // Assert } + @Test + public void insertOrMergeEntityWorks() throws Exception { + System.out.println("insertOrMergeEntityWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + + // Act + 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()); + + 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()"); From bf53b9c4e03e2ccac31dda10e13bcae846e9300f Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 15:51:07 -0800 Subject: [PATCH 12/59] Add support for "getEntity" and "queryEntities" --- .../services/table/TableContract.java | 12 ++ .../implementation/AtomReaderWriter.java | 52 +++++- .../TableExceptionProcessor.java | 56 +++++++ .../table/implementation/TableRestProxy.java | 49 ++++++ .../table/models/GetEntityResult.java | 13 ++ .../table/models/QueryEntitiesOptions.java | 20 +++ .../table/models/QueryEntitiesResult.java | 34 ++++ .../table/TableServiceIntegrationTest.java | 156 +++++++++++++----- 8 files changed, 345 insertions(+), 47 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/GetEntityResult.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesOptions.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesResult.java diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 412c9afbdea5..c214333e8a63 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -18,10 +18,13 @@ import com.microsoft.windowsazure.services.core.ServiceException; 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; @@ -83,4 +86,13 @@ UpdateEntityResult insertOrMergeEntity(String table, Entity entity, TableService void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options) throws ServiceException; + + GetEntityResult getEntity(String table, String partitionKey, String rowKey) throws ServiceException; + + GetEntityResult getEntity(String table, String partitionKey, String rowKey, TableServiceOptions options) + throws ServiceException; + + QueryEntitiesResult queryEntities(String table) throws ServiceException; + + QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions options) throws ServiceException; } 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 index 0f85fcede8cb..c5803bfb5411 100644 --- 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 @@ -117,26 +117,40 @@ public TableEntry parseTableEntry(InputStream stream) { } } - public Entity parseEntityEntry(InputStream stream) { + public List parseEntityEntries(InputStream stream) { try { XMLStreamReader xmlr = xmlStreamFactory.getReader(stream); - Entity result = new Entity(); expect(xmlr, XMLStreamConstants.START_DOCUMENT); + expect(xmlr, XMLStreamConstants.START_ELEMENT, "feed"); - result.setEtag(xmlr.getAttributeValue(null, "etag")); - expect(xmlr, XMLStreamConstants.START_ELEMENT, "entry"); - - while (!isEndElement(xmlr, "entry")) { - if (isStartElement(xmlr, "properties")) { - result.setProperties(parseEntryProperties(xmlr)); + 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, "entry"); + 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; @@ -218,6 +232,26 @@ private TableEntry parseTableEntry(XMLStreamReader xmlr) throws XMLStreamExcepti 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(); 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 index fc1ac6473d7d..93a99570fdba 100644 --- 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 @@ -25,10 +25,13 @@ import com.microsoft.windowsazure.services.table.TableContract; 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; @@ -404,4 +407,57 @@ public void deleteEntity(String table, String partitionKey, String rowKey, Delet 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)); + } + } } 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 index 603a0c04f1d5..51fc33bbeddd 100644 --- 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 @@ -37,12 +37,15 @@ import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions; import com.microsoft.windowsazure.services.table.models.Entity; import com.microsoft.windowsazure.services.table.models.FilterExpression; +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.LitteralFilterExpression; import com.microsoft.windowsazure.services.table.models.QueryBuilder; +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; @@ -463,4 +466,50 @@ public void deleteEntity(String table, String partitionKey, String rowKey, Delet 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.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; + } } 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/QueryEntitiesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesOptions.java new file mode 100644 index 000000000000..55652baec98d --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryEntitiesOptions.java @@ -0,0 +1,20 @@ +package com.microsoft.windowsazure.services.table.models; + +public class QueryEntitiesOptions extends TableServiceOptions { + private QueryBuilder query; + + @Override + public QueryEntitiesOptions setTimeout(Integer timeout) { + super.setTimeout(timeout); + return this; + } + + public QueryBuilder getQuery() { + return query; + } + + public QueryEntitiesOptions setQuery(QueryBuilder query) { + this.query = query; + 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/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java index 35bac81f30cc..27420b286d08 100644 --- 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 @@ -28,9 +28,11 @@ 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.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.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; @@ -40,7 +42,7 @@ public class TableServiceIntegrationTest extends IntegrationTestBase { 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_3; //private static String TEST_TABLE_4; private static String CREATABLE_TABLE_1; private static String CREATABLE_TABLE_2; @@ -64,7 +66,7 @@ public static void setup() throws Exception { TEST_TABLE_1 = testTables[0]; TEST_TABLE_2 = testTables[1]; - //TEST_TABLE_3 = testTables[2]; + TEST_TABLE_3 = testTables[2]; //TEST_TABLE_4 = testTables[3]; CREATABLE_TABLE_1 = creatableTables[0]; @@ -273,21 +275,20 @@ public void insertEntityWorks() throws Exception { // 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, - new Entity().setPartitionKey("001").setRowKey("002").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())); + InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity); // Assert assertNotNull(result); assertNotNull(result.getEntity()); assertEquals("001", result.getEntity().getPartitionKey()); - assertEquals("002", result.getEntity().getRowKey()); + assertEquals("insertEntityWorks", result.getEntity().getRowKey()); assertNotNull(result.getEntity().getTimestamp()); assertNotNull(result.getEntity().getEtag()); @@ -314,15 +315,13 @@ public void updateEntityWorks() throws Exception { // 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, - 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())); - + InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity); result.getEntity().setProperty("test4", EdmType.INT32, 5); service.updateEntity(TEST_TABLE_2, result.getEntity()); @@ -336,13 +335,12 @@ public void insertOrReplaceEntityWorks() throws Exception { // Arrange Configuration config = createConfiguration(); TableContract service = TableService.create(config); - - // Act 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); @@ -358,13 +356,12 @@ public void insertOrMergeEntityWorks() throws Exception { // Arrange Configuration config = createConfiguration(); TableContract service = TableService.create(config); - - // Act 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); @@ -380,14 +377,13 @@ public void mergeEntityWorks() throws Exception { // 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, - 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())); + InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity); result.getEntity().setProperty("test4", EdmType.INT32, 5); result.getEntity().setProperty("test6", EdmType.INT32, 6); @@ -403,14 +399,13 @@ public void deleteEntityWorks() throws Exception { // 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, - 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())); + InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity); service.deleteEntity(TEST_TABLE_2, result.getEntity().getPartitionKey(), result.getEntity().getRowKey()); @@ -424,18 +419,103 @@ public void deleteEntityWithETagWorks() throws Exception { // 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, - 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())); + 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 queryEntityWorks() throws Exception { + System.out.println("queryEntityWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + Entity entity = new Entity().setPartitionKey("001").setRowKey("queryEntityWorks") + .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("queryEntityWorks", 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); + } } From a72bc2ad684f9a18d9cbc62ba0d4446497545877 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 16:29:43 -0800 Subject: [PATCH 13/59] Rename a few classes, fix support for query continuation --- .../table/implementation/TableRestProxy.java | 72 +++++++------- .../services/table/models/BinaryFilter.java | 34 +++++++ .../table/models/BinaryFilterExpression.java | 34 ------- ...terExpression.java => ConstantFilter.java} | 4 +- .../services/table/models/Filter.java | 47 +++++++++ .../table/models/FilterExpression.java | 47 --------- ...terExpression.java => LitteralFilter.java} | 4 +- .../services/table/models/Query.java | 67 +++++++++++++ .../services/table/models/QueryBuilder.java | 96 ------------------- .../table/models/QueryEntitiesOptions.java | 26 ++++- .../table/models/QueryTablesOptions.java | 16 +++- .../table/models/QueryTablesResult.java | 10 +- ...FilterExpression.java => UnaryFilter.java} | 10 +- 13 files changed, 230 insertions(+), 237 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilter.java delete mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilterExpression.java rename microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/{ConstantFilterExpression.java => ConstantFilter.java} (61%) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Filter.java delete mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java rename microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/{LitteralFilterExpression.java => LitteralFilter.java} (62%) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Query.java delete mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryBuilder.java rename microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/{UnaryFilterExpression.java => UnaryFilter.java} (52%) 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 index 51fc33bbeddd..95558d7021dd 100644 --- 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 @@ -32,25 +32,25 @@ 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.models.BinaryFilterExpression; -import com.microsoft.windowsazure.services.table.models.ConstantFilterExpression; +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.FilterExpression; +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.LitteralFilterExpression; -import com.microsoft.windowsazure.services.table.models.QueryBuilder; +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.ServiceProperties; import com.microsoft.windowsazure.services.table.models.TableServiceOptions; -import com.microsoft.windowsazure.services.table.models.UnaryFilterExpression; +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.WebResource; @@ -130,13 +130,13 @@ private WebResource addOptionalQueryParam(WebResource webResource, String key, O return PipelineHelpers.addOptionalQueryParam(webResource, key, value); } - private WebResource addOptionalQuery(WebResource webResource, QueryBuilder query) { + private WebResource addOptionalQuery(WebResource webResource, Query query) { if (query == null) return webResource; - if (query.getFields() != null && query.getFields().size() > 0) { + if (query.getSelectFields() != null && query.getSelectFields().size() > 0) { webResource = addOptionalQueryParam(webResource, "$select", - CommaStringBuilder.join(encodeODataURIValues(query.getFields()))); + CommaStringBuilder.join(encodeODataURIValues(query.getSelectFields()))); } if (query.getTop() != null) { @@ -147,54 +147,45 @@ private WebResource addOptionalQuery(WebResource webResource, QueryBuilder query webResource = addOptionalQueryParam(webResource, "$filter", buildFilterExpression(query.getFilter())); } - if (query.getOrderBy() != null) { + if (query.getOrderByFields() != null) { webResource = addOptionalQueryParam(webResource, "$orderby", - CommaStringBuilder.join(encodeODataURIValues(query.getOrderBy()))); - } - - if (query.getNextPartitionKey() != null) { - webResource = addOptionalQueryParam(webResource, "NextPartitionKey", - encodeODataURIValue(query.getNextPartitionKey())); - } - - if (query.getNextRowKey() != null) { - webResource = addOptionalQueryParam(webResource, "NextRowKey", encodeODataURIValue(query.getNextRowKey())); + CommaStringBuilder.join(encodeODataURIValues(query.getOrderByFields()))); } return webResource; } - private String buildFilterExpression(FilterExpression filter) { + private String buildFilterExpression(Filter filter) { StringBuilder sb = new StringBuilder(); buildFilterExpression(filter, sb); return sb.toString(); } - private void buildFilterExpression(FilterExpression filter, StringBuilder sb) { + private void buildFilterExpression(Filter filter, StringBuilder sb) { if (filter == null) return; - if (filter instanceof LitteralFilterExpression) { - sb.append(((LitteralFilterExpression) filter).getLitteral()); + if (filter instanceof LitteralFilter) { + sb.append(((LitteralFilter) filter).getLitteral()); } - else if (filter instanceof ConstantFilterExpression) { + else if (filter instanceof ConstantFilter) { sb.append("'"); - sb.append(((ConstantFilterExpression) filter).getValue()); + sb.append(((ConstantFilter) filter).getValue()); sb.append("'"); } - else if (filter instanceof UnaryFilterExpression) { - sb.append(((UnaryFilterExpression) filter).getOperator()); + else if (filter instanceof UnaryFilter) { + sb.append(((UnaryFilter) filter).getOperator()); sb.append("("); - buildFilterExpression(((UnaryFilterExpression) filter).getOperand(), sb); + buildFilterExpression(((UnaryFilter) filter).getOperand(), sb); sb.append(")"); } - else if (filter instanceof BinaryFilterExpression) { + else if (filter instanceof BinaryFilter) { sb.append("("); - buildFilterExpression(((BinaryFilterExpression) filter).getLeft(), sb); + buildFilterExpression(((BinaryFilter) filter).getLeft(), sb); sb.append(" "); - sb.append(((BinaryFilterExpression) filter).getOperator()); + sb.append(((BinaryFilter) filter).getOperator()); sb.append(" "); - buildFilterExpression(((BinaryFilterExpression) filter).getRight(), sb); + buildFilterExpression(((BinaryFilter) filter).getRight(), sb); sb.append(")"); } } @@ -288,15 +279,12 @@ public QueryTablesResult listTables() throws ServiceException { @Override public QueryTablesResult listTables(ListTablesOptions options) throws ServiceException { // Append Max char to end '{' is 1 + 'z' in AsciiTable ==> uppperBound is prefix + '{' - FilterExpression filter = FilterExpression.and( - FilterExpression.ge(FilterExpression.litteral("TableName"), - FilterExpression.constant(options.getPrefix())), - FilterExpression.le(FilterExpression.litteral("TableName"), - FilterExpression.constant(options.getPrefix() + "{"))); + 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 QueryBuilder().setFilter(filter)); + queryTableOptions.setQuery(new Query().setFilter(filter)); return queryTables(queryTableOptions); } @@ -309,6 +297,7 @@ public QueryTablesResult queryTables() throws ServiceException { 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); @@ -317,7 +306,7 @@ public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceE ThrowIfError(response); QueryTablesResult result = new QueryTablesResult(); - result.setContinuationToken(response.getHeaders().getFirst("x-ms-continuation-NextTableName")); + result.setNextTableName(response.getHeaders().getFirst("x-ms-continuation-NextTableName")); result.setTables(atomReaderWriter.parseTableEntries(response.getEntityInputStream())); return result; @@ -498,6 +487,9 @@ public QueryEntitiesResult queryEntities(String table) throws ServiceException { 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); 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/BinaryFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilterExpression.java deleted file mode 100644 index 25547090206a..000000000000 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BinaryFilterExpression.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.microsoft.windowsazure.services.table.models; - -public class BinaryFilterExpression extends FilterExpression { - private String operator; - private FilterExpression left; - private FilterExpression right; - - public String getOperator() { - return operator; - } - - public BinaryFilterExpression setOperator(String operator) { - this.operator = operator; - return this; - } - - public FilterExpression getLeft() { - return left; - } - - public BinaryFilterExpression setLeft(FilterExpression left) { - this.left = left; - return this; - } - - public FilterExpression getRight() { - return right; - } - - public BinaryFilterExpression setRight(FilterExpression right) { - this.right = right; - return this; - } -} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilter.java similarity index 61% rename from microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilterExpression.java rename to microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilter.java index 4c38168ca295..9dd16b1a1daa 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilterExpression.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ConstantFilter.java @@ -1,13 +1,13 @@ package com.microsoft.windowsazure.services.table.models; -public class ConstantFilterExpression extends FilterExpression { +public class ConstantFilter extends Filter { private Object value; public Object getValue() { return value; } - public ConstantFilterExpression setValue(Object 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/Filter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Filter.java new file mode 100644 index 000000000000..fcb3560758a2 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Filter.java @@ -0,0 +1,47 @@ +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); + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java deleted file mode 100644 index 655a53838e75..000000000000 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/FilterExpression.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.microsoft.windowsazure.services.table.models; - -public class FilterExpression { - public static UnaryFilterExpression not(FilterExpression operand) { - return new UnaryFilterExpression().setOperator("not").setOperand(operand); - } - - public static BinaryFilterExpression and(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("and").setLeft(left).setRight(right); - } - - public static BinaryFilterExpression or(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("or").setLeft(left).setRight(right); - } - - public static BinaryFilterExpression eq(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("eq").setLeft(left).setRight(right); - } - - public static BinaryFilterExpression ne(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("ne").setLeft(left).setRight(right); - } - - public static BinaryFilterExpression ge(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("ge").setLeft(left).setRight(right); - } - - public static BinaryFilterExpression gt(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("gt").setLeft(left).setRight(right); - } - - public static BinaryFilterExpression lt(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("lt").setLeft(left).setRight(right); - } - - public static BinaryFilterExpression le(FilterExpression left, FilterExpression right) { - return new BinaryFilterExpression().setOperator("le").setLeft(left).setRight(right); - } - - public static ConstantFilterExpression constant(Object value) { - return new ConstantFilterExpression().setValue(value); - } - - public static LitteralFilterExpression litteral(String value) { - return new LitteralFilterExpression().setLitteral(value); - } -} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilter.java similarity index 62% rename from microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilterExpression.java rename to microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilter.java index 8c4d857624e3..d67f47162c4d 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilterExpression.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilter.java @@ -1,13 +1,13 @@ package com.microsoft.windowsazure.services.table.models; -public class LitteralFilterExpression extends FilterExpression { +public class LitteralFilter extends Filter { private String litteral; public String getLitteral() { return litteral; } - public LitteralFilterExpression setLitteral(String 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/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/QueryBuilder.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryBuilder.java deleted file mode 100644 index 7c915cf53cb4..000000000000 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryBuilder.java +++ /dev/null @@ -1,96 +0,0 @@ -package com.microsoft.windowsazure.services.table.models; - -import java.util.List; - -public class QueryBuilder { - private List fields; - private String from; - private FilterExpression filter; - private List orderBy; - private Integer top; - private String partitionKey; - private String nextPartitionKey; - private String rowKey; - private String nextRowKey; - - public List getFields() { - return fields; - } - - public QueryBuilder setFields(List fields) { - this.fields = fields; - return this; - } - - public String getFrom() { - return from; - } - - public QueryBuilder setFrom(String from) { - this.from = from; - return this; - } - - public FilterExpression getFilter() { - return filter; - } - - public QueryBuilder setFilter(FilterExpression where) { - this.filter = where; - return this; - } - - public List getOrderBy() { - return orderBy; - } - - public QueryBuilder setOrderBy(List orderBy) { - this.orderBy = orderBy; - return this; - } - - public Integer getTop() { - return top; - } - - public QueryBuilder setTop(Integer top) { - this.top = top; - return this; - } - - public String getPartitionKey() { - return partitionKey; - } - - public QueryBuilder setPartitionKey(String partitionKey) { - this.partitionKey = partitionKey; - return this; - } - - public String getNextPartitionKey() { - return nextPartitionKey; - } - - public QueryBuilder setNextPartitionKey(String nextPartitionKey) { - this.nextPartitionKey = nextPartitionKey; - return this; - } - - public String getRowKey() { - return rowKey; - } - - public QueryBuilder setRowKey(String rowKey) { - this.rowKey = rowKey; - return this; - } - - public String getNextRowKey() { - return nextRowKey; - } - - public QueryBuilder setNextRowKey(String nextRowKey) { - this.nextRowKey = nextRowKey; - 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 index 55652baec98d..1de1cd47426b 100644 --- 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 @@ -1,7 +1,9 @@ package com.microsoft.windowsazure.services.table.models; public class QueryEntitiesOptions extends TableServiceOptions { - private QueryBuilder query; + private Query query; + public String nextPartitionKey; + public String nextRowKey; @Override public QueryEntitiesOptions setTimeout(Integer timeout) { @@ -9,12 +11,30 @@ public QueryEntitiesOptions setTimeout(Integer timeout) { return this; } - public QueryBuilder getQuery() { + public Query getQuery() { return query; } - public QueryEntitiesOptions setQuery(QueryBuilder 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/QueryTablesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesOptions.java index d4896fa67498..d402cd8527cc 100644 --- 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 @@ -1,7 +1,8 @@ package com.microsoft.windowsazure.services.table.models; public class QueryTablesOptions extends TableServiceOptions { - private QueryBuilder query; + private String nextTableName; + private Query query; @Override public QueryTablesOptions setTimeout(Integer timeout) { @@ -9,12 +10,21 @@ public QueryTablesOptions setTimeout(Integer timeout) { return this; } - public QueryBuilder getQuery() { + public Query getQuery() { return query; } - public QueryTablesOptions setQuery(QueryBuilder 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 index 3816ffe5899a..dfd1c1b7f4aa 100644 --- 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 @@ -3,15 +3,15 @@ import java.util.List; public class QueryTablesResult { - private String continuationToken; + private String nextTableName; private List tables; - public String getContinuationToken() { - return continuationToken; + public String getNextTableName() { + return nextTableName; } - public void setContinuationToken(String continuationToken) { - this.continuationToken = continuationToken; + public void setNextTableName(String nextTableName) { + this.nextTableName = nextTableName; } public List getTables() { diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilterExpression.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilter.java similarity index 52% rename from microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilterExpression.java rename to microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilter.java index 3e0efd984bff..42d4b7a870fc 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilterExpression.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/UnaryFilter.java @@ -1,23 +1,23 @@ package com.microsoft.windowsazure.services.table.models; -public class UnaryFilterExpression extends FilterExpression { +public class UnaryFilter extends Filter { private String operator; - private FilterExpression operand; + private Filter operand; public String getOperator() { return operator; } - public UnaryFilterExpression setOperator(String operator) { + public UnaryFilter setOperator(String operator) { this.operator = operator; return this; } - public FilterExpression getOperand() { + public Filter getOperand() { return operand; } - public UnaryFilterExpression setOperand(FilterExpression operand) { + public UnaryFilter setOperand(Filter operand) { this.operand = operand; return this; } From 711e23d2b66d96336403592fb609d5f531bfd60c Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 17:04:54 -0800 Subject: [PATCH 14/59] Fix bug with server sometimes sending back shorter ISO 8601 dates --- .../implementation/ISO8601DateConverter.java | 17 ++++++- .../ISO8601DateConverterTests.java | 51 +++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/blob/implementation/ISO8601DateConverterTests.java 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 index 08d7d6ea9ce8..e796bf00794c 100644 --- 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 @@ -27,13 +27,22 @@ 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 { - return getFormat().parse(date); + 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() { @@ -41,4 +50,10 @@ private DateFormat getFormat() { 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/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); + } +} From 06146b7c25c6ec34b1860402a0f047a33fce0697 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 17:12:31 -0800 Subject: [PATCH 15/59] Adding test for query entities continuation The test is actually not testing pagination by default because there is no way to reach the default # of entities (1,000) in a reasonable amount of time. Also adding test for query entities filtering --- .../table/TableServiceIntegrationTest.java | 86 +++++++++++++++++-- 1 file changed, 80 insertions(+), 6 deletions(-) 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 index 27420b286d08..cf45713dc8fd 100644 --- 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 @@ -28,10 +28,13 @@ 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; @@ -43,7 +46,8 @@ public class TableServiceIntegrationTest extends IntegrationTestBase { 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_4; + private static String TEST_TABLE_5; private static String CREATABLE_TABLE_1; private static String CREATABLE_TABLE_2; //private static String CREATABLE_TABLE_3; @@ -67,7 +71,8 @@ public static void setup() throws Exception { TEST_TABLE_1 = testTables[0]; TEST_TABLE_2 = testTables[1]; TEST_TABLE_3 = testTables[2]; - //TEST_TABLE_4 = testTables[3]; + TEST_TABLE_4 = testTables[3]; + TEST_TABLE_5 = testTables[4]; CREATABLE_TABLE_1 = creatableTables[0]; CREATABLE_TABLE_2 = creatableTables[1]; @@ -476,13 +481,13 @@ public void getEntityWorks() throws Exception { } @Test - public void queryEntityWorks() throws Exception { - System.out.println("queryEntityWorks()"); + 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("queryEntityWorks") + 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()); @@ -499,7 +504,7 @@ public void queryEntityWorks() throws Exception { assertNotNull(result.getEntities().get(0)); assertEquals("001", result.getEntities().get(0).getPartitionKey()); - assertEquals("queryEntityWorks", result.getEntities().get(0).getRowKey()); + assertEquals("queryEntitiesWorks", result.getEntities().get(0).getRowKey()); assertNotNull(result.getEntities().get(0).getTimestamp()); assertNotNull(result.getEntities().get(0).getEtag()); @@ -518,4 +523,73 @@ public void queryEntityWorks() throws Exception { 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()); + } } From 8fdbcf75d024c17cd220c37b32c668a1b1f9578f Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 11 Jan 2012 17:23:22 -0800 Subject: [PATCH 16/59] Adding a couple more unit test for query entities Also adding escape mechanism for expressing filters --- .../table/implementation/TableRestProxy.java | 4 +++ .../services/table/models/Filter.java | 4 +++ .../table/models/RawStringFilter.java | 14 +++++++++ .../table/TableServiceIntegrationTest.java | 30 +++++++++++++------ 4 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/RawStringFilter.java 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 index 95558d7021dd..3f18d447b788 100644 --- 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 @@ -48,6 +48,7 @@ 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; @@ -188,6 +189,9 @@ else if (filter instanceof BinaryFilter) { 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) { 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 index fcb3560758a2..f0a54f58b09b 100644 --- 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 @@ -44,4 +44,8 @@ public static ConstantFilter constant(Object 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/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/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java index cf45713dc8fd..8e12ebac2b04 100644 --- 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 @@ -581,15 +581,27 @@ public void queryEntitiesWithFilterWorks() throws Exception { 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"))))); + { + // 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()); + // 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()); + } } } From 3f5f5d780d48deb6534a6d43fc5c2fd99d401ef9 Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Thu, 12 Jan 2012 16:18:06 -0800 Subject: [PATCH 17/59] Initial (not quite working yet) support for batch operations --- microsoft-azure-api/pom.xml | 5 + .../windowsazure/services/table/Exports.java | 2 + .../services/table/TableContract.java | 6 + .../implementation/MimeReaderWriter.java | 113 ++++++ .../TableExceptionProcessor.java | 28 ++ .../table/implementation/TableRestProxy.java | 104 +++++- .../table/models/BatchOperations.java | 197 ++++++++++ .../services/table/models/BatchResult.java | 5 + .../table/TableServiceIntegrationTest.java | 47 +++ .../AtomReaderWriterTests.java | 6 +- .../implementation/MimeMultipartTests.java | 336 ++++++++++++++++++ 11 files changed, 841 insertions(+), 8 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/MimeReaderWriter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchOperations.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchResult.java rename microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/{ => implementation}/AtomReaderWriterTests.java (93%) create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/MimeMultipartTests.java diff --git a/microsoft-azure-api/pom.xml b/microsoft-azure-api/pom.xml index 5da3a62a9e6c..36651b165c7c 100644 --- a/microsoft-azure-api/pom.xml +++ b/microsoft-azure-api/pom.xml @@ -80,6 +80,11 @@ commons-logging 1.1.1 + + javax.mail + mail + 1.4 + diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java index 21ad0784df4b..59322db2264d 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java @@ -18,6 +18,7 @@ import com.microsoft.windowsazure.services.table.implementation.AtomReaderWriter; import com.microsoft.windowsazure.services.table.implementation.DefaultEdmValueConterter; import com.microsoft.windowsazure.services.table.implementation.DefaultXMLStreamFactory; +import com.microsoft.windowsazure.services.table.implementation.MimeReaderWriter; import com.microsoft.windowsazure.services.table.implementation.SharedKeyFilter; import com.microsoft.windowsazure.services.table.implementation.SharedKeyLiteFilter; import com.microsoft.windowsazure.services.table.implementation.TableExceptionProcessor; @@ -34,6 +35,7 @@ public void register(Builder.Registry registry) { registry.add(SharedKeyFilter.class); registry.add(XMLStreamFactory.class, DefaultXMLStreamFactory.class); registry.add(AtomReaderWriter.class); + registry.add(MimeReaderWriter.class); registry.add(EdmValueConverter.class, DefaultEdmValueConterter.class); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index c214333e8a63..63ec2c1b2086 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -16,6 +16,8 @@ import com.microsoft.windowsazure.services.core.FilterableService; 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.DeleteEntityOptions; import com.microsoft.windowsazure.services.table.models.Entity; import com.microsoft.windowsazure.services.table.models.GetEntityResult; @@ -95,4 +97,8 @@ GetEntityResult getEntity(String table, String partitionKey, String rowKey, Tabl QueryEntitiesResult queryEntities(String table) throws ServiceException; QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions options) throws ServiceException; + + BatchResult batch(BatchOperations operations) throws ServiceException; + + BatchResult batch(BatchOperations operations, TableServiceOptions options) throws ServiceException; } 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..933d3f8aac55 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/MimeReaderWriter.java @@ -0,0 +1,113 @@ +package com.microsoft.windowsazure.services.table.implementation; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.UUID; + +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; + +public class MimeReaderWriter { + + @Inject + public MimeReaderWriter() { + } + + public MimeMultipart getMimeMultipart(List bodyPartContents) { + try { + return getMimeMultipartCore(bodyPartContents); + } + catch (MessagingException e) { + throw new RuntimeException(e); + } + } + + private MimeMultipart getMimeMultipartCore(List bodyPartContents) throws MessagingException { + // 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 (String bodyPart : bodyPartContents) { + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + + mimeBodyPart.setContent(bodyPart, "application/http"); + + //Note: Both content type and encoding need to be set *after* setting content, because + // MimeBodyPart implementation replaces them when calling "setContent". + mimeBodyPart.setHeader("Content-Type", "application/http"); + 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; + } + } +} 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 index 93a99570fdba..c8bf695b7cdd 100644 --- 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 @@ -23,6 +23,8 @@ 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; @@ -460,4 +462,30 @@ public QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions opti 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 index 3f18d447b788..39deca1589bf 100644 --- 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 @@ -14,12 +14,17 @@ */ package com.microsoft.windowsazure.services.table.implementation; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.inject.Inject; import javax.inject.Named; +import javax.mail.internet.MimeMultipart; import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter; import com.microsoft.windowsazure.services.blob.implementation.RFC1123DateConverter; @@ -32,6 +37,10 @@ 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.models.BatchOperations; +import com.microsoft.windowsazure.services.table.models.BatchOperations.InsertOperation; +import com.microsoft.windowsazure.services.table.models.BatchOperations.Operation; +import com.microsoft.windowsazure.services.table.models.BatchResult; import com.microsoft.windowsazure.services.table.models.BinaryFilter; import com.microsoft.windowsazure.services.table.models.ConstantFilter; import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions; @@ -67,11 +76,12 @@ public class TableRestProxy implements TableContract { private final ServiceFilter[] filters; private final SharedKeyFilter filter; private final AtomReaderWriter atomReaderWriter; + private final MimeReaderWriter mimeReaderWriter; @Inject public TableRestProxy(HttpURLConnectionClient channel, @Named(TableConfiguration.URI) String url, SharedKeyFilter filter, DateFactory dateFactory, ISO8601DateConverter iso8601DateConverter, - AtomReaderWriter atomReaderWriter) { + AtomReaderWriter atomReaderWriter, MimeReaderWriter mimeReaderWriter) { this.channel = channel; this.url = url; @@ -81,12 +91,13 @@ public TableRestProxy(HttpURLConnectionClient channel, @Named(TableConfiguration this.filters = new ServiceFilter[0]; this.dateFactory = dateFactory; this.atomReaderWriter = atomReaderWriter; + this.mimeReaderWriter = mimeReaderWriter; channel.addFilter(filter); } public TableRestProxy(HttpURLConnectionClient channel, ServiceFilter[] filters, String url, SharedKeyFilter filter, - DateFactory dateFactory, AtomReaderWriter atomReaderWriter, RFC1123DateConverter dateMapper, - ISO8601DateConverter iso8601DateConverter) { + DateFactory dateFactory, AtomReaderWriter atomReaderWriter, MimeReaderWriter mimeReaderWriter, + RFC1123DateConverter dateMapper, ISO8601DateConverter iso8601DateConverter) { this.channel = channel; this.filters = filters; @@ -94,6 +105,7 @@ public TableRestProxy(HttpURLConnectionClient channel, ServiceFilter[] filters, this.filter = filter; this.dateFactory = dateFactory; this.atomReaderWriter = atomReaderWriter; + this.mimeReaderWriter = mimeReaderWriter; this.dateMapper = dateMapper; this.iso8601DateConverter = iso8601DateConverter; } @@ -103,7 +115,7 @@ 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.dateMapper, this.iso8601DateConverter); + this.atomReaderWriter, this.mimeReaderWriter, this.dateMapper, this.iso8601DateConverter); } private void ThrowIfError(ClientResponse r) { @@ -508,4 +520,88 @@ public QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions opti 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 = createMimeMultipart(operations); + builder = builder.type(entity.getContentType()); + + ClientResponse response = builder.post(ClientResponse.class, entity); + ThrowIfError(response); + + return null; + } + + private MimeMultipart createMimeMultipart(BatchOperations operations) { + try { + List bodyPartContents = new ArrayList(); + int contentId = 1; + for (Operation operation : operations.getOperations()) { + + String bodyPartContent = null; + // INSERT + if (operation instanceof InsertOperation) { + InsertOperation op = (InsertOperation) operation; + + //TODO: Review code to make sure encoding is correct + InputStream stream = atomReaderWriter.generateEntityEntry(op.getEntity()); + byte[] bytes = inputStreamToByteArray(stream); + String content = new String(bytes, "UTF-8"); + + StringBuilder sb = new StringBuilder(); + sb.append(String.format("POST %s HTTP/1.1\r\n", channel.resource(url).path(op.getTable()).getURI())); + sb.append(String.format("Content-ID: %d\r\n", contentId++)); + sb.append("Content-Type: application/atom+xml;type=entry\r\n"); + sb.append(String.format("Content-Length: %d\r\n", content.length())); + sb.append("\r\n"); + sb.append(content); + + bodyPartContent = sb.toString(); + } + + if (bodyPartContent != null) { + bodyPartContents.add(bodyPartContent); + } + } + + return mimeReaderWriter.getMimeMultipart(bodyPartContents); + } + catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + 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/models/BatchOperations.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchOperations.java new file mode 100644 index 000000000000..43c93c22b0f8 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchOperations.java @@ -0,0 +1,197 @@ +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 InsertOperation().setTable(table).setEntity(entity)); + return this; + } + + public BatchOperations addUpdateEntity(String table, Entity entity) { + this.operations.add(new UpdateOperation().setTable(table).setEntity(entity)); + return this; + } + + public BatchOperations addMergeEntity(String table, Entity entity) { + this.operations.add(new MergeOperation().setTable(table).setEntity(entity)); + return this; + } + + public BatchOperations addInsertOrReplaceEntity(String table, Entity entity) { + this.operations.add(new InsertOrReplaceOperation().setTable(table).setEntity(entity)); + return this; + } + + public BatchOperations addInsertOrMergeEntity(String table, Entity entity) { + this.operations.add(new InsertOrMergeOperation().setTable(table).setEntity(entity)); + return this; + } + + public BatchOperations addDeleteEntity(String table, String partitionKey, String rowKey) { + this.operations.add(new DeleteOperation().setTable(table).setPartitionKey(partitionKey).setRowKey(rowKey)); + return this; + } + + public abstract class Operation { + } + + public class InsertOperation extends Operation { + private String table; + private Entity entity; + + public String getTable() { + return table; + } + + public InsertOperation setTable(String table) { + this.table = table; + return this; + } + + public Entity getEntity() { + return entity; + } + + public InsertOperation setEntity(Entity entity) { + this.entity = entity; + return this; + } + } + + public class UpdateOperation extends Operation { + private String table; + private Entity entity; + + public String getTable() { + return table; + } + + public UpdateOperation setTable(String table) { + this.table = table; + return this; + } + + public Entity getEntity() { + return entity; + } + + public UpdateOperation setEntity(Entity entity) { + this.entity = entity; + return this; + } + } + + public class MergeOperation extends Operation { + private String table; + private Entity entity; + + public String getTable() { + return table; + } + + public MergeOperation setTable(String table) { + this.table = table; + return this; + } + + public Entity getEntity() { + return entity; + } + + public MergeOperation setEntity(Entity entity) { + this.entity = entity; + return this; + } + } + + public class InsertOrReplaceOperation extends Operation { + private String table; + private Entity entity; + + public String getTable() { + return table; + } + + public InsertOrReplaceOperation setTable(String table) { + this.table = table; + return this; + } + + public Entity getEntity() { + return entity; + } + + public InsertOrReplaceOperation setEntity(Entity entity) { + this.entity = entity; + return this; + } + } + + public class InsertOrMergeOperation extends Operation { + private String table; + private Entity entity; + + public String getTable() { + return table; + } + + public InsertOrMergeOperation setTable(String table) { + this.table = table; + return this; + } + + public Entity getEntity() { + return entity; + } + + public InsertOrMergeOperation setEntity(Entity entity) { + this.entity = entity; + return this; + } + } + + public class DeleteOperation extends Operation { + private String table; + private String partitionKey; + private String rowKey; + + public String getTable() { + return table; + } + + public DeleteOperation setTable(String table) { + this.table = table; + return this; + } + + public String getPartitionKey() { + return partitionKey; + } + + public DeleteOperation setPartitionKey(String partitionKey) { + this.partitionKey = partitionKey; + return this; + } + + public String getRowKey() { + return rowKey; + } + + public DeleteOperation setRowKey(String rowKey) { + this.rowKey = rowKey; + 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..462dc4d10ef6 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/BatchResult.java @@ -0,0 +1,5 @@ +package com.microsoft.windowsazure.services.table.models; + +public class BatchResult { + +} 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 index 8e12ebac2b04..c489c675735a 100644 --- 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 @@ -25,6 +25,11 @@ 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.DeleteEntityOptions; import com.microsoft.windowsazure.services.table.models.EdmType; import com.microsoft.windowsazure.services.table.models.Entity; @@ -48,6 +53,7 @@ public class TableServiceIntegrationTest extends IntegrationTestBase { 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 CREATABLE_TABLE_1; private static String CREATABLE_TABLE_2; //private static String CREATABLE_TABLE_3; @@ -56,6 +62,9 @@ public class TableServiceIntegrationTest extends IntegrationTestBase { @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]; @@ -73,6 +82,7 @@ public static void setup() throws Exception { TEST_TABLE_3 = testTables[2]; TEST_TABLE_4 = testTables[3]; TEST_TABLE_5 = testTables[4]; + TEST_TABLE_6 = testTables[5]; CREATABLE_TABLE_1 = creatableTables[0]; CREATABLE_TABLE_2 = creatableTables[1]; @@ -82,6 +92,8 @@ public static void setup() throws Exception { Configuration config = createConfiguration(); TableContract service = TableService.create(config); + deleteAllTables(service, testTables); + deleteAllTables(service, creatableTables); createTables(service, testTablesPrefix, testTables); } @@ -95,6 +107,9 @@ public static void cleanup() throws Exception { } 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)) { @@ -112,6 +127,17 @@ private static void deleteTables(TableContract service, String prefix, String[] } } + 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)); @@ -604,4 +630,25 @@ public void queryEntitiesWithFilterWorks() throws Exception { assertEquals("queryEntitiesWithFilterWorks-3", result.getEntities().get(0).getRowKey()); } } + + @Test + public void batchWorks() throws Exception { + System.out.println("batchWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + String table = TEST_TABLE_6; + String partitionKey = "001"; + Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchWorks") + .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 + BatchResult result = service.batch(new BatchOperations().addInsertEntity(table, entity)); + + // Assert + assertNotNull(result); + } } diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriterTests.java similarity index 93% rename from microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java rename to microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriterTests.java index 9aebd223e75b..aa14a71f8597 100644 --- a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/AtomReaderWriterTests.java +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/implementation/AtomReaderWriterTests.java @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.microsoft.windowsazure.services.table; +package com.microsoft.windowsazure.services.table.implementation; import static org.junit.Assert.*; @@ -24,9 +24,7 @@ import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter; import com.microsoft.windowsazure.services.core.utils.DefaultDateFactory; -import com.microsoft.windowsazure.services.table.implementation.AtomReaderWriter; -import com.microsoft.windowsazure.services.table.implementation.DefaultEdmValueConterter; -import com.microsoft.windowsazure.services.table.implementation.DefaultXMLStreamFactory; +import com.microsoft.windowsazure.services.table.IntegrationTestBase; import com.microsoft.windowsazure.services.table.models.TableEntry; public class AtomReaderWriterTests extends IntegrationTestBase { 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" + + " <updated>2009-04-30T20:45:13.7155321Z</updated>\r\n" + + " <author>\r\n" + + " <name />\r\n" + + " </author>\r\n" + + " <id />\r\n" + + " <content type=\"application/xml\">\r\n" + + " <m:properties>\r\n" + + " <d:PartitionKey>Channel_19</d:PartitionKey>\r\n" + + " <d:RowKey>1</d:RowKey>\r\n" + + " <d:Timestamp m:type=\"Edm.DateTime\">0001-01-01T00:00:00</d:Timestamp>\r\n" + + " <d:Rating m:type=\"Edm.Int32\">9</d:Rating>\r\n" + + " <d:Text>.NET...</d:Title>\r\n" + + " </m:properties>\r\n" + + " </content>\r\n" + + "</entry>"; + //@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" + + "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\r\n" + + "<entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns=\"http://www.w3.org/2005/Atom\">\r\n" + + " <title />\r\n" + + " <updated>2009-04-30T20:45:13.7155321Z</updated>\r\n" + + " <author>\r\n" + + " <name />\r\n" + + " </author>\r\n" + + " <id />\r\n" + + " <content type=\"application/xml\">\r\n" + + " <m:properties>\r\n" + + " <d:PartitionKey>Channel_19</d:PartitionKey>\r\n" + + " <d:RowKey>1</d:RowKey>\r\n" + + " <d:Timestamp m:type=\"Edm.DateTime\">0001-01-01T00:00:00</d:Timestamp>\r\n" + + " <d:Rating m:type=\"Edm.Int32\">9</d:Rating>\r\n" + + " <d:Text>.NET...</d:Title>\r\n" + + " </m:properties>\r\n" + + " </content>\r\n" + + "</entry>\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" + + "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\r\n" + + "<entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\" xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\" xmlns=\"http://www.w3.org/2005/Atom\">\r\n" + + " <title />\r\n" + + " <updated>2009-04-30T20:45:13.7155321Z</updated>\r\n" + + " <author>\r\n" + + " <name />\r\n" + + " </author>\r\n" + + " <id />\r\n" + + " <content type=\"application/xml\">\r\n" + + " <m:properties>\r\n" + + " <d:PartitionKey>Channel_19</d:PartitionKey>\r\n" + + " <d:RowKey>1</d:RowKey>\r\n" + + " <d:Timestamp m:type=\"Edm.DateTime\">0001-01-01T00:00:00</d:Timestamp>\r\n" + + " <d:Rating m:type=\"Edm.Int32\">9</d:Rating>\r\n" + + " <d:Text>.NET...</d:Title>\r\n" + + " </m:properties>\r\n" + + " </content>\r\n" + + "</entry>\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; + } + } +} From 802e9b0f69ac0b7e35a0daf143dd2b934f0e4d34 Mon Sep 17 00:00:00 2001 From: Renaud Paquay <renaud.paquay@microsoft.com> Date: Fri, 13 Jan 2012 16:44:36 -0800 Subject: [PATCH 18/59] Add full support for "batch" operation on tables --- .../core/utils/pipeline/PipelineHelpers.java | 18 +- .../windowsazure/services/table/Exports.java | 2 + .../implementation/HttpReaderWriter.java | 168 ++++++++++++++++++ .../implementation/InputStreamDataSource.java | 38 ++++ .../implementation/MimeReaderWriter.java | 50 +++++- .../table/implementation/TableRestProxy.java | 164 +++++++++++++---- .../table/models/BatchOperations.java | 50 +++--- .../services/table/models/BatchResult.java | 60 +++++++ .../table/TableServiceIntegrationTest.java | 3 + 9 files changed, 473 insertions(+), 80 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/HttpReaderWriter.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/InputStreamDataSource.java diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java index 9cebe855ee2a..a36de8dc0247 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.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.core.utils.pipeline; @@ -28,7 +28,7 @@ public class PipelineHelpers { public static void ThrowIfError(ClientResponse r) { - if (r.getStatus() >= 300) { + if (r.getStatus() >= 400) { throw new UniformInterfaceException(r); } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java index 59322db2264d..ee52dd47bfb2 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java @@ -18,6 +18,7 @@ import com.microsoft.windowsazure.services.table.implementation.AtomReaderWriter; import com.microsoft.windowsazure.services.table.implementation.DefaultEdmValueConterter; import com.microsoft.windowsazure.services.table.implementation.DefaultXMLStreamFactory; +import com.microsoft.windowsazure.services.table.implementation.HttpReaderWriter; import com.microsoft.windowsazure.services.table.implementation.MimeReaderWriter; import com.microsoft.windowsazure.services.table.implementation.SharedKeyFilter; import com.microsoft.windowsazure.services.table.implementation.SharedKeyLiteFilter; @@ -36,6 +37,7 @@ public void register(Builder.Registry registry) { registry.add(XMLStreamFactory.class, DefaultXMLStreamFactory.class); registry.add(AtomReaderWriter.class); registry.add(MimeReaderWriter.class); + registry.add(HttpReaderWriter.class); registry.add(EdmValueConverter.class, DefaultEdmValueConterter.class); } } 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<Header> 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 index 933d3f8aac55..9d2282ab08e6 100644 --- 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 @@ -3,15 +3,19 @@ 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 { @@ -19,16 +23,20 @@ public class MimeReaderWriter { public MimeReaderWriter() { } - public MimeMultipart getMimeMultipart(List<String> bodyPartContents) { + public MimeMultipart getMimeMultipart(List<DataSource> bodyPartContents) { try { return getMimeMultipartCore(bodyPartContents); } catch (MessagingException e) { throw new RuntimeException(e); } + catch (IOException e) { + throw new RuntimeException(e); + } } - private MimeMultipart getMimeMultipartCore(List<String> bodyPartContents) throws MessagingException { + private MimeMultipart getMimeMultipartCore(List<DataSource> 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()); @@ -38,14 +46,11 @@ private MimeMultipart getMimeMultipartCore(List<String> bodyPartContents) throws // MimeMultipart changeSets = new MimeMultipart(new SetBoundaryMultipartDataSource(changeSet)); - for (String bodyPart : bodyPartContents) { + for (DataSource bodyPart : bodyPartContents) { MimeBodyPart mimeBodyPart = new MimeBodyPart(); - mimeBodyPart.setContent(bodyPart, "application/http"); - - //Note: Both content type and encoding need to be set *after* setting content, because - // MimeBodyPart implementation replaces them when calling "setContent". - mimeBodyPart.setHeader("Content-Type", "application/http"); + mimeBodyPart.setDataHandler(new DataHandler(bodyPart)); + mimeBodyPart.setHeader("Content-Type", bodyPart.getContentType()); mimeBodyPart.setHeader("Content-Transfer-Encoding", "binary"); changeSets.addBodyPart(mimeBodyPart); @@ -110,4 +115,33 @@ public BodyPart getBodyPart(int index) throws MessagingException { return null; } } + + public List<DataSource> 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<DataSource> 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<DataSource> result = new ArrayList<DataSource>(); + 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/TableRestProxy.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/implementation/TableRestProxy.java index 39deca1589bf..944bb8214424 100644 --- 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 @@ -14,16 +14,20 @@ */ 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.io.UnsupportedEncodingException; 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; @@ -32,15 +36,27 @@ 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.InsertOperation; +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; @@ -63,8 +79,10 @@ 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"; @@ -77,11 +95,12 @@ public class TableRestProxy implements TableContract { 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) { + AtomReaderWriter atomReaderWriter, MimeReaderWriter mimeReaderWriter, HttpReaderWriter httpReaderWriter) { this.channel = channel; this.url = url; @@ -92,12 +111,14 @@ public TableRestProxy(HttpURLConnectionClient channel, @Named(TableConfiguration 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, - RFC1123DateConverter dateMapper, ISO8601DateConverter iso8601DateConverter) { + HttpReaderWriter httpReaderWriter, RFC1123DateConverter dateMapper, + ISO8601DateConverter iso8601DateConverter) { this.channel = channel; this.filters = filters; @@ -106,6 +127,7 @@ public TableRestProxy(HttpURLConnectionClient channel, ServiceFilter[] filters, this.dateFactory = dateFactory; this.atomReaderWriter = atomReaderWriter; this.mimeReaderWriter = mimeReaderWriter; + this.httpReaderWriter = httpReaderWriter; this.dateMapper = dateMapper; this.iso8601DateConverter = iso8601DateConverter; } @@ -115,7 +137,8 @@ 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.dateMapper, this.iso8601DateConverter); + this.atomReaderWriter, this.mimeReaderWriter, this.httpReaderWriter, this.dateMapper, + this.iso8601DateConverter); } private void ThrowIfError(ClientResponse r) { @@ -539,46 +562,111 @@ public BatchResult batch(BatchOperations operations, TableServiceOptions options ClientResponse response = builder.post(ClientResponse.class, entity); ThrowIfError(response); - return null; + BatchResult result = new BatchResult(); + result.setEntries(parseMimeMultipart(response, operations)); + + return result; } - private MimeMultipart createMimeMultipart(BatchOperations operations) { - try { - List<String> bodyPartContents = new ArrayList<String>(); - int contentId = 1; - for (Operation operation : operations.getOperations()) { - - String bodyPartContent = null; - // INSERT - if (operation instanceof InsertOperation) { - InsertOperation op = (InsertOperation) operation; - - //TODO: Review code to make sure encoding is correct - InputStream stream = atomReaderWriter.generateEntityEntry(op.getEntity()); - byte[] bytes = inputStreamToByteArray(stream); - String content = new String(bytes, "UTF-8"); - - StringBuilder sb = new StringBuilder(); - sb.append(String.format("POST %s HTTP/1.1\r\n", channel.resource(url).path(op.getTable()).getURI())); - sb.append(String.format("Content-ID: %d\r\n", contentId++)); - sb.append("Content-Type: application/atom+xml;type=entry\r\n"); - sb.append(String.format("Content-Length: %d\r\n", content.length())); - sb.append("\r\n"); - sb.append(content); - - bodyPartContent = sb.toString(); - } + private List<Entry> parseMimeMultipart(ClientResponse response, BatchOperations operations) { + List<DataSource> 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); + } - if (bodyPartContent != null) { - bodyPartContents.add(bodyPartContent); + List<Entry> result = new ArrayList<Entry>(); + 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<Header> e = headers.getAllHeaders(); + while (e.hasMoreElements()) { + Header header = e.nextElement(); + inBoundHeaders.putSingle(header.getName(), header.getValue()); } - } - return mimeReaderWriter.getMimeMultipart(bodyPartContents); + 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); + } } - catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); + + return result; + } + + private MimeMultipart createMimeMultipart(BatchOperations operations) { + List<DataSource> bodyPartContents = new ArrayList<DataSource>(); + int contentId = 1; + for (Operation operation : operations.getOperations()) { + + DataSource bodyPartContent = null; + // INSERT + if (operation instanceof InsertEntityOperation) { + InsertEntityOperation op = (InsertEntityOperation) operation; + + // + // Stream content into byte[] so that we have the length + // + InputStream stream = atomReaderWriter.generateEntityEntry(op.getEntity()); + 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)); + + //TODO: Review code to make sure encoding is correct + ByteArrayOutputStream httpRequest = new ByteArrayOutputStream(); + httpReaderWriter.appendMethod(httpRequest, "POST", channel.resource(url).path(op.getTable()).getURI()); + httpReaderWriter.appendHeaders(httpRequest, headers); + httpReaderWriter.appendEntity(httpRequest, new ByteArrayInputStream(bytes)); + + bodyPartContent = new InputStreamDataSource(new ByteArrayInputStream(httpRequest.toByteArray()), + "application/http"); + } + + if (bodyPartContent != null) { + bodyPartContents.add(bodyPartContent); + } } + + return mimeReaderWriter.getMimeMultipart(bodyPartContents); } private byte[] inputStreamToByteArray(InputStream inputStream) { 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 index 43c93c22b0f8..12eb2a98314e 100644 --- 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 @@ -15,39 +15,39 @@ public void setOperations(List<Operation> operations) { } public BatchOperations addInsertEntity(String table, Entity entity) { - this.operations.add(new InsertOperation().setTable(table).setEntity(entity)); + this.operations.add(new InsertEntityOperation().setTable(table).setEntity(entity)); return this; } public BatchOperations addUpdateEntity(String table, Entity entity) { - this.operations.add(new UpdateOperation().setTable(table).setEntity(entity)); + this.operations.add(new UpdateEntityOperation().setTable(table).setEntity(entity)); return this; } public BatchOperations addMergeEntity(String table, Entity entity) { - this.operations.add(new MergeOperation().setTable(table).setEntity(entity)); + this.operations.add(new MergeEntityOperation().setTable(table).setEntity(entity)); return this; } public BatchOperations addInsertOrReplaceEntity(String table, Entity entity) { - this.operations.add(new InsertOrReplaceOperation().setTable(table).setEntity(entity)); + this.operations.add(new InsertOrReplaceEntityOperation().setTable(table).setEntity(entity)); return this; } public BatchOperations addInsertOrMergeEntity(String table, Entity entity) { - this.operations.add(new InsertOrMergeOperation().setTable(table).setEntity(entity)); + this.operations.add(new InsertOrMergeEntityOperation().setTable(table).setEntity(entity)); return this; } public BatchOperations addDeleteEntity(String table, String partitionKey, String rowKey) { - this.operations.add(new DeleteOperation().setTable(table).setPartitionKey(partitionKey).setRowKey(rowKey)); + this.operations.add(new DeleteEntityOperation().setTable(table).setPartitionKey(partitionKey).setRowKey(rowKey)); return this; } public abstract class Operation { } - public class InsertOperation extends Operation { + public class InsertEntityOperation extends Operation { private String table; private Entity entity; @@ -55,7 +55,7 @@ public String getTable() { return table; } - public InsertOperation setTable(String table) { + public InsertEntityOperation setTable(String table) { this.table = table; return this; } @@ -64,13 +64,13 @@ public Entity getEntity() { return entity; } - public InsertOperation setEntity(Entity entity) { + public InsertEntityOperation setEntity(Entity entity) { this.entity = entity; return this; } } - public class UpdateOperation extends Operation { + public class UpdateEntityOperation extends Operation { private String table; private Entity entity; @@ -78,7 +78,7 @@ public String getTable() { return table; } - public UpdateOperation setTable(String table) { + public UpdateEntityOperation setTable(String table) { this.table = table; return this; } @@ -87,13 +87,13 @@ public Entity getEntity() { return entity; } - public UpdateOperation setEntity(Entity entity) { + public UpdateEntityOperation setEntity(Entity entity) { this.entity = entity; return this; } } - public class MergeOperation extends Operation { + public class MergeEntityOperation extends Operation { private String table; private Entity entity; @@ -101,7 +101,7 @@ public String getTable() { return table; } - public MergeOperation setTable(String table) { + public MergeEntityOperation setTable(String table) { this.table = table; return this; } @@ -110,13 +110,13 @@ public Entity getEntity() { return entity; } - public MergeOperation setEntity(Entity entity) { + public MergeEntityOperation setEntity(Entity entity) { this.entity = entity; return this; } } - public class InsertOrReplaceOperation extends Operation { + public class InsertOrReplaceEntityOperation extends Operation { private String table; private Entity entity; @@ -124,7 +124,7 @@ public String getTable() { return table; } - public InsertOrReplaceOperation setTable(String table) { + public InsertOrReplaceEntityOperation setTable(String table) { this.table = table; return this; } @@ -133,13 +133,13 @@ public Entity getEntity() { return entity; } - public InsertOrReplaceOperation setEntity(Entity entity) { + public InsertOrReplaceEntityOperation setEntity(Entity entity) { this.entity = entity; return this; } } - public class InsertOrMergeOperation extends Operation { + public class InsertOrMergeEntityOperation extends Operation { private String table; private Entity entity; @@ -147,7 +147,7 @@ public String getTable() { return table; } - public InsertOrMergeOperation setTable(String table) { + public InsertOrMergeEntityOperation setTable(String table) { this.table = table; return this; } @@ -156,13 +156,13 @@ public Entity getEntity() { return entity; } - public InsertOrMergeOperation setEntity(Entity entity) { + public InsertOrMergeEntityOperation setEntity(Entity entity) { this.entity = entity; return this; } } - public class DeleteOperation extends Operation { + public class DeleteEntityOperation extends Operation { private String table; private String partitionKey; private String rowKey; @@ -171,7 +171,7 @@ public String getTable() { return table; } - public DeleteOperation setTable(String table) { + public DeleteEntityOperation setTable(String table) { this.table = table; return this; } @@ -180,7 +180,7 @@ public String getPartitionKey() { return partitionKey; } - public DeleteOperation setPartitionKey(String partitionKey) { + public DeleteEntityOperation setPartitionKey(String partitionKey) { this.partitionKey = partitionKey; return this; } @@ -189,7 +189,7 @@ public String getRowKey() { return rowKey; } - public DeleteOperation setRowKey(String rowKey) { + public DeleteEntityOperation setRowKey(String rowKey) { this.rowKey = rowKey; 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 index 462dc4d10ef6..30a0b3beebd9 100644 --- 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 @@ -1,5 +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<Entry> entries = new ArrayList<Entry>(); + + public List<Entry> getEntries() { + return entries; + } + + public BatchResult setEntries(List<Entry> 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/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java index c489c675735a..aadff9804481 100644 --- 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 @@ -30,6 +30,7 @@ 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.InsertEntity; import com.microsoft.windowsazure.services.table.models.DeleteEntityOptions; import com.microsoft.windowsazure.services.table.models.EdmType; import com.microsoft.windowsazure.services.table.models.Entity; @@ -650,5 +651,7 @@ public void batchWorks() throws Exception { // Assert assertNotNull(result); + assertEquals(1, result.getEntries().size()); + assertEquals(InsertEntity.class, result.getEntries().get(0).getClass()); } } From 34de8fec26ac99eac38bd4717d0f649fabefded1 Mon Sep 17 00:00:00 2001 From: Renaud Paquay <renaud.paquay@microsoft.com> Date: Fri, 13 Jan 2012 16:53:53 -0800 Subject: [PATCH 19/59] Additional test for "batch" operation --- .../table/TableServiceIntegrationTest.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) 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 index aadff9804481..8fd16a9e2bb6 100644 --- 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 @@ -55,6 +55,7 @@ public class TableServiceIntegrationTest extends IntegrationTestBase { 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 CREATABLE_TABLE_1; private static String CREATABLE_TABLE_2; //private static String CREATABLE_TABLE_3; @@ -84,6 +85,7 @@ public static void setup() throws Exception { TEST_TABLE_4 = testTables[3]; TEST_TABLE_5 = testTables[4]; TEST_TABLE_6 = testTables[5]; + TEST_TABLE_7 = testTables[6]; CREATABLE_TABLE_1 = creatableTables[0]; CREATABLE_TABLE_2 = creatableTables[1]; @@ -654,4 +656,58 @@ public void batchWorks() throws Exception { assertEquals(1, result.getEntries().size()); assertEquals(InsertEntity.class, result.getEntries().get(0).getClass()); } + + @Test + public void batchMultipleWorks() 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); + } + } } From 18c825525a7b8782f92d383e5d1eaf303c6bf4eb Mon Sep 17 00:00:00 2001 From: Renaud Paquay <renaud.paquay@microsoft.com> Date: Sat, 14 Jan 2012 11:24:48 -0800 Subject: [PATCH 20/59] Simple code refactorings --- .../table/implementation/TableRestProxy.java | 91 +++++++++---------- .../table/models/BatchOperations.java | 17 ++-- 2 files changed, 54 insertions(+), 54 deletions(-) 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 index 944bb8214424..a18ffe066a21 100644 --- 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 @@ -556,19 +556,61 @@ public BatchResult batch(BatchOperations operations, TableServiceOptions options WebResource.Builder builder = webResource.getRequestBuilder(); builder = addTableRequestHeaders(builder); - MimeMultipart entity = createMimeMultipart(operations); + MimeMultipart entity = createBatchRequestBody(operations); builder = builder.type(entity.getContentType()); ClientResponse response = builder.post(ClientResponse.class, entity); ThrowIfError(response); BatchResult result = new BatchResult(); - result.setEntries(parseMimeMultipart(response, operations)); + result.setEntries(parseBatchResponse(response, operations)); return result; } - private List<Entry> parseMimeMultipart(ClientResponse response, BatchOperations operations) { + private MimeMultipart createBatchRequestBody(BatchOperations operations) { + List<DataSource> bodyPartContents = new ArrayList<DataSource>(); + int contentId = 1; + for (Operation operation : operations.getOperations()) { + + DataSource bodyPartContent = null; + // INSERT + if (operation instanceof InsertEntityOperation) { + InsertEntityOperation op = (InsertEntityOperation) operation; + + // + // Stream content into byte[] so that we have the length + // + InputStream stream = atomReaderWriter.generateEntityEntry(op.getEntity()); + 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)); + + //TODO: Review code to make sure encoding is correct + ByteArrayOutputStream httpRequest = new ByteArrayOutputStream(); + httpReaderWriter.appendMethod(httpRequest, "POST", channel.resource(url).path(op.getTable()).getURI()); + httpReaderWriter.appendHeaders(httpRequest, headers); + httpReaderWriter.appendEntity(httpRequest, new ByteArrayInputStream(bytes)); + + bodyPartContent = new InputStreamDataSource(new ByteArrayInputStream(httpRequest.toByteArray()), + "application/http"); + } + + if (bodyPartContent != null) { + bodyPartContents.add(bodyPartContent); + } + } + + return mimeReaderWriter.getMimeMultipart(bodyPartContents); + } + + private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations operations) { List<DataSource> parts = mimeReaderWriter.parseParts(response.getEntityInputStream(), response.getHeaders() .getFirst("Content-Type")); @@ -627,48 +669,6 @@ else if (operation instanceof DeleteEntityOperation) { return result; } - private MimeMultipart createMimeMultipart(BatchOperations operations) { - List<DataSource> bodyPartContents = new ArrayList<DataSource>(); - int contentId = 1; - for (Operation operation : operations.getOperations()) { - - DataSource bodyPartContent = null; - // INSERT - if (operation instanceof InsertEntityOperation) { - InsertEntityOperation op = (InsertEntityOperation) operation; - - // - // Stream content into byte[] so that we have the length - // - InputStream stream = atomReaderWriter.generateEntityEntry(op.getEntity()); - 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)); - - //TODO: Review code to make sure encoding is correct - ByteArrayOutputStream httpRequest = new ByteArrayOutputStream(); - httpReaderWriter.appendMethod(httpRequest, "POST", channel.resource(url).path(op.getTable()).getURI()); - httpReaderWriter.appendHeaders(httpRequest, headers); - httpReaderWriter.appendEntity(httpRequest, new ByteArrayInputStream(bytes)); - - bodyPartContent = new InputStreamDataSource(new ByteArrayInputStream(httpRequest.toByteArray()), - "application/http"); - } - - if (bodyPartContent != null) { - bodyPartContents.add(bodyPartContent); - } - } - - return mimeReaderWriter.getMimeMultipart(bodyPartContents); - } - private byte[] inputStreamToByteArray(InputStream inputStream) { try { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); @@ -691,5 +691,4 @@ private byte[] inputStreamToByteArray(InputStream inputStream) { throw new RuntimeException(e); } } - } 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 index 12eb2a98314e..f60bfa8fb005 100644 --- 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 @@ -40,14 +40,15 @@ public BatchOperations addInsertOrMergeEntity(String table, Entity entity) { } public BatchOperations addDeleteEntity(String table, String partitionKey, String rowKey) { - this.operations.add(new DeleteEntityOperation().setTable(table).setPartitionKey(partitionKey).setRowKey(rowKey)); + this.operations + .add(new DeleteEntityOperation().setTable(table).setPartitionKey(partitionKey).setRowKey(rowKey)); return this; } - public abstract class Operation { + public static abstract class Operation { } - public class InsertEntityOperation extends Operation { + public static class InsertEntityOperation extends Operation { private String table; private Entity entity; @@ -70,7 +71,7 @@ public InsertEntityOperation setEntity(Entity entity) { } } - public class UpdateEntityOperation extends Operation { + public static class UpdateEntityOperation extends Operation { private String table; private Entity entity; @@ -93,7 +94,7 @@ public UpdateEntityOperation setEntity(Entity entity) { } } - public class MergeEntityOperation extends Operation { + public static class MergeEntityOperation extends Operation { private String table; private Entity entity; @@ -116,7 +117,7 @@ public MergeEntityOperation setEntity(Entity entity) { } } - public class InsertOrReplaceEntityOperation extends Operation { + public static class InsertOrReplaceEntityOperation extends Operation { private String table; private Entity entity; @@ -139,7 +140,7 @@ public InsertOrReplaceEntityOperation setEntity(Entity entity) { } } - public class InsertOrMergeEntityOperation extends Operation { + public static class InsertOrMergeEntityOperation extends Operation { private String table; private Entity entity; @@ -162,7 +163,7 @@ public InsertOrMergeEntityOperation setEntity(Entity entity) { } } - public class DeleteEntityOperation extends Operation { + public static class DeleteEntityOperation extends Operation { private String table; private String partitionKey; private String rowKey; From 04cc6449fb846c8e8c73900ee6568799b21a2a23 Mon Sep 17 00:00:00 2001 From: Renaud Paquay <renaud.paquay@microsoft.com> Date: Sat, 14 Jan 2012 12:49:06 -0800 Subject: [PATCH 21/59] Support for all batch operations --- .../table/implementation/TableRestProxy.java | 121 ++++++++-- .../table/models/BatchOperations.java | 16 +- .../table/TableServiceIntegrationTest.java | 210 +++++++++++++++++- 3 files changed, 315 insertions(+), 32 deletions(-) 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 index a18ffe066a21..096c5b4ec312 100644 --- 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 @@ -18,6 +18,7 @@ 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; @@ -574,32 +575,41 @@ private MimeMultipart createBatchRequestBody(BatchOperations operations) { for (Operation operation : operations.getOperations()) { DataSource bodyPartContent = null; - // INSERT if (operation instanceof InsertEntityOperation) { InsertEntityOperation op = (InsertEntityOperation) operation; - - // - // Stream content into byte[] so that we have the length - // - InputStream stream = atomReaderWriter.generateEntityEntry(op.getEntity()); - 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)); - - //TODO: Review code to make sure encoding is correct - ByteArrayOutputStream httpRequest = new ByteArrayOutputStream(); - httpReaderWriter.appendMethod(httpRequest, "POST", channel.resource(url).path(op.getTable()).getURI()); - httpReaderWriter.appendHeaders(httpRequest, headers); - httpReaderWriter.appendEntity(httpRequest, new ByteArrayInputStream(bytes)); - - bodyPartContent = new InputStreamDataSource(new ByteArrayInputStream(httpRequest.toByteArray()), - "application/http"); + 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) { @@ -610,6 +620,69 @@ private MimeMultipart createBatchRequestBody(BatchOperations operations) { 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<Entry> parseBatchResponse(ClientResponse response, BatchOperations operations) { List<DataSource> parts = mimeReaderWriter.parseParts(response.getEntityInputStream(), response.getHeaders() .getFirst("Content-Type")); 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 index f60bfa8fb005..f1df873ac4da 100644 --- 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 @@ -39,9 +39,9 @@ public BatchOperations addInsertOrMergeEntity(String table, Entity entity) { return this; } - public BatchOperations addDeleteEntity(String table, String partitionKey, String rowKey) { - this.operations - .add(new DeleteEntityOperation().setTable(table).setPartitionKey(partitionKey).setRowKey(rowKey)); + 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; } @@ -167,6 +167,7 @@ public static class DeleteEntityOperation extends Operation { private String table; private String partitionKey; private String rowKey; + private String etag; public String getTable() { return table; @@ -194,5 +195,14 @@ 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/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/TableServiceIntegrationTest.java index 8fd16a9e2bb6..c8143349741d 100644 --- 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 @@ -30,7 +30,9 @@ 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; @@ -56,6 +58,7 @@ public class TableServiceIntegrationTest extends IntegrationTestBase { 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; @@ -86,6 +89,7 @@ public static void setup() throws Exception { 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]; @@ -635,20 +639,21 @@ public void queryEntitiesWithFilterWorks() throws Exception { } @Test - public void batchWorks() throws Exception { - System.out.println("batchWorks()"); + 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"; - Entity entity = new Entity().setPartitionKey(partitionKey).setRowKey("batchWorks") + + // 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()); - // Act BatchResult result = service.batch(new BatchOperations().addInsertEntity(table, entity)); // Assert @@ -658,7 +663,129 @@ public void batchWorks() throws Exception { } @Test - public void batchMultipleWorks() throws Exception { + 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 @@ -710,4 +837,77 @@ public void batchMultipleWorks() throws Exception { 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()); + } } From 87ebda2335c57b0e44b193021ecf4ea51bf5ca29 Mon Sep 17 00:00:00 2001 From: Louis DeJardin <lodejard@microsoft.com> Date: Mon, 13 Feb 2012 15:41:27 -0800 Subject: [PATCH 22/59] Commenting out an optional diagnostic line --- .../windowsazure/services/core/utils/pipeline/Exports.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java index f7aae2cd9188..234581b023fb 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java @@ -22,7 +22,6 @@ import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; -import com.sun.jersey.api.client.filter.LoggingFilter; public class Exports implements Builder.Exports { @@ -53,7 +52,7 @@ public Client create(String profile, Builder builder, Map<String, Object> proper public HttpURLConnectionClient create(String profile, Builder builder, Map<String, Object> properties) { ClientConfig clientConfig = (ClientConfig) properties.get("ClientConfig"); HttpURLConnectionClient client = HttpURLConnectionClient.create(clientConfig); - client.addFilter(new LoggingFilter()); + //client.addFilter(new LoggingFilter()); return client; } }); From 3a7d97c495f92ac8b0d13643f254ca912eb43c64 Mon Sep 17 00:00:00 2001 From: Joe Giardino <joegiard@microsoft.com> Date: Wed, 22 Feb 2012 13:23:01 -0800 Subject: [PATCH 23/59] =?UTF-8?q?Table=20Client=20commit=20=E2=80=A2=09the?= =?UTF-8?q?=20table=20client=20=E2=80=A2=09relevant=20core=20updates=20?= =?UTF-8?q?=E2=80=A2=09table=20tests=20=E2=80=A2=09pom.xml=20is=20updated?= =?UTF-8?q?=20to=20reference=20apache=20commons=20lang=20=E2=80=A2=09Updat?= =?UTF-8?q?ing=20useragent=20to=20v1.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- microsoft-azure-api/pom.xml | 5 + .../core/storage/AccessCondition.java | 9 +- .../core/storage/CloudStorageAccount.java | 21 + .../services/core/storage/Constants.java | 2 +- .../services/core/storage/ResultSegment.java | 10 +- .../services/core/storage/ServiceClient.java | 10 +- .../core/storage/StorageCredentials.java | 8 +- ...orageCredentialsSharedAccessSignature.java | 8 +- .../core/storage/StorageErrorCodeStrings.java | 5 + .../utils/implementation/ExecutionEngine.java | 13 + .../implementation/StorageErrorResponse.java | 1 + .../services/table/client/AtomPubParser.java | 562 +++++++ .../table/client/CloudTableClient.java | 1359 +++++++++++++++++ .../table/client/DynamicTableEntity.java | 104 ++ .../services/table/client/EdmType.java | 203 +++ .../services/table/client/EntityProperty.java | 496 ++++++ .../services/table/client/EntityResolver.java | 61 + .../services/table/client/Ignore.java | 37 + .../services/table/client/MimeHeader.java | 25 + .../services/table/client/MimeHelper.java | 510 +++++++ .../services/table/client/MimePart.java | 28 + .../services/table/client/ODataConstants.java | 183 +++ .../services/table/client/ODataPayload.java | 42 + .../services/table/client/PropertyPair.java | 277 ++++ .../table/client/QueryTableOperation.java | 268 ++++ .../services/table/client/StoreAs.java | 49 + .../table/client/TableBatchOperation.java | 514 +++++++ .../services/table/client/TableConstants.java | 143 ++ .../services/table/client/TableEntity.java | 170 +++ .../services/table/client/TableOperation.java | 699 +++++++++ .../table/client/TableOperationType.java | 43 + .../services/table/client/TableQuery.java | 773 ++++++++++ .../services/table/client/TableRequest.java | 432 ++++++ .../table/client/TableRequestOptions.java | 35 + .../services/table/client/TableResponse.java | 75 + .../services/table/client/TableResult.java | 179 +++ .../table/client/TableServiceEntity.java | 414 +++++ .../table/client/TableServiceException.java | 172 +++ .../table/client/TableUpdateType.java | 31 + .../client/TableBatchOperationTests.java | 762 +++++++++ .../table/client/TableClientTests.java | 268 ++++ .../table/client/TableEscapingTests.java | 231 +++ .../table/client/TableOperationTests.java | 591 +++++++ .../table/client/TableQueryTests.java | 427 ++++++ .../table/client/TableSerializerTests.java | 269 ++++ .../services/table/client/TableTestBase.java | 599 ++++++++ 46 files changed, 11099 insertions(+), 24 deletions(-) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/CloudTableClient.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/DynamicTableEntity.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EdmType.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimePart.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataConstants.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataPayload.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/PropertyPair.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/QueryTableOperation.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/StoreAs.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableBatchOperation.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableConstants.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableEntity.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperation.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperationType.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableQuery.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequest.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequestOptions.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResponse.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResult.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceEntity.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceException.java create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableUpdateType.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableBatchOperationTests.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableClientTests.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableEscapingTests.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableOperationTests.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableQueryTests.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableSerializerTests.java create mode 100644 microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableTestBase.java diff --git a/microsoft-azure-api/pom.xml b/microsoft-azure-api/pom.xml index 3e7dec871f27..d9b0c4c4f2cf 100644 --- a/microsoft-azure-api/pom.xml +++ b/microsoft-azure-api/pom.xml @@ -85,6 +85,11 @@ <artifactId>mail</artifactId> <version>1.4</version> </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>3.1</version> + </dependency> </dependencies> <build> 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. * <p> - * Setting this access condition modifies the request to include the HTTP <i>If-Match</i> 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 <i>If-Match</i> conditional header. If this + * access condition is set, the operation is performed only if the ETag of the resource matches the specified ETag. * <p> * For more information, see <a href= 'http://go.microsoft.com/fwlink/?LinkID=224642&clcid=0x409'>Specifying * Conditional Headers for Blob Service Operations</a>. @@ -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. * <p> - * Setting this access condition modifies the request to include the HTTP <i>If-None-Match</i> 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 <i>If-None-Match</i> 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. * <p> * For more information, see <a href= 'http://go.microsoft.com/fwlink/?LinkID=224642&clcid=0x409'>Specifying diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java index 8818215a944e..8beb56163e4c 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java @@ -24,6 +24,7 @@ import com.microsoft.windowsazure.services.blob.client.CloudBlobClient; import com.microsoft.windowsazure.services.core.storage.utils.Utility; import com.microsoft.windowsazure.services.queue.client.CloudQueueClient; +import com.microsoft.windowsazure.services.table.client.CloudTableClient; /** * Represents a Windows Azure storage account. @@ -538,6 +539,26 @@ public CloudQueueClient createCloudQueueClient() { return new CloudQueueClient(this.getQueueEndpoint(), this.getCredentials()); } + /** + * Creates a new table service client. + * + * @return A client object that uses the Table service endpoint. + */ + public CloudTableClient createCloudTableClient() { + if (this.getTableEndpoint() == null) { + throw new IllegalArgumentException("No table endpoint configured."); + } + + if (this.credentials == null) { + throw new IllegalArgumentException("No credentials provided."); + } + + if (!this.credentials.canCredentialsSignRequest()) { + throw new IllegalArgumentException("CloudTableClient requires a credential that can sign request"); + } + return new CloudTableClient(this.getTableEndpoint(), this.getCredentials()); + } + /** * Returns the endpoint for the Blob service, as configured for the storage account. This method is not supported * when using shared access signature credentials. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java index 492320796e70..7676efe058da 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java @@ -287,7 +287,7 @@ public static class HeaderConstants { /** * Specifies the value to use for UserAgent header. */ - public static final String USER_AGENT_VERSION = "Client v0.1.1"; + public static final String USER_AGENT_VERSION = "Client v0.1.2"; } /** diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java index debb82c4c6a8..39d2c8b73706 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java @@ -39,9 +39,9 @@ public class ResultSegment<T> { private final int pageSize; /** - * Holds the Iterable collection of results. + * Holds the ArrayList of results. */ - private final Iterable<T> results; + private final ArrayList<T> results; /** * Reserved for internal use. Creates an instance of the <code>ResultSegment</code> class. @@ -115,11 +115,11 @@ public int getRemainingPageResults() { } /** - * Returns an enumerable set of results from the blob service. + * Returns an enumerable set of results from the service. * - * @return The results retrieved from the blob service. + * @return The results retrieved from the service. */ - public Iterable<T> getResults() { + public ArrayList<T> getResults() { return this.results; } } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java index e9d0b3f6ad37..baa4c1db0465 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java @@ -242,13 +242,13 @@ public void setRetryPolicyFactory(final RetryPolicyFactory retryPolicyFactory) { /** * Sets the timeout to use when making requests to the storage service. * <p> - * The server timeout interval begins at the time that the complete request has been received by the service, and - * the server begins processing the response. If the timeout interval elapses before the response is returned to the + * The server timeout interval begins at the time that the complete request has been received by the service, and the + * server begins processing the response. If the timeout interval elapses before the response is returned to the * client, the operation times out. The timeout interval resets with each retry, if the request is retried. * - * The default timeout interval for a request made via the service client is 90 seconds. You can change this value - * on the service client by setting this property, so that all subsequent requests made via the service client will - * use the new timeout interval. You can also change this value for an individual request, by setting the + * The default timeout interval for a request made via the service client is 90 seconds. You can change this value on + * the service client by setting this property, so that all subsequent requests made via the service client will use + * the new timeout interval. You can also change this value for an individual request, by setting the * {@link RequestOptions#timeoutIntervalInMs} property. * * If you are downloading a large blob, you should increase the value of the timeout beyond the default value. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java index 84d4c055976a..84b4c194ac8f 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java @@ -39,8 +39,8 @@ public abstract class StorageCredentials { * Either include an account name with an account key (specifying values for * {@link CloudStorageAccount#ACCOUNT_NAME_NAME} and {@link CloudStorageAccount#ACCOUNT_KEY_NAME} ), or a * shared access signature (specifying a value for - * {@link CloudStorageAccount#SHARED_ACCESS_SIGNATURE_NAME} ). If you use an account name and account - * key, do not include a shared access signature, and vice versa. + * {@link CloudStorageAccount#SHARED_ACCESS_SIGNATURE_NAME} ). If you use an account name and account key, + * do not include a shared access signature, and vice versa. * * @return A {@link StorageCredentials} object representing the storage credentials determined from the name/value * pairs. @@ -81,8 +81,8 @@ protected static StorageCredentials tryParseCredentials(final HashMap<String, St * @param connectionString * A <code>String</code> that contains the key/value pairs that represent the storage credentials. * <p> - * The format for the connection string is in the pattern "<i>keyname=value</i>". Multiple key/value - * pairs can be separated by a semi-colon, for example, "<i>keyname1=value1;keyname2=value2</i>". + * The format for the connection string is in the pattern "<i>keyname=value</i>". Multiple key/value pairs + * can be separated by a semi-colon, for example, "<i>keyname1=value1;keyname2=value2</i>". * * @return A {@link StorageCredentials} object representing the storage credentials determined from the connection * string. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java index c33ce78d6459..2f3e44a380ed 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java @@ -96,8 +96,8 @@ public String computeHmac256(final String value) { /** * Computes a signature for the specified string using the HMAC-SHA256 algorithm with the specified operation - * context. This is not a valid operation for objects of type <code>StorageCredentialsSharedAccessSignature</code> - * so the method merely returns <code>null</code>. + * context. This is not a valid operation for objects of type <code>StorageCredentialsSharedAccessSignature</code> so + * the method merely returns <code>null</code>. * * @param value * The UTF-8-encoded string to sign. @@ -130,8 +130,8 @@ public String computeHmac512(final String value) { /** * Computes a signature for the specified string using the HMAC-SHA512 algorithm with the specified operation - * context. This is not a valid operation for objects of type <code>StorageCredentialsSharedAccessSignature</code> - * so the method merely returns <code>null</code>. + * context. This is not a valid operation for objects of type <code>StorageCredentialsSharedAccessSignature</code> so + * the method merely returns <code>null</code>. * * @param value * The UTF-8-encoded string to sign. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java index 5e3742055426..624a31467311 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java @@ -188,6 +188,11 @@ public final class StorageErrorCodeStrings { */ public static final String SERVER_BUSY = "ServerBusy"; + /** + * Table Already Exists + */ + public static final String TABLE_ALREADY_EXISTS = "TableAlreadyExists"; + /** * One or more header values are not supported. */ diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java index 02e355447905..e30d5f1e4544 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.microsoft.windowsazure.services.core.storage.utils.implementation; import java.io.IOException; @@ -34,6 +35,7 @@ import com.microsoft.windowsazure.services.core.storage.RetryResult; import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.table.client.TableServiceException; /** * RESERVED FOR INTERNAL USE. A class that handles execution of StorageOperations and enforces retry policies. @@ -166,6 +168,17 @@ public static <CLIENT_TYPE, PARENT_TYPE, RESULT_TYPE> RESULT_TYPE executeWithRet setLastException(opContext, translatedException); throw translatedException; } + catch (final TableServiceException e) { + task.getResult().setStatusCode(e.getHttpStatusCode()); + task.getResult().setStatusMessage(e.getMessage()); + setLastException(opContext, e); + if (!e.isRetryable()) { + throw e; + } + else { + translatedException = e; + } + } catch (final StorageException e) { // Non Retryable, just throw // do not translate StorageException diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java index 049bf75757fd..90a2713c5793 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.microsoft.windowsazure.services.core.storage.utils.implementation; import java.io.InputStream; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java new file mode 100644 index 000000000000..4b462cb33413 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java @@ -0,0 +1,562 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.InputStream; +import java.io.OutputStream; +import java.text.ParseException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map.Entry; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.commons.lang3.StringEscapeUtils; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * Reserved for internal use. A class used to read and write Table entities in OData AtomPub format requests and + * responses. + * <p> + * For more information about OData, see the <a href="http://www.odata.org/">Open Data Protocol</a> website. For more + * information about the AtomPub format used in OData, see <a + * href="http://www.odata.org/developers/protocols/atom-format">OData Protocol Atom Format</a>. + */ +class AtomPubParser { + /** + * Reserved for internal use. A static factory method to construct an <code>XMLStreamWriter</code> instance based on + * the specified <code>OutputStream</code>. + * + * @param outStream + * The <code>OutputStream</code> instance to create an <code>XMLStreamWriter</code> on. + * @return + * An <code>XMLStreamWriter</code> instance based on the specified <code>OutputStream</code>. + * @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 <code>XMLStreamReader</code> on the input stream. + * @param clazzType + * The class type <code>T</code> implementing {@link TableEntity} for the entity returned. Set to + * <code>null</code> to ignore the returned entity and copy only response properties into the + * {@link TableResult} object. + * @param resolver + * An {@link EntityResolver} instance to project the entity into an instance of type <code>R</code>. Set + * to <code>null</code> to return the entity as an instance of the class type <code>T</code>. + * @param 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 <T extends TableEntity, R> TableResult parseEntity(final XMLStreamReader xmlr, + final Class<T> clazzType, final EntityResolver<R> 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 <code>InputStream</code> to read the data to parse from. + * @param clazzType + * The class type <code>T</code> implementing {@link TableEntity} for the entities returned. Set to + * <code>null</code> to ignore the returned entities and copy only response properties into the + * {@link TableResult} objects. + * @param resolver + * An {@link EntityResolver} instance to project the entities into instances of type <code>R</code>. Set + * to <code>null</code> to return the entities as instances of the class type <code>T</code>. + * @param 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 <T extends TableEntity, R> ODataPayload<?> parseResponse(final InputStream inStream, + final Class<T> clazzType, final EntityResolver<R> resolver, final OperationContext opContext) + throws XMLStreamException, ParseException, InstantiationException, IllegalAccessException, StorageException { + ODataPayload<T> corePayload = null; + ODataPayload<R> resolvedPayload = null; + ODataPayload<?> commonPayload = null; + + if (resolver != null) { + resolvedPayload = new ODataPayload<R>(); + commonPayload = resolvedPayload; + } + else { + corePayload = new ODataPayload<T>(); + 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 + * <code>XMLStreamReader</code> using the specified class type and optionally projects the entity result with the + * specified resolver into a {@link TableResult} object. + * + * @param xmlr + * The <code>XMLStreamReader</code> to read the data to parse from. + * @param httpStatusCode + * The HTTP status code returned with the operation response. + * @param clazzType + * The class type <code>T</code> implementing {@link TableEntity} for the entity returned. Set to + * <code>null</code> to ignore the returned entity and copy only response properties into the + * {@link TableResult} object. + * @param resolver + * An {@link EntityResolver} instance to project the entity into an instance of type <code>R</code>. Set + * to <code>null</code> to return the entitys as instance of the class type <code>T</code>. + * @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 <T extends TableEntity, R> TableResult parseSingleOpResponse(final XMLStreamReader xmlr, + final int httpStatusCode, final Class<T> clazzType, final EntityResolver<R> 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 <code>String</code> property names to {@link EntityProperty} data typed values. + * + * @param xmlr + * The <code>XMLStreamReader</code> to read the data from. + * @param opContext + * An {@link OperationContext} object used to track the execution of the operation. + * + * @return + * A <code>java.util.HashMap</code> containing a map of <code>String</code> 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<String, EntityProperty> readProperties(final XMLStreamReader xmlr, + final OperationContext opContext) throws XMLStreamException, ParseException { + int eventType = xmlr.getEventType(); + xmlr.require(XMLStreamConstants.START_ELEMENT, null, ODataConstants.PROPERTIES); + final HashMap<String, EntityProperty> properties = new HashMap<String, EntityProperty>(); + + 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 + * <code>true<code> and a reference to an entity within a table when <code>false</code>. + * @param xmlw + * The <code>XMLStreamWriter</code> 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<String, EntityProperty> 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<String, EntityProperty> 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 <code>OutputStream</code> 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 + * <code>true<code> and a reference to an entity within a table when <code>false</code>. + * @param outStream + * The <code>OutputStream</code> 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 <code>XMLStreamWriter</code> 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 + * <code>true<code> and a reference to an entity within a table when <code>false</code>. + * @param xmlw + * The <code>XMLStreamWriter</code> 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. + * <p> + * 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. + * <p> + * A Table service endpoint is the base URI for Table service resources, including the DNS name of the storage account: + * <br> + * <code>    http://<em>myaccount</em>.table.core.windows.net</code><br> + * For more information, see the MSDN topic <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179360.aspx">Addressing Table Service Resources</a>. + * <p> + * 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 <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/hh225339.aspx">Authenticating Access to Your Storage + * Account</a>. + * + */ +public final class CloudTableClient extends ServiceClient { + + /** + * Reserved for internal use. An {@link EntityResolver} that projects table entity data as a <code>String</code> + * containing the table name. + */ + private final EntityResolver<String> tableNameResolver = new EntityResolver<String>() { + @Override + public String resolve(String partitionKey, String rowKey, Date timeStamp, + HashMap<String, EntityProperty> properties, String etag) { + return properties.get(TableConstants.TABLE_NAME).getValueAsString(); + } + }; + + /** + * Initializes an instance of the {@link CloudTableClient} class using a Table service endpoint. + * <p> + * 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 <code>java.net.URI</code> 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 <code>java.net.URI</code> 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. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd135729.aspx">Create + * Table</a> REST API to create the specified table, using the Table service endpoint and storage account + * credentials of this instance. + * + * @param tableName + * A <code>String</code> 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}. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd135729.aspx">Create + * Table</a> 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 <code>String</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> 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. + * <p> + * This method first invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> REST API to determine if the table exists, and if not, invokes the <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd135729.aspx">Create Table</a> Storage Service REST + * API to create the specified table, using the Table service endpoint and storage account credentials of this + * instance. + * + * @param tableName + * A <code>String</code> object containing the name of the table to create. + * + * @return + * A value of <code>true</code> if the operation created a new table, otherwise <code>false</code>. + * + * @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}. + * <p> + * This method first invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> REST API to determine if the table exists, and if not, invokes the <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd135729.aspx">Create Table</a> 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 <code>String</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A value of <code>true</code> if the operation created a new table, otherwise <code>false</code>. + * + * @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. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179387.aspx">Delete + * Table</a> 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 <code>String</code> 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}. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179387.aspx">Delete + * Table</a> 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 <code>String</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> 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. + * <p> + * This method first invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> REST API to determine if the table exists, and if so, invokes the <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179387.aspx">Delete Table</a> 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 <code>String</code> object containing the name of the table to delete. + * + * @return + * A value of <code>true</code> if the operation deleted an existing table, otherwise <code>false</code>. + * + * @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}. + * <p> + * This method first invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> REST API to determine if the table exists, and if so, invokes the <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179387.aspx">Delete Table</a> 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 <code>String</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A value of <code>true</code> if the operation deleted an existing table, otherwise <code>false</code>. + * + * @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. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> REST API to determine if the table exists, using the Table service endpoint and storage account + * credentials of this instance. + * + * @param tableName + * A <code>String</code> object containing the name of the table to find. + * + * @return + * A value of <code>true</code> if the table exists, otherwise <code>false</code>. + * + * @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}. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> 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 <code>String</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A value of <code>true</code> if the table exists, otherwise <code>false</code>. + * + * @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. + * <p> + * This method invokes an <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd894038.aspx">Entity Group + * Transaction</a> 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 <code>String</code> 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 <code>java.util.ArrayList</code> 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<TableResult> 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. + * <p> + * This method invokes an <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd894038.aspx">Entity Group + * Transaction</a> 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 <code>String</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A <code>java.util.ArrayList</code> 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<TableResult> 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. + * <p> + * This method will invoke the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> to execute the specified operation on the table, using the Table service endpoint and + * storage account credentials of this instance. + * + * @param tableName + * A <code>String</code> 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}. + * <p> + * This method will invoke the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>String</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> 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. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>R</code>. + * + * @return + * A collection implementing the <code>Iterable</code> interface containing the projection into type + * <code>R</code> of the results of executing the query. + */ + @DoesServiceRequest + public <R> Iterable<R> execute(final TableQuery<?> query, final EntityResolver<R> 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}. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>R</code>. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. Specify <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A collection implementing the <code>Iterable</code> interface containing the projection into type + * <code>R</code> of the results of executing the query. + */ + @DoesServiceRequest + @SuppressWarnings("unchecked") + public <R> Iterable<R> execute(final TableQuery<?> query, final EntityResolver<R> resolver, + final TableRequestOptions options, final OperationContext opContext) { + Utility.assertNotNull("query", query); + Utility.assertNotNull("Query requires a valid class type or resolver.", resolver); + return (Iterable<R>) this.generateIteratorForQuery(query, resolver, options, opContext); + } + + /** + * Executes a query. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>Iterable</code> interface specialized for type T of the results of + * executing the query. + */ + @DoesServiceRequest + public <T extends TableEntity> Iterable<T> execute(final TableQuery<T> query) { + return this.execute(query, null, null); + } + + /** + * Executes a query, using the specified {@link TableRequestOptions} and {@link OperationContext}. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A collection implementing the <code>Iterable</code> interface specialized for type T of the results of + * executing the query. + */ + @SuppressWarnings("unchecked") + @DoesServiceRequest + public <T extends TableEntity> Iterable<T> execute(final TableQuery<T> query, final TableRequestOptions options, + final OperationContext opContext) { + Utility.assertNotNull("query", query); + return (Iterable<T>) 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 <code>executeSegmented</code> allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>R</code>. + * @param continuationToken + * A {@link ResultContinuation} object representing a continuation token from the server when the + * operation returns a partial result. Specify <code>null</code> 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 <code>R</code> 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 <R> ResultSegment<R> executeSegmented(final TableQuery<?> query, final EntityResolver<R> 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 <code>executeSegmented</code> allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>R</code>. + * @param continuationToken + * A {@link ResultContinuation} object representing a continuation token from the server when the + * operation returns a partial result. Specify <code>null</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A {@link ResultSegment} containing the projection into type <code>R</code> 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 <R> ResultSegment<R> executeSegmented(final TableQuery<?> query, final EntityResolver<R> 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<R>) this + .executeQuerySegmentedImpl(query, resolver, continuationToken, options, opContext); + } + + /** + * Executes a query in segmented mode with a {@link ResultContinuation} continuation token. + * Executing a query with <code>executeSegmented</code> allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>null</code> 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 <T extends TableEntity> ResultSegment<T> executeSegmented(final TableQuery<T> 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 <code>executeSegmented</code> allows the query to be resumed after returning partial + * results, using information returned by the server in the {@link ResultSegment} object. + * <p> + * This method will invoke a <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179421.aspx">Query + * Entities</a> operation on the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179423.aspx">Table + * Service REST API</a> 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 <code>null</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> 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 <T extends TableEntity> ResultSegment<T> executeSegmented(final TableQuery<T> query, + final ResultContinuation continuationToken, final TableRequestOptions options, + final OperationContext opContext) throws IOException, URISyntaxException, StorageException { + Utility.assertNotNull("query", query); + return (ResultSegment<T>) this.executeQuerySegmentedImpl(query, null, continuationToken, options, opContext); + } + + /** + * Lists the table names in the storage account. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> REST API to list the table names, using the Table service endpoint and storage account credentials of + * this instance. + * + * @return + * An <code>Iterable</code> collection of the table names in the storage account. + */ + @DoesServiceRequest + public Iterable<String> listTables() { + return this.listTables(null); + } + + /** + * Lists the table names in the storage account that match the specified prefix. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> 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 <String> containing the prefix to match on table names to return. + * + * @return + * An <code>Iterable</code> collection of the table names in the storage account that match the specified + * prefix. + */ + @DoesServiceRequest + public Iterable<String> 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}. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> 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 <String> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * An <code>Iterable</code> collection of the table names in the storage account that match the specified + * prefix. + */ + @DoesServiceRequest + public Iterable<String> 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. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> REST API to list the table names, using the Table service endpoint and storage account credentials of + * this instance. + * + * @param prefix + * A <String> containing the prefix to match on table names to return. + * + * @return + * A {@link ResultSegment} of <code>String</code> 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<String> 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. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> 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 <String> containing the prefix to match on table names to return. + * + * @return + * A {@link ResultSegment} of <code>String</code> 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<String> 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. + * <p> + * This method invokes the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179405.aspx">Query + * Tables</a> 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 <String> 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 <code>null</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * A {@link ResultSegment} of <code>String</code> 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<String> 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 <String> 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<TableServiceEntity> generateListTablesQuery(final String prefix) { + TableQuery<TableServiceEntity> listQuery = TableQuery.<TableServiceEntity> 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 + * <code>R</code>. Pass <code>null</code> 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 + * <code>null</code> 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 <code>XMLStreamReader</code>. + * @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 <T extends TableEntity, R> ResultSegment<?> executeQuerySegmentedCore(final TableQuery<T> queryToExecute, + final EntityResolver<R> 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<T> clazzResponse = null; + ODataPayload<R> resolvedResponse = null; + + InputStream inStream = queryRequest.getInputStream(); + + try { + if (resolver == null) { + clazzResponse = (ODataPayload<T>) AtomPubParser.parseResponse(inStream, queryToExecute.getClazzType(), + null, opContext); + } + else { + resolvedResponse = (ODataPayload<R>) AtomPubParser.parseResponse(inStream, + queryToExecute.getClazzType(), resolver, opContext); + } + } + finally { + inStream.close(); + } + + final ResultContinuation nextToken = TableResponse.getTableContinuationFromResponse(queryRequest); + + if (resolver == null) { + return new ResultSegment<T>(clazzResponse.results, + queryToExecute.getTakeCount() == null ? clazzResponse.results.size() + : queryToExecute.getTakeCount(), nextToken); + } + else { + return new ResultSegment<R>(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 <code>R</code>. Pass <code>null</code> 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 + * <code>null</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> 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 <T extends TableEntity, R> ResultSegment<?> executeQuerySegmentedImpl(final TableQuery<T> queryToExecute, + final EntityResolver<R> 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<CloudTableClient, TableQuery<T>, ResultSegment<?>> impl = new StorageOperation<CloudTableClient, TableQuery<T>, ResultSegment<?>>( + options) { + @Override + public ResultSegment<?> execute(final CloudTableClient client, final TableQuery<T> 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 <code>R</code>. Pass <code>null</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * @return + * An instance of <code>Iterable</code> specialized for the {@link TableEntity} or {@link EntityResolver} + * type returned by the query. + */ + protected <T extends TableEntity, R> Iterable<?> generateIteratorForQuery(final TableQuery<T> queryRef, + final EntityResolver<R> 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<CloudTableClient, TableQuery<T>, ResultSegment<T>> impl = new SegmentedStorageOperation<CloudTableClient, TableQuery<T>, ResultSegment<T>>( + options, null) { + @Override + public ResultSegment<T> execute(final CloudTableClient client, final TableQuery<T> queryToExecute, + final OperationContext opContext) throws Exception { + + @SuppressWarnings("unchecked") + final ResultSegment<T> result = (ResultSegment<T>) 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<CloudTableClient, TableQuery<T>, T>(impl, this, queryRef, + options.getRetryPolicyFactory(), opContext); + } + else { + final SegmentedStorageOperation<CloudTableClient, TableQuery<T>, ResultSegment<R>> impl = new SegmentedStorageOperation<CloudTableClient, TableQuery<T>, ResultSegment<R>>( + options, null) { + @Override + public ResultSegment<R> execute(final CloudTableClient client, final TableQuery<T> queryToExecute, + final OperationContext opContext) throws Exception { + + @SuppressWarnings("unchecked") + final ResultSegment<R> result = (ResultSegment<R>) 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<CloudTableClient, TableQuery<T>, 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<String, EntityProperty> properties = new HashMap<String, EntityProperty>(); + + /** + * Nullary default constructor. + */ + public DynamicTableEntity() { + // empty ctor + } + + /** + * Constructs a {@link DynamicTableEntity} instance using the specified property map. + * + * @param properties + * A <code>java.util.HashMap</code> containing a map of <code>String</code> property names to + * {@link EntityProperty} data typed values to store in the new {@link DynamicTableEntity}. + */ + public DynamicTableEntity(final HashMap<String, EntityProperty> properties) { + this.setProperties(properties); + } + + /** + * Gets the property map for this {@link DynamicTableEntity} instance. + * + * @return + * A <code>java.util.HashMap</code> containing the map of <code>String</code> property names to + * {@link EntityProperty} data typed values for this {@link DynamicTableEntity} instance. + */ + public HashMap<String, EntityProperty> 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 <code>java.util.HashMap</code> of <code>String</code> 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<String, EntityProperty> properties, final OperationContext opContext) { + this.setProperties(properties); + } + + /** + * Sets the property map for this {@link DynamicTableEntity} instance. + * + * @param properties + * A <code>java.util.HashMap</code> containing the map of <code>String</code> property names to + * {@link EntityProperty} data typed values to set in this {@link DynamicTableEntity} instance. + */ + public void setProperties(final HashMap<String, EntityProperty> 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 <code>java.util.HashMap</code> containing the map of <code>String</code> 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<String, EntityProperty> 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. + * <p> + * For more information about OData, see the <a href="http://www.odata.org/">Open Data Protocol</a> website. + * <p> + * For an overview of the available EDM primitive data types and names, see the <a + * href="http://www.odata.org/developers/protocols/overview#AbstractTypeSystem">Primitive Data Types</a> section of the + * <a href="http://www.odata.org/developers/protocols/overview">OData Protocol Overview</a>. + * <p> + * The Abstract Type System used to define the primitive types supported by OData is defined in detail in <a + * href="http://msdn.microsoft.com/en-us/library/dd541474.aspx">[MC-CSDL] (section 2.2.1). + */ +public enum EdmType { + /** + * <strong>Null</strong> Represents the absence of a value + */ + NULL, + + /** + * <strong>Edm.Binary</strong> Represents fixed- or variable-length binary data + */ + BINARY, + + /** + * <strong>Edm.Boolean</strong> Represents the mathematical concept of binary-valued logic + */ + BOOLEAN, + + /** + * <strong>Edm.Byte</strong> Represents a unsigned 8-bit integer value + */ + BYTE, + + /** + * <strong>Edm.DateTime</strong> Represents date and time with values ranging from 12:00:00 midnight, January 1, + * 1753 A.D. through 11:59:59 P.M, December 9999 A.D. + */ + DATE_TIME, + + /** + * <strong>Edm.Decimal</strong> Represents numeric values with fixed precision and scale. This type can describe a + * numeric value ranging from negative 10^255 + 1 to positive 10^255 -1 + */ + DECIMAL, + + /** + * <strong>Edm.Double</strong> Represents a floating point number with 15 digits precision that can represent values + * with approximate range of +/- 2.23e -308 through +/- 1.79e +308 + */ + DOUBLE, + + /** + * <strong>Edm.Single</strong> Represents a floating point number with 7 digits precision that can represent values + * with approximate range of +/- 1.18e -38 through +/- 3.40e +38 + */ + SINGLE, + + /** + * <strong>Edm.Guid</strong> Represents a 16-byte (128-bit) unique identifier value + */ + GUID, + + /** + * <strong>Edm.Int16</strong> Represents a signed 16-bit integer value + */ + INT16, + + /** + * <strong>Edm.Int32</strong> Represents a signed 32-bit integer value + */ + INT32, + + /** + * <strong>Edm.Int64</strong> Represents a signed 64-bit integer value + */ + INT64, + + /** + * <strong>Edm.SByte</strong> Represents a signed 8-bit integer value + */ + SBYTE, + + /** + * <strong>Edm.String</strong> Represents fixed- or variable-length character data + */ + STRING, + + /** + * <strong>Edm.Time</strong> Represents the time of day with values ranging from 0:00:00.x to 23:59:59.y, where x + * and y depend upon the precision + */ + TIME, + + /** + * <strong>Edm.DateTimeOffset</strong> Represents date and time as an Offset in minutes from GMT, with values + * ranging from 12:00:00 midnight, January 1, 1753 A.D. through 11:59:59 P.M, December 9999 A.D + */ + DATE_TIME_OFFSET; + + /** + * Parses an EDM data type name and return the matching {@link EdmType} enumeration value. A <code>null</code> or + * empty value parameter is matched as {@link EdmType#STRING}. Note that only the subset of EDM data types + * supported in Windows Azure Table storage is parsed, consisting of {@link #BINARY}, {@link #BOOLEAN}, + * {@link #BYTE} , {@link #DATE_TIME}, {@link #DOUBLE}, {@link #GUID}, {@link #INT32}, {@link #INT64}, and + * {@link #STRING}. Any other type will cause an {@link IllegalArgumentException} to be thrown. + * + * @param value + * A <code>String</code> containing the name of an EDM data type. + * @return + * The {@link EdmType} enumeration value matching the specified EDM data type. + * @throws IllegalArgumentException + * if an EDM data type not supported in Windows Azure Table storage is passed as an argument. + * + */ + public static EdmType parse(final String value) { + if (value == null || value.length() == 0) { + return EdmType.STRING; + } + else if (value.equals(ODataConstants.EDMTYPE_DATETIME)) { + return EdmType.DATE_TIME; + } + else if (value.equals(ODataConstants.EDMTYPE_INT32)) { + return EdmType.INT32; + } + else if (value.equals(ODataConstants.EDMTYPE_BOOLEAN)) { + return EdmType.BOOLEAN; + } + else if (value.equals(ODataConstants.EDMTYPE_DOUBLE)) { + return EdmType.DOUBLE; + } + else if (value.equals(ODataConstants.EDMTYPE_INT64)) { + return EdmType.INT64; + } + else if (value.equals(ODataConstants.EDMTYPE_GUID)) { + return EdmType.GUID; + } + else if (value.equals(ODataConstants.EDMTYPE_BINARY)) { + return EdmType.BINARY; + } + + throw new IllegalArgumentException("Invalid value for edmtype: ".concat(value)); + } + + /** + * Returns the name of the EDM data type corresponding to the enumeration value. + * + * @return + * A <code>String</code> containing the name of the EDM data type. + */ + @Override + public String toString() { + if (this == EdmType.BINARY) { + return ODataConstants.EDMTYPE_BINARY; + } + else if (this == EdmType.STRING) { + return Constants.EMPTY_STRING; + } + else if (this == EdmType.BOOLEAN) { + return ODataConstants.EDMTYPE_BOOLEAN; + } + else if (this == EdmType.DOUBLE) { + return ODataConstants.EDMTYPE_DOUBLE; + } + else if (this == EdmType.GUID) { + return ODataConstants.EDMTYPE_GUID; + } + else if (this == EdmType.INT32) { + return ODataConstants.EDMTYPE_INT32; + } + else if (this == EdmType.INT64) { + return ODataConstants.EDMTYPE_INT64; + } + else if (this == EdmType.DATE_TIME) { + return ODataConstants.EDMTYPE_DATETIME; + } + else { + // VNext : Update here if we add to supported edmtypes in the future. + return Constants.EMPTY_STRING; + } + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java new file mode 100644 index 000000000000..b449c874abb8 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java @@ -0,0 +1,496 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.text.ParseException; +import java.util.Date; +import java.util.UUID; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.utils.Base64; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * A class which represents a single typed property value in a table entity. An {@link EntityProperty} stores the data + * type as an {@link EdmType}. The value, which may be <code>null</code> for object types, but not for primitive types, + * is serialized and stored as a <code>String</code>. + * <p> + * {@link EntityProperty} provides overloaded constructors and overloads of the <code>setValue</code> method for + * supported value types. Each overloaded constructor or <code>setValue</code> method sets the {@link EdmType} and + * serializes the value appropriately based on the parameter type. + * <p> + * Use one of the <code>getValueAs</code><em>Type</em> methods to deserialize an {@link EntityProperty} as the + * appropriate Java type. The method will throw a {@link ParseException} or {@link IllegalArgumentException} if the + * {@link EntityProperty} cannot be deserialized as the Java type. + */ +public final class EntityProperty { + private String value; + private EdmType edmType = EdmType.NULL; + private boolean isNull = false; + + /** + * Constructs an {@link EntityProperty} instance from a <code>boolean</code> value. + * + * @param value + * The <code>boolean</code> value of the entity property to set. + */ + public EntityProperty(final boolean value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a <code>byte[]</code> value. + * + * @param value + * The <code>byte[]</code> value of the entity property to set. + */ + public EntityProperty(final byte[] value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a <code>Byte[]</code>. + * + * @param value + * The <code>Byte[]</code> to set as the entity property value. + */ + public EntityProperty(final Byte[] value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a <code>Date</code> value. + * + * @param value + * The <code>Date</code> to set as the entity property value. + */ + public EntityProperty(final Date value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a <code>double</code> value. + * + * @param value + * The <code>double</code> value of the entity property to set. + */ + public EntityProperty(final double value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from an <code>int</code> value. + * + * @param value + * The <code>int</code> value of the entity property to set. + */ + public EntityProperty(final int value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a <code>long</code> value. + * + * @param value + * The <code>long</code> value of the entity property to set. + */ + public EntityProperty(final long value) { + this.setValue(value); + } + + /** + * Constructs an {@link EntityProperty} instance from a <code>String</code> value. + * + * @param value + * The <code>String</code> to set as the entity property value. + */ + public EntityProperty(final String value) { + this.setValue(value); + } + + /** + * Reserved for internal use. Constructs an {@link EntityProperty} instance from a <code>String</code> value and a + * data type, and verifies that the value can be interpreted as the specified data type. + * + * @param value + * The <code>String</code> representation of the value to construct. + * @param edmType + * The {@link EdmType} data type of the value to construct. + * @throws ParseException + * if the <code>String</code> representation of the value cannot be interpreted as the data type. + */ + protected EntityProperty(final String value, final EdmType edmType) throws ParseException { + this.edmType = edmType; + this.value = value; + + // validate data is encoded correctly + if (edmType == EdmType.STRING) { + return; + } + else if (edmType == EdmType.BINARY) { + this.getValueAsByteArray(); + } + else if (edmType == EdmType.BOOLEAN) { + this.getValueAsBoolean(); + } + else if (edmType == EdmType.DOUBLE) { + this.getValueAsDouble(); + } + else if (edmType == EdmType.GUID) { + this.getValueAsUUID(); + } + else if (edmType == EdmType.INT32) { + this.getValueAsInteger(); + } + else if (edmType == EdmType.INT64) { + this.getValueAsLong(); + } + else if (edmType == EdmType.DATE_TIME) { + this.getValueAsDate(); + } + } + + /** + * Constructs an {@link EntityProperty} instance from a <code>java.util.UUID</code> value. + * + * @param value + * The <code>java.util.UUID</code> to set as the entity property value. + */ + public EntityProperty(final UUID value) { + this.setValue(value); + } + + /** + * Reserved for internal use. Constructs an {@link EntityProperty} instance as a <code>null</code> value with the + * specified type. + * + * @param type + * The {@link EdmType} to set as the entity property type. + */ + protected EntityProperty(EdmType type) { + this.value = null; + this.edmType = type; + this.isNull = true; + } + + /** + * Gets the {@link EdmType} storage data type for the {@link EntityProperty}. + * + * @return + * The {@link EdmType} enumeration value for the data type of the {@link EntityProperty}. + */ + public EdmType getEdmType() { + return this.edmType; + } + + /** + * Gets a flag indicating that the {@link EntityProperty} value is <code>null</code>. + * + * @return + * A <code>boolean</code> flag indicating that the {@link EntityProperty} value is <code>null</code>. + */ + public boolean getIsNull() { + return this.isNull; + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>boolean</code>. + * + * @return + * A <code>boolean</code> representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is <code>null</code> or cannot be parsed as a <code>boolean</code>. + */ + public boolean getValueAsBoolean() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Boolean.parseBoolean(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>byte</code> array. + * + * @return + * A <code>byte[]</code> representation of the {@link EntityProperty} value, or <code>null</code>. + */ + public byte[] getValueAsByteArray() { + return this.isNull ? null : Base64.decode(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>Byte</code> array. + * + * @return + * A <code>Byte[]</code> representation of the {@link EntityProperty} value, or <code>null</code>. + */ + public Byte[] getValueAsByteObjectArray() { + return this.isNull ? null : Base64.decodeAsByteObjectArray(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>Date</code>. + * + * @return + * A <code>Date</code> representation of the {@link EntityProperty} value, or <code>null</code>. + * + * @throws IllegalArgumentException + * if the value is not <code>null</code> and cannot be parsed as a <code>Date</code>. + */ + public Date getValueAsDate() { + if (this.isNull) { + return null; + } + + return Utility.parseDate(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>double</code>. + * + * @return + * A <code>double</code> representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is <code>null</code> or cannot be parsed as a <code>double</code>. + */ + public double getValueAsDouble() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Double.parseDouble(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as an <code>int</code>. + * + * @return + * An <code>int</code> representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is <code>null</code> or cannot be parsed as an <code>int</code>. + */ + public int getValueAsInteger() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Integer.parseInt(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>long</code>. + * + * @return + * A <code>long</code> representation of the {@link EntityProperty} value. + * + * @throws IllegalArgumentException + * if the value is <code>null</code> or cannot be parsed as a <code>long</code>. + */ + public long getValueAsLong() { + if (this.isNull) { + throw new IllegalArgumentException("EntityProperty cannot be set to null for value types."); + } + return Long.parseLong(this.value); + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>String</code>. + * + * @return + * A <code>String</code> representation of the {@link EntityProperty} value, or <code>null</code>. + */ + public String getValueAsString() { + return this.isNull ? null : this.value; + } + + /** + * Gets the value of this {@link EntityProperty} as a <code>java.util.UUID</code>. + * + * @return + * A <code>java.util.UUID</code> representation of the {@link EntityProperty} value, or <code>null</code>. + * + * @throws IllegalArgumentException + * if the value cannot be parsed as a <code>java.util.UUID</code>. + */ + public UUID getValueAsUUID() { + return this.isNull ? null : UUID.fromString(this.value); + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>boolean</code> value. + * + * @param value + * The <code>boolean</code> value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final boolean value) { + this.edmType = EdmType.BOOLEAN; + this.isNull = false; + this.value = value ? Constants.TRUE : Constants.FALSE; + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>byte[]</code> value. + * + * @param value + * The <code>byte[]</code> value to set as the {@link EntityProperty} value. This value may be + * <code>null</code>. + */ + public synchronized final void setValue(final byte[] value) { + this.edmType = EdmType.BINARY; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = Base64.encode(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>Byte[]</code> value. + * + * @param value + * The <code>Byte[]</code> value to set as the {@link EntityProperty} value. This value may be + * <code>null</code>. + */ + public synchronized final void setValue(final Byte[] value) { + this.edmType = EdmType.BINARY; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = Base64.encode(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>Date</code> value. + * + * @param value + * The <code>Date</code> value to set as the {@link EntityProperty} value. This value may be + * <code>null</code>. + */ + public synchronized final void setValue(final Date value) { + this.edmType = EdmType.DATE_TIME; + + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = Utility.getTimeByZoneAndFormat(value, Utility.UTC_ZONE, Utility.ISO8061_LONG_PATTERN); + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>double</code> value. + * + * @param value + * The <code>double</code> value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final double value) { + this.edmType = EdmType.DOUBLE; + this.isNull = false; + this.value = Double.toString(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>int</code> value. + * + * @param value + * The <code>int</code> value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final int value) { + this.edmType = EdmType.INT32; + this.isNull = false; + this.value = Integer.toString(value); + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>long</code> value. + * + * @param value + * The <code>long</code> value to set as the {@link EntityProperty} value. + */ + public synchronized final void setValue(final long value) { + this.edmType = EdmType.INT64; + this.isNull = false; + this.value = Long.toString(value); + } + + /** + * Sets this {@link EntityProperty} using the <code>String</code> value. + * + * @param value + * The <code>String</code> value to set as the {@link EntityProperty} value. This value may be + * <code>null</code>. + */ + public synchronized final void setValue(final String value) { + this.edmType = EdmType.STRING; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = value; + } + + /** + * Sets this {@link EntityProperty} using the serialized <code>java.util.UUID</code> value. + * + * @param value + * The <code>java.util.UUID</code> value to set as the {@link EntityProperty} value. + * This value may be <code>null</code>. + */ + public synchronized final void setValue(final UUID value) { + this.edmType = EdmType.GUID; + if (value == null) { + this.value = null; + this.isNull = true; + return; + } + else { + this.isNull = false; + } + + this.value = value.toString(); + } + + /** + * Reserved for internal use. Sets the null value flag to the specified <code>boolean</code> value. + * + * @param isNull + * The <code>boolean</code> value to set in the null value flag. + */ + protected void setIsNull(final boolean isNull) { + this.isNull = isNull; + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java new file mode 100644 index 000000000000..1f7d95d3a812 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java @@ -0,0 +1,61 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.Date; +import java.util.HashMap; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * An interface to perform client side projection on a retrieved entity. An {@link EntityResolver} instance must + * implement a <code>resolve</code> method projecting the entity data represented by the parameters passed in as a new + * instance of the type specified by the type parameter. + * <p> + * This interface is useful for converting directly from table entity data to a client object type without requiring a + * separate table entity class type that deserializes every property individually. For example, a client can perform a + * client side projection of a <em>Customer</em> entity by simply returning the <code>String</code> for the + * <em>CustomerName</em> property of each entity. The result of this projection will be a collection of + * <code>String</code>s containing each customer name. + * + * @param <T> + * The type of the object that the resolver produces. + */ +public interface EntityResolver<T> { + /** + * Returns a reference to a new object instance of type <code>T</code> containing a projection of the specified + * table entity data. + * + * @param partitionKey + * A <code>String</code> containing the PartitionKey value for the entity. + * @param rowKey + * A <code>String</code> containing the RowKey value for the entity. + * @param timeStamp + * A <code>Date</code> containing the Timestamp value for the entity. + * @param properties + * The <code>java.util.HashMap</code> of <code>String</code> property names to {@link EntityProperty} + * data type and value pairs representing the table entity data. + * @param etag + * A <code>String</code> containing the Etag for the entity. + * @return + * A reference to an object instance of type <code>T</code> constructed as a projection of the table entity + * parameters. + * @throws StorageException + * if an error occurs during the operation. + */ + T resolve(String partitionKey, String rowKey, Date timeStamp, HashMap<String, EntityProperty> properties, + String etag) throws StorageException; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java new file mode 100644 index 000000000000..53653dc95150 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java @@ -0,0 +1,37 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation set on a method to prevent its use in serializing or deserializing a property by reflection. Apply the + * <code>@Ignore</code> annotation to methods in a class implementing {@link TableEntity} to force them to be ignored + * during reflection-based serialization and deserialization. See the documentation for {@link TableServiceEntity} for + * more information on using reflection-based serialization and deserialization. + * + * @see StoreAs + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface Ignore { + // No attributes +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java new file mode 100644 index 000000000000..bffc95cdd427 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java @@ -0,0 +1,25 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +/** + * Reserved for internal use. A class that represents a given MIME Header. + */ +class MimeHeader { + protected String boundary; + protected String contentType; + protected String contentTransferEncoding; + protected String subBoundary; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java new file mode 100644 index 000000000000..143a4022e374 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java @@ -0,0 +1,510 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.util.ArrayList; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * Reserved for internal use. A class used to read and write MIME requests and responses. + */ +class MimeHelper { + /** + * Reserved for internal use. A static factory method that generates a {@link StorageException} for invalid MIME + * responses. + * + * @return + * The {@link StorageException} for the invalid MIME response. + */ + protected static StorageException generateMimeParseException() { + return new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, "Invalid MIME response received.", + Constants.HeaderConstants.HTTP_UNUSED_306, null, null); + } + + /** + * Reserved for internal use. Returns the HTTP verb for a table operation. + * + * @param operation + * The {@link TableOperation} instance to get the HTTP verb for. + * @return + * A <code>String</code> containing the HTTP verb to use with the operation. + */ + protected static String getHttpVerbForOperation(final TableOperation operation) { + if (operation.getOperationType() == TableOperationType.INSERT) { + return "POST"; + } + else if (operation.getOperationType() == TableOperationType.DELETE) { + return "DELETE"; + } + else if (operation.getOperationType() == TableOperationType.MERGE + || operation.getOperationType() == TableOperationType.INSERT_OR_MERGE) { + return "MERGE"; + } + else if (operation.getOperationType() == TableOperationType.REPLACE + || operation.getOperationType() == TableOperationType.INSERT_OR_REPLACE) { + return "PUT"; + } + else if (operation.getOperationType() == TableOperationType.RETRIEVE) { + return "GET"; + } + else { + throw new IllegalArgumentException("Unknown table operation"); + } + } + + /** + * Reserved for internal use. Returns the next non-blank line from the {@link BufferedReader}. + * + * @param reader + * The {@link BufferedReader} to read lines from. + * @return + * A <code>String</code> containing the next non-blank line from the {@link BufferedReader}, or + * <code>null</code>. + * @throws IOException + * if an error occurs reading from the {@link BufferedReader}. + */ + protected static String getNextLineSkippingBlankLines(final BufferedReader reader) throws IOException { + String tString = null; + do { + tString = reader.readLine(); + } while (tString != null && tString.length() == 0); + + return tString; + } + + /** + * Reserved for internal use. Reads the response stream from a batch operation into an <code>ArrayList</code> of + * {@link MimePart} objects. + * + * @param inStream + * An {@link InputStream} containing the operation response stream. + * @param expectedBundaryName + * A <code>String</code> containing the MIME part boundary string. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * @return + * An <code>ArrayList</code> of {@link MimePart} objects parsed from the input stream. + * @throws IOException + * if an error occurs accessing the input stream. + * @throws StorageException + * if an error occurs parsing the input stream. + */ + protected static ArrayList<MimePart> readBatchResponseStream(final InputStream inStream, + final String expectedBundaryName, final OperationContext opContext) throws IOException, StorageException { + final ArrayList<MimePart> result = new ArrayList<MimePart>(); + final InputStreamReader streamReader = new InputStreamReader(inStream, "UTF-8"); + final BufferedReader reader = new BufferedReader(streamReader); + final String mungedExpectedBoundaryName = "--".concat(expectedBundaryName); + + final MimeHeader docHeader = readMimeHeader(reader, opContext); + if (docHeader.boundary == null || !docHeader.boundary.equals(mungedExpectedBoundaryName)) { + throw generateMimeParseException(); + } + + MimeHeader currHeader = null; + + // No explicit changeset present + if (docHeader.subBoundary == null) { + do { + result.add(readMimePart(reader, docHeader.boundary, opContext)); + currHeader = readMimeHeader(reader, opContext); + } while (currHeader != null); + } + else { + // explicit changeset present. + currHeader = readMimeHeader(reader, opContext); + if (currHeader == null) { + throw new TableServiceException( + -1, + "An Error Occurred while processing the request, check the extended error information for more details.", + null, reader); + } + else { + do { + result.add(readMimePart(reader, docHeader.subBoundary, opContext)); + currHeader = readMimeHeader(reader, opContext); + } while (currHeader != null); + } + } + + return result; + } + + /** + * Reserved for internal use. A static factory method that constructs a {@link MimeHeader} by parsing the MIME + * header + * data from a {@link BufferedReader}. + * + * @param reader + * The {@link BufferedReader} containing the response stream to parse. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * @return + * A {@link MimeHeader} constructed by parsing the MIME header data from the {@link BufferedReader}. + * @throws IOException + * if an error occurs accessing the input stream. + * @throws StorageException + * if an error occurs parsing the input stream. + */ + protected static MimeHeader readMimeHeader(final BufferedReader reader, final OperationContext opContext) + throws IOException, StorageException { + final MimeHeader retHeader = new MimeHeader(); + reader.mark(1024 * 1024); + + // First thing is separator + retHeader.boundary = getNextLineSkippingBlankLines(reader); + if (retHeader.boundary.endsWith("--")) { + return null; + } + if (!retHeader.boundary.startsWith("--")) { + reader.reset(); + return null; + } + + for (int m = 0; m < 2; m++) { + final String tempString = reader.readLine(); + if (tempString.length() == 0) { + break; + } + + if (tempString.startsWith("Content-Type:")) { + final String[] headerVals = tempString.split("Content-Type: "); + if (headerVals == null || headerVals.length != 2) { + throw generateMimeParseException(); + } + retHeader.contentType = headerVals[1]; + } + else if (tempString.startsWith("Content-Transfer-Encoding:")) { + final String[] headerVals = tempString.split("Content-Transfer-Encoding: "); + if (headerVals == null || headerVals.length != 2) { + throw generateMimeParseException(); + } + retHeader.contentTransferEncoding = headerVals[1]; + } + else { + throw generateMimeParseException(); + } + } + + // Validate headers + if (Utility.isNullOrEmpty(retHeader.boundary) || retHeader.contentType == null) { + throw generateMimeParseException(); + } + + if (retHeader.contentType.startsWith("multipart/mixed; boundary=")) { + final String[] headerVals = retHeader.contentType.split("multipart/mixed; boundary="); + if (headerVals == null || headerVals.length != 2) { + throw generateMimeParseException(); + } + retHeader.subBoundary = "--".concat(headerVals[1]); + } + else if (!retHeader.contentType.equals("application/http")) { + throw generateMimeParseException(); + } + + if (retHeader.contentTransferEncoding != null && !retHeader.contentTransferEncoding.equals("binary")) { + throw generateMimeParseException(); + } + + return retHeader; + } + + // Returns at start of next mime boundary header + /** + * Reserved for internal use. A static factory method that generates a {@link MimePart} containing the next MIME + * part read from the {@link BufferedReader}. + * The {@link BufferedReader} is left positioned at the start of the next MIME boundary header. + * + * @param reader + * The {@link BufferedReader} containing the response stream to parse. + * @param boundary + * A <code>String</code> containing the MIME part boundary string. + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * @return + * A {@link MimePart} constructed by parsing the next MIME part data from the {@link BufferedReader}. + * @throws IOException + * if an error occured accessing the input stream. + * @throws StorageException + * if an error occured parsing the input stream. + */ + protected static MimePart readMimePart(final BufferedReader reader, final String boundary, + final OperationContext opContext) throws IOException, StorageException { + final MimePart retPart = new MimePart(); + // Read HttpStatus code + String tempStr = getNextLineSkippingBlankLines(reader); + if (!tempStr.startsWith("HTTP/1.1 ")) { + throw generateMimeParseException(); + } + + final String[] headerVals = tempStr.split(" "); + + if (headerVals.length < 3) { + throw generateMimeParseException(); + } + + retPart.httpStatusCode = Integer.parseInt(headerVals[1]); + // "HTTP/1.1 XXX ".length() => 13 + retPart.httpStatusMessage = tempStr.substring(13); + + // Read headers + tempStr = reader.readLine(); + while (tempStr != null && tempStr.length() > 0) { + final String[] headerParts = tempStr.split(": "); + if (headerParts.length < 2) { + throw generateMimeParseException(); + } + + retPart.headers.put(headerParts[0], headerParts[1]); + tempStr = reader.readLine(); + } + + // Store xml payload + reader.mark(1024 * 1024); + tempStr = getNextLineSkippingBlankLines(reader); + + if (tempStr == null) { + throw generateMimeParseException(); + } + + // empty body + if (tempStr.startsWith(boundary)) { + reader.reset(); + retPart.payload = Constants.EMPTY_STRING; + return retPart; + } + else if (!tempStr.startsWith("<?xml version=")) { + throw generateMimeParseException(); + } + final StringBuilder payloadBuilder = new StringBuilder(); + // read until mime closure or end of file + while (!tempStr.startsWith(boundary)) { + payloadBuilder.append(tempStr); + reader.mark(1024 * 1024); + tempStr = getNextLineSkippingBlankLines(reader); + if (tempStr == null) { + throw generateMimeParseException(); + } + } + + // positions stream at start of next MIME Header + reader.reset(); + + retPart.payload = payloadBuilder.toString(); + + return retPart; + } + + /** + * Reserved for internal use. Writes the batch operation to the output stream using batch request syntax. + * Batch request syntax is described in the MSDN topic <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd894038.aspx">Performing Entity Group + * Transactions</a>. + * + * @param outStream + * The {@link OutputStream} to write the batch request to. + * @param tableName + * A <code>String</code> 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 <code>String</code> containing the identifier to use as the MIME boundary for the batch request. + * @param changeSet + * A <code>String</code> 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 <code>null</code> 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 <code>String</code> 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 <code>String</code> 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 <code>String</code> 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 <code>String</code> containing the entity associated with an operation in + * AtomPub format. + * + * @param operation + * A {@link TableOperation} containing the entity to write to the returned <code>String</code>. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * @return + * A <code>String</code> 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<String, String> headers = new HashMap<String, String>(); + 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 <code>String</code> representation of the Atom namespace. + */ + public static final String ATOM_NS = "http://www.w3.org/2005/Atom"; + + /** + * The <code>String</code> representation of the OData Data namespace. + */ + public static final String DATA_SERVICES_NS = "http://schemas.microsoft.com/ado/2007/08/dataservices"; + + /** + * The <code>String</code> representation of the OData Metadata namespace. + */ + public static final String DATA_SERVICES_METADATA_NS = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata"; + + /** + * The <code>String</code> representation of the Atom namespace in brackets. + */ + public static final String BRACKETED_ATOM_NS = "{" + ATOM_NS + "}"; // default + + /** + * The <code>String</code> representation of the OData Data namespace in brackets. + */ + public static final String BRACKETED_DATA_SERVICES_NS = "{" + DATA_SERVICES_NS + "}"; // d: + + /** + * The <code>String</code> representation of the OData Metadata namespace in brackets. + */ + public static final String BRACKETED_DATA_SERVICES_METADATA_NS = "{" + DATA_SERVICES_METADATA_NS + "}"; // m: + + /** + * The <code>String</code> representation of the Atom Entry <em>feed</em> element name. + */ + public static final String FEED = "feed"; + + /** + * The <code>String</code> representation of the Atom Entry <em>title</em> element name. + */ + public static final String TITLE = "title"; + + /** + * The <code>String</code> representation of the Atom Entry <em>id</em> element name. + */ + public static final String ID = "id"; + + /** + * The <code>String</code> representation of the Atom Entry <em>updated</em> element name. + */ + public static final String UPDATED = "updated"; + + /** + * The <code>String</code> representation of the Atom Entry <em>link</em> element name. + */ + public static final String LINK = "link"; + + /** + * The <code>String</code> representation of the Atom Entry <em>author</em> element name. + */ + public static final String AUTHOR = "author"; + + /** + * The <code>String</code> representation of the Atom Entry <em>name</em> element name. + */ + public static final String NAME = "name"; + + /** + * The <code>String</code> representation of the Atom Entry <em>entry</em> element name. + */ + public static final String ENTRY = "entry"; + + /** + * The <code>String</code> representation of the Atom Entry <em>category</em> element name. + */ + public static final String CATEGORY = "category"; + + /** + * The <code>String</code> representation of the Atom Entry <em>content</em> element name. + */ + public static final String CONTENT = "content"; + + /** + * The <code>String</code> representation of the OData Metadata <em>properties</em> element name. + */ + public static final String PROPERTIES = "properties"; + + /** + * The <code>String</code> representation of the Atom Entry <em>etag</em> element name. + */ + public static final String ETAG = "etag"; + + /** + * The <code>String</code> representation of the <em>type</em> attribute name. + */ + public static final String TYPE = "type"; + + /** + * The <code>String</code> representation of the <em>term</em> element name. + */ + public static final String TERM = "term"; + + /** + * The <code>String</code> representation of <em>scheme</em>. + */ + public static final String SCHEME = "scheme"; + + /** + * The <code>String</code> representation of <em>href</em>. + */ + public static final String HREF = "href"; + + /** + * The <code>String</code> representation of <em>rel</em>. + */ + public static final String REL = "rel"; + + /** + * The <code>String</code> representation of the <em>null</em> attribute name. + */ + public static final String NULL = "null"; + + /** + * The <code>String</code> representation of the content type attribute value to send. + */ + public static final String ODATA_CONTENT_TYPE = "application/xml"; + + // Odata edm types + + /** + * The <code>String</code> representation of the <em>Edm.DateTime</em> metadata type attribute value. + */ + public static final String EDMTYPE_DATETIME = "Edm.DateTime"; + + /** + * The <code>String</code> representation of the <em>Edm.Binary</em> metadata type attribute value. + */ + public static final String EDMTYPE_BINARY = "Edm.Binary"; + + /** + * The <code>String</code> representation of the <em>Edm.Boolean</em> metadata type attribute value. + */ + public static final String EDMTYPE_BOOLEAN = "Edm.Boolean"; + + /** + * The <code>String</code> representation of the <em>Edm.Double</em> metadata type attribute value. + */ + public static final String EDMTYPE_DOUBLE = "Edm.Double"; + + /** + * The <code>String</code> representation of the <em>Edm.Guid</em> metadata type attribute value. + */ + public static final String EDMTYPE_GUID = "Edm.Guid"; + + /** + * The <code>String</code> representation of the <em>Edm.Int32</em> metadata type attribute value. + */ + public static final String EDMTYPE_INT32 = "Edm.Int32"; + + /** + * The <code>String</code> representation of the <em>Edm.Int64</em> 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<T> { + /** + * A collection of table entities. + */ + protected ArrayList<T> results; + + /** + * A collection of {@link TableResults} which include additional information about the entities returned by an + * operation. + */ + protected ArrayList<TableResult> tableResults; + + /** + * Constructs an {@link ODataPayload} instance with new empty entity and {@link TableResult} collections. + */ + protected ODataPayload() { + this.results = new ArrayList<T>(); + this.tableResults = new ArrayList<TableResult>(); + } +} 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 <code>get<em>PropertyName</em></code> + * and <code>set<em>PropertyName</em></code>, 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<String, PropertyPair> generatePropertyPairs(final Class<?> clazzType) { + final Method[] methods = clazzType.getMethods(); + final HashMap<String, PropertyPair> propMap = new HashMap<String, PropertyPair>(); + + 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<String> keysToRemove = new ArrayList<String>(); + final ArrayList<String> keysToAlter = new ArrayList<String>(); + + for (final Entry<String, PropertyPair> 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 <code>true</code> 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 <code>execute</code> 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 <code>execute</code> 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 <code>String</code> containing the PartitionKey value for the entity. + * @param rowKey + * A <code>String</code> 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 <code>String</code> 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 <code>String</code> 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 <code>java.lang.Class</code> 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 <code>XMLStreamReader</code> containing the response to the query operation. + * @param httpStatusCode + * The HTTP status code returned from the operation request. + * @param etagFromHeader + * The <code>String</code> 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 <code>XMLStreamReader</code>. + * @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}. + * <p> + * This method will invoke the Storage Service REST API to execute this table operation, using the Table service + * endpoint and storage account credentials in the {@link CloudTableClient} object. + * + * @param client + * A {@link CloudTableClient} instance specifying the Table service endpoint and storage account + * credentials to use. + * @param tableName + * A <code>String</code> 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<CloudTableClient, QueryTableOperation, TableResult> impl = new StorageOperation<CloudTableClient, QueryTableOperation, TableResult>( + 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 <code>java.lang.Class</code> 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 <code>String</code> 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 <code>String</code> 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 + * <code>@StoreAs</code> annotation. + * <p> + * Example: + * <p> + * <code>@StoreAs(name = "EntityPropertyName")<br>public String getObjectPropertyName() { ... }</code> + * <p> + * <code>@StoreAs(name = "EntityPropertyName")<br>public void setObjectPropertyName(String name) { ... }</code> + * <p> + * This example shows how the methods that would get and set an entity property named <em>ObjectPropertyName</em> in the + * default case can be annotated to get and set an entity property named <em>EntityPropertyName</em>. 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 <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd894038.aspx">Entity Group Transaction</a>. + * <p> + * 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<TableOperation> { + 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 <code>ArrayList</code>. + * + * @param index + * The index in the batch operation <code>ArrayList</code> 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 <code>ArrayList</code>. + * + * @param element + * The {@link TableOperation} to add to the batch operation. + * @return + * <code>true</code> 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 <code>ArrayList</code> starting at the specified + * index. + * + * @param index + * The index in the batch operation <code>ArrayList</code> to add the table operation at. + * @param c + * The collection of {@link TableOperation} objects to add to the batch operation. + * @return + * <code>true</code> 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 <code>ArrayList</code>. + * + * @param c + * The collection of {@link TableOperation} objects to add to the batch operation. + * @return + * <code>true</code> 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 <code>String</code> containing the PartitionKey of the entity to retrieve. + * @param rowKey + * A <code>String</code> 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 <code>String</code> containing the PartitionKey of the entity to retrieve. + * @param rowKey + * A <code>String</code> 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 <code>ArrayList</code> 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 <code>Object</code> from the batch operation. + * + * @param o + * The <code>Object</code> to remove from the batch operation. + * @return + * <code>true</code> 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 + * <code>true</code> 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 <code>String</code> 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}. + * <p> + * 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 <code>String</code> containing the name of the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. + * + * @return + * An <code>ArrayList</code> of {@link TableResult} containing the results of executing the operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + protected ArrayList<TableResult> 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<CloudTableClient, TableBatchOperation, ArrayList<TableResult>> impl = new StorageOperation<CloudTableClient, TableBatchOperation, ArrayList<TableResult>>( + options) { + @Override + public ArrayList<TableResult> 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<MimePart> 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<TableResult> result = new ArrayList<TableResult>(); + 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 <code>ArrayList</code>. + * + * @param fromIndex + * The inclusive lower bound of the range of {@link TableOperation} objects to remove from the batch + * operation <code>ArrayList</code>. + * @param toIndex + * The exclusive upper bound of the range of {@link TableOperation} objects to remove from the batch + * operation <code>ArrayList</code>. + */ + @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 <code>writeEntity</code> and <code>readEntity</code> 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. + * <p> + * The Storage client library includes two implementations of {@link TableEntity} that provide for simple property + * access and serialization: + * <p> + * {@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. + * <p> + * {@link TableServiceEntity} is an implementation of {@link TableEntity} that uses reflection-based serialization and + * deserialization behavior in its <code>writeEntity</code> and <code>readEntity</code> methods. + * {@link TableServiceEntity}-derived classes with methods that follow a convention for types and naming are serialized + * and deserialized automatically. + * <p> + * 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 + * <code>TableServiceEntity.readEntityWithReflection</code> in <code>readEntity</code> and + * <code>TableServiceEntity.writeEntityWithReflection</code> in <code>writeEntity</code>. 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 + * <p> + * <code>public <em>type</em> get<em>PropertyName</em>() { ... }</code> + * <p> + * and + * <p> + * <code>public void set<em>PropertyName</em>(<em>type</em> parameter) { ... }</code> + * <p> + * where <em>PropertyName</em> is a property name for the table entity, and <em>type</em> 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 <code>name</code> + * 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 <code>name</code> 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. + * <p> + * + * @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 <code>String</code> containing the Etag for the entity. + */ + public String getEtag(); + + /** + * Gets the PartitionKey value for the entity. + * + * @return + * A <code>String</code> containing the PartitionKey value for the entity. + */ + public String getPartitionKey(); + + /** + * Gets the RowKey value for the entity. + * + * @return + * A <code>String</code> containing the RowKey value for the entity. + */ + public String getRowKey(); + + /** + * Gets the Timestamp for the entity. + * + * @return + * A <code>Date</code> 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 <code>String</code> property names to {@link EntityProperty} data typed values. + * + * @param properties + * The <code>java.util.HashMap</code> of <code>String</code> 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<String, EntityProperty> properties, OperationContext opContext) + throws StorageException; + + /** + * Sets the Etag for the entity. + * + * @param etag + * The <code>String</code> containing the Etag to set for the entity. + */ + public void setEtag(String etag); + + /** + * Sets the PartitionKey value for the entity. + * + * @param partitionKey + * The <code>String</code> containing the PartitionKey value to set for the entity. + */ + public void setPartitionKey(String partitionKey); + + /** + * Sets the RowKey value for the entity. + * + * @param rowKey + * The <code>String</code> containing the RowKey value to set for the entity. + */ + public void setRowKey(String rowKey); + + /** + * Sets the Timestamp value for the entity. + * + * @param timeStamp + * The <code>Date</code> containing the Timestamp value to set for the entity. + */ + public void setTimestamp(Date timeStamp); + + /** + * Returns a map of <code>String</code> 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 <code>java.util.HashMap</code> of <code>String</code> 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<String, EntityProperty> 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. + * <p> + * 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 + * <code>execute</code> 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 <code>execute</code> 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 <code>String</code> containing the PartitionKey value for the entity to retrieve. + * @param rowKey + * A <code>String</code> 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 <code>String</code> containing the PartitionKey value for the entity to retrieve. + * @param rowKey + * A <code>String</code> 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}. + * <p> + * This method will invoke the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd135727.aspx">Delete + * Entity</a> REST API to execute this table operation, using the Table service endpoint and storage account + * credentials in the {@link CloudTableClient} object. + * + * @param client + * A {@link CloudTableClient} instance specifying the Table service endpoint, storage account + * credentials, and any additional query parameters. + * @param tableName + * A <code>String</code> containing the name of the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. + * + * @return + * A {@link TableResult} containing the results of executing the operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + private TableResult performDelete(final CloudTableClient client, final String tableName, + final TableRequestOptions options, final OperationContext opContext) throws StorageException { + final boolean isTableEntry = TableConstants.TABLES_SERVICE_TABLES_NAME.equals(tableName); + final String tableIdentity = isTableEntry ? this.getEntity().writeEntity(opContext) + .get(TableConstants.TABLE_NAME).getValueAsString() : null; + + if (!isTableEntry) { + Utility.assertNotNullOrEmpty("Delete requires a valid ETag", this.getEntity().getEtag()); + Utility.assertNotNullOrEmpty("Delete requires a valid PartitionKey", this.getEntity().getPartitionKey()); + Utility.assertNotNullOrEmpty("Delete requires a valid RowKey", this.getEntity().getRowKey()); + } + + final StorageOperation<CloudTableClient, TableOperation, TableResult> impl = new StorageOperation<CloudTableClient, TableOperation, TableResult>( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + + final HttpURLConnection request = TableRequest.delete(client.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}. + * <p> + * This method will invoke the Insert Entity REST API to execute this table operation, using the Table service + * endpoint and storage account credentials in the {@link CloudTableClient} object. + * + * @param client + * A {@link CloudTableClient} instance specifying the Table service endpoint, storage account + * credentials, and any additional query parameters. + * @param tableName + * A <code>String</code> containing the name of the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. + * + * @return + * A {@link TableResult} containing the results of executing the operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + private TableResult performInsert(final CloudTableClient client, final String tableName, + final TableRequestOptions options, final OperationContext opContext) throws StorageException { + final boolean isTableEntry = TableConstants.TABLES_SERVICE_TABLES_NAME.equals(tableName); + final String tableIdentity = isTableEntry ? this.getEntity().writeEntity(opContext) + .get(TableConstants.TABLE_NAME).getValueAsString() : null; + + // Upserts need row key and partition key + if (!isTableEntry && this.opType != TableOperationType.INSERT) { + Utility.assertNotNullOrEmpty("Upserts require a valid PartitionKey", this.getEntity().getPartitionKey()); + Utility.assertNotNullOrEmpty("Upserts require a valid RowKey", this.getEntity().getRowKey()); + } + + final StorageOperation<CloudTableClient, TableOperation, TableResult> impl = new StorageOperation<CloudTableClient, TableOperation, TableResult>( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + final HttpURLConnection request = TableRequest.insert(client.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}. + * <p> + * This method will invoke the Merge Entity REST API to execute this table operation, using the Table service + * endpoint and storage account credentials in the {@link CloudTableClient} object. + * + * @param client + * A {@link CloudTableClient} instance specifying the Table service endpoint, storage account + * credentials, and any additional query parameters. + * @param tableName + * A <code>String</code> containing the name of the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. + * + * @return + * A {@link TableResult} containing the results of executing the operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + private TableResult performMerge(final CloudTableClient client, final String tableName, + final TableRequestOptions options, final OperationContext opContext) throws StorageException { + Utility.assertNotNullOrEmpty("Merge requires a valid ETag", this.getEntity().getEtag()); + Utility.assertNotNullOrEmpty("Merge requires a valid PartitionKey", this.getEntity().getPartitionKey()); + Utility.assertNotNullOrEmpty("Merge requires a valid RowKey", this.getEntity().getRowKey()); + + final StorageOperation<CloudTableClient, TableOperation, TableResult> impl = new StorageOperation<CloudTableClient, TableOperation, TableResult>( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + + final HttpURLConnection request = TableRequest.merge(client.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}. + * <p> + * This method will invoke the Storage Service REST API to execute this table operation, using the Table service + * endpoint and storage account credentials in the {@link CloudTableClient} object. + * + * @param client + * A {@link CloudTableClient} instance specifying the Table service endpoint, storage account + * credentials, and any additional query parameters. + * @param tableName + * A <code>String</code> containing the name of the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. + * + * @return + * A {@link TableResult} containing the results of executing the operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + 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<CloudTableClient, TableOperation, TableResult> impl = new StorageOperation<CloudTableClient, TableOperation, TableResult>( + options) { + @Override + public TableResult execute(final CloudTableClient client, final TableOperation operation, + final OperationContext opContext) throws Exception { + + final HttpURLConnection request = TableRequest.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}. + * <p> + * This method will invoke the Storage Service REST API to execute this table operation, using the Table service + * endpoint and storage account credentials in the {@link CloudTableClient} object. + * + * @param client + * A {@link CloudTableClient} instance specifying the Table service endpoint, storage account + * credentials, and any additional query parameters. + * @param tableName + * A <code>String</code> containing the name of the table. + * @param options + * A {@link TableRequestOptions} object that specifies execution options such as retry policy and timeout + * settings for the operation. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. + * + * @return + * A {@link TableResult} containing the results of executing the operation. + * + * @throws StorageException + * if an error occurs in the storage operation. + */ + protected TableResult execute(final CloudTableClient client, final String tableName, TableRequestOptions options, + OperationContext opContext) throws StorageException { + if (opContext == null) { + opContext = new OperationContext(); + } + + if (options == null) { + options = new TableRequestOptions(); + } + + opContext.initialize(); + options.applyDefaults(client); + Utility.assertNotNullOrEmpty("TableName", tableName); + + if (this.getOperationType() == TableOperationType.INSERT + || this.getOperationType() == TableOperationType.INSERT_OR_MERGE + || this.getOperationType() == TableOperationType.INSERT_OR_REPLACE) { + return this.performInsert(client, tableName, options, opContext); + } + else if (this.getOperationType() == TableOperationType.DELETE) { + return this.performDelete(client, tableName, options, opContext); + } + else if (this.getOperationType() == TableOperationType.MERGE) { + return this.performMerge(client, tableName, options, opContext); + } + else if (this.getOperationType() == TableOperationType.REPLACE) { + return this.performUpdate(client, tableName, options, opContext); + } + else if (this.getOperationType() == TableOperationType.RETRIEVE) { + return ((QueryTableOperation) this).performRetrieve(client, tableName, options, opContext); + } + else { + throw new IllegalArgumentException("Unknown table operation"); + } + } + + /** + * Reserved for internal use. 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 <code>true</code> to use the specified <code>entryName</code> parameter, or <code>false</code> 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 <code>isSingleIndexEntry</code> parameter is + * <code>true</code>. + * @return + * A <code>String</code> 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 <code>String</code> containing the name of the table. + * @return + * A <code>String</code> 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 <code>XMLStreamReader</code> containing the response to an insert operation. + * @param httpStatusCode + * The HTTP status code returned from the operation request. + * @param etagFromHeader + * The <code>String</code> 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 <code>XMLStreamReader</code>. + * @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 <code>null</code>. + */ + 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 <code>execute</code> or <code>executeSegmented</code> + * 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. + * <p> + * 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 + * <code>TableQuery</code> 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 + * <code>TableQuery</code>, so the values saved in the <code>TableQuery</code> will be the last encountered in order of + * execution. + * <p> + * As an example, you could construct a table query using fluent syntax: + * <p> + * <code>TableQuery<TableServiceEntity> myQuery = TableQuery.from("Products", DynamicTableEntity.class)<br> + *     .where("(PartitionKey eq 'ProductsMNO') and (RowKey ge 'Napkin')")<br> + *     .take(25)<br> + *     .select(new String[] {"InventoryCount"});</code> + * <p> + * 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. + * <p> + * 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 <code>String</code> types for comparison purposes. + * <p> + * The values that may be used in table queries are explained in more detail in the MSDN topic <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/dd894031.aspx">Querying Tables and Entities</a>, 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. + * <p> + * 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} + * <code>.class</code> as the class type. + * + * @param <T> + * 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<T extends TableEntity> { + /** + * 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. + * <p> + * 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} + * <code>.class</code> as the class type parameter if no more specialized type is required. + * + * @param tablename + * A <code>String</code> containing the name of the source table to query. + * @param clazzType + * The <code>java.lang.Class</code> of the class <code>T</code> 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 <T extends TableEntity> TableQuery<T> from(final String tablename, final Class<T> clazzType) { + return new TableQuery<T>(tablename, clazzType); + } + + /** + * Generates a property filter condition string for a <code>boolean</code> 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: + * <p> + * <code>String condition = generateFilterCondition("BooleanProperty", QueryComparisons.EQUAL, false);</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>BooleanProperty eq false</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>boolean</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>byte[]</code> 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: + * <p> + * <code>String condition = generateFilterCondition("ByteArray", QueryComparisons.EQUAL, new byte[] {0x01, 0x0f});</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>ByteArray eq X'010f'</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>byte[]</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>Byte[]</code> 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: + * <p> + * <code>String condition = generateFilterCondition("ByteArray", QueryComparisons.EQUAL, new Byte[] {0x01, 0xfe});</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>ByteArray eq X'01fe'</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>Byte[]</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>Date</code> 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: + * <p> + * <code>String condition = generateFilterCondition("FutureDate", QueryComparisons.GREATER_THAN, new Date());</code> + * <p> + * This statement sets <code>condition</code> to something like the following value: + * <p> + * <code>FutureDate gt datetime'2013-01-31T09:00:00'</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>Date</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>double</code> 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: + * <p> + * <code>String condition = generateFilterCondition("Circumference", QueryComparisons.EQUAL, 2 * 3.141592);</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>Circumference eq 6.283184</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>double</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>int</code> 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: + * <p> + * <code>String condition = generateFilterCondition("Population", QueryComparisons.GREATER_THAN, 1000);</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>Population gt 1000</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * An <code>int</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>long</code> 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: + * <p> + * <code>String condition = generateFilterCondition("StellarMass", QueryComparisons.GREATER_THAN, 7000000000L);</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>StellarMass gt 7000000000</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>long</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>String</code> 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: + * <p> + * <code>String condition = generateFilterCondition("Platform", QueryComparisons.EQUAL, "Windows Azure");</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>Platform eq 'Windows Azure'</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>String</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>String</code> containing the value to compare with the property. + * @param edmType + * The {@link EdmType} to format the value as. + * @return + * A <code>String</code> 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 <code>UUID</code> 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: + * <p> + * <code>String condition = generateFilterCondition("Identity", QueryComparisons.EQUAL, UUID.fromString(</code> + * <code>"c9da6455-213d-42c9-9a79-3e9149a57833"));</code> + * <p> + * This statement sets <code>condition</code> to the following value: + * <p> + * <code>Identity eq guid'c9da6455-213d-42c9-9a79-3e9149a57833'</code> + * + * @param propertyName + * A <code>String</code> containing the name of the property to compare. + * @param operation + * A <code>String</code> containing the comparison operator to use. + * @param value + * A <code>UUID</code> containing the value to compare with the property. + * @return + * A <code>String</code> 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 <code>String</code> containing the first formatted filter condition. + * @param operator + * A <code>String</code> containing <code>Operators.AND</code> or <code>Operators.OR</code>. + * @param filterB + * A <code>String</code> containing the first formatted filter condition. + * @return + * A <code>String</code> 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<T> 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}<code>.class</code> as the class type parameter if no more specialized type is + * required. + * + * @param tablename + * A <code>String</code> containing the name of the source table to query. + * @param clazzType + * The <code>java.lang.Class</code> of the class <code>T</code> that represents the table entity type for + * the query. Class <code>T</code> must be a type that implements {@link TableEntity} and has a nullary + * constructor, + */ + public TableQuery(final String tableName, final Class<T> clazzType) { + this.setSourceTableName(tableName); + this.setClazzType(clazzType); + } + + /** + * Gets the class type of the table entities returned by the query. + * + * @return + * The <code>java.lang.Class</code> of the class <code>T</code> that represents the table entity type for + * the query. + */ + public Class<T> 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. + * <p> + * Note that the system properties <code>PartitionKey</code>, <code>RowKey</code>, and <code>Timestamp</code> are + * automatically requested from the table service whether specified in the {@link TableQuery} or not. + * + * @return + * An array of <code>String</code> 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 <code>java.lang.Class</code> of the class <code>T</code> implementing the {@link TableEntity} + * interface that + * represents the table entity type for the query. + */ + public Class<T> 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 <code>String</code> 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 <code>String</code> 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. + * <p> + * If the value returned by <code>getTakeCount</code> 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 + * <code>select</code> 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. + * <p> + * Note that the system properties <code>PartitionKey</code>, <code>RowKey</code>, and <code>Timestamp</code> are + * automatically requested from the table service whether specified in the {@link TableQuery} or not. + * + * @param columns + * An array of <code>String</code> 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<T> 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. + * <p> + * Callers may specify {@link TableServiceEntity}<code>.class</code> as the class type parameter if no more + * specialized type is required. + * + * @param clazzType + * The <code>java.lang.Class</code> of the class <code>T</code> that represents the table entity type for + * the query. Class <code>T</code> must be a type that implements {@link TableEntity} and has a nullary + * constructor, + */ + public void setClazzType(final Class<T> 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. + * <p> + * Note that the system properties <code>PartitionKey</code>, <code>RowKey</code>, and <code>Timestamp</code> are + * automatically requested from the table service whether specified in the {@link TableQuery} or not. + * + * @param columns + * An array of <code>String</code> 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. + * <p> + * 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 <code>String</code> types for comparison purposes. For + * example, to query all entities with a PartitionKey value of "AccessLogs" on table query <code>myQuery</code>: + * <p> + * <code>    myQuery.setFilterString("PartitionKey eq 'AccessLogs'");</code> + * <p> + * The values that may be used in table queries are explained in more detail in the MSDN topic + * + * <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd894031.aspx">Querying Tables and Entities</a>, + * 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. + * <p> + * Note that no more than 15 discrete comparisons are permitted within a filter string. + * + * @param filterString + * A <code>String</code> 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 <code>String</code> 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. + * <p> + * If the value specified for the <code>takeCount</code> 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. + * <p> + * If the value specified for the <code>take</code> 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<T> 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. + * <p> + * 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 <code>String</code> types for comparison purposes. For example, to + * query all entities with a PartitionKey value of "AccessLogs" on table query <code>myQuery</code>: + * <p> + * <code>    myQuery = myQuery.where("PartitionKey eq 'AccessLogs'");</code> + * <p> + * The values that may be used in table queries are explained in more detail in the MSDN topic + * + * <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd894031.aspx">Querying Tables and Entities</a>, + * 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. + * <p> + * Note that no more than 15 discrete comparisons are permitted within a filter string. + * + * @param filter + * A <code>String</code> 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<T> 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 <code>null</code> 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 <code>HttpURLConnection</code> to perform a table batch operation. + * + * @param rootUri + * A <code>java.net.URI</code> containing an absolute URI to the resource. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param batchID + * The <code>String</code> 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 <code>null</code> to + * safely ignore operation context. + * + * @return + * An <code>HttpURLConnection</code> 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 <code>HttpURLConnection</code> to perform an operation. + * + * @param rootUri + * A <code>java.net.URI</code> 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 + * <code><em>tableName</em>(<em>identity</em>)</code>. If <code>null</code>, only the <em>tableName</em> + * value will be passed. + * @param timeoutInMs + * The server timeout interval in milliseconds. + * @param queryBuilder + * The <code>UriQueryBuilder</code> 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 <code>HttpURLConnection</code> 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 <code>HttpURLConnection</code> to perform a delete operation. + * + * @param rootUri + * A <code>java.net.URI</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * An <code>HttpURLConnection</code> 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 <code>HttpURLConnection</code> to perform an insert operation. + * + * @param rootUri + * A <code>java.net.URI</code> 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 <code>null</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * An <code>HttpURLConnection</code> 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 <code>java.net.URI</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * An <code>HttpURLConnection</code> 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 <code>java.net.URI</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * An <code>HttpURLConnection</code> 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 <code>java.net.URI</code> containing an absolute URI to the resource. + * @param tableName + * The name of the table. + * @param identity + * A <code>String</code> representing the identity of the entity. The resulting request will be formatted + * using <em>/tableName(identity)</em> if identity is not >code>null</code> 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 <code>null</code> to use the request options specified on the + * {@link CloudTableClient}. + * @param opContext + * An {@link OperationContext} object for tracking the current operation. Specify <code>null</code> to + * safely ignore operation context. + * + * @return + * An <code>HttpURLConnection</code> 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 <code>TableRequestOptions</code> + * instance, if they are currently <code>null</code>, 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 <code>java.net.HttpURLConnection<code> request response to parse for continuation token + * information. + * + * @return + * A {@link ResultContinuation} instance from continuation token information in the response, or + * <code>null</code> 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<String, EntityProperty> 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 <code>String</code> 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 <code>java.util.HashMap</code> of <code>String</code> property names to {@link EntityProperty} data + * typed values representing the properties of a table entity. + */ + public HashMap<String, EntityProperty> 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 <code>Object</code>. + */ + 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 <code>T</code>. + */ + @SuppressWarnings("unchecked") + public <T> T getResultAsType() { + return (T) this.getResult(); + } + + /** + * Reserved for internal use. Sets the Etag associated with the table operation results. + * + * @param etag + * A <code>String</code> 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 <code>String</code> 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 <code>java.util.HashMap</code> of <code>String</code> 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<String, EntityProperty> 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 <code>Object</code> 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 + * <code>readEntity</code> and <code>writeEntity</code> methods that by default serialize and deserialize all properties + * via reflection. A table entity class may extend this class and override the <code>readEntity</code> and + * <code>writeEntity</code> methods to provide customized or more performant serialization logic. + * <p> + * 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 + * <p> + * <code>public <em>type</em> get<em>PropertyName</em>() { ... }</code> + * <p> + * and + * <p> + * <code>public void set<em>PropertyName</em>(<em>type</em> parameter) { ... }</code> + * <p> + * where <em>PropertyName</em> is a property name for the table entity, and <em>type</em> 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 <code>name</code> 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 + * <code>name</code> 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. + * <p> + * The following table shows the supported property data types in Windows Azure storage and the corresponding Java types + * when deserialized. + * <TABLE BORDER="1" WIDTH="100%" CELLPADDING="3" CELLSPACING="0"> + * <TR BGCOLOR="#EEEEFF" CLASS="TableSubHeadingColor"> + * <th>Storage Type</th> + * <th>EdmType Value</th> + * <th>Java Type</th> + * <th>Description</th> + * </tr> + * <tr> + * <td><strong>Edm.Binary</strong></td> + * <td>{@link EdmType#BINARY}</td> + * <td><code>byte[], Byte[]</code></td> + * <td>An array of bytes up to 64 KB in size.</td> + * </tr> + * <tr> + * <td><strong>Edm.Boolean</strong></td> + * <td>{@link EdmType#BOOLEAN}</td> + * <td><code>boolean, Boolean</code></td> + * <td>A Boolean value.</td> + * </tr> + * <tr> + * <td><strong>Edm.Byte</strong></td> + * <td>{@link EdmType#BYTE}</td> + * <td><code>boolean, Boolean</code></td> + * <td>A Boolean value.</td> + * </tr> + * <tr> + * <td><strong>Edm.DateTime</strong></td> + * <td>{@link EdmType#DATE_TIME}</td> + * <td><code>Date</code></td> + * <td>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.</td> + * </tr> + * <tr> + * <td><strong>Edm.Double</strong></td> + * <td>{@link EdmType#DOUBLE}</td> + * <td><code>double, Double</code></td> + * <td>A 64-bit double-precision floating point value.</td> + * </tr> + * <tr> + * <td><strong>Edm.Guid</strong></td> + * <td>{@link EdmType#GUID}</td> + * <td><code>UUID</code></td> + * <td>A 128-bit globally unique identifier.</td> + * </tr> + * <tr> + * <td><strong>Edm.Int32</strong></td> + * <td>{@link EdmType#INT32}</td> + * <td><code>int, Integer</code></td> + * <td>A 32-bit integer value.</td> + * </tr> + * <tr> + * <td><strong>Edm.Int64</strong></td> + * <td>{@link EdmType#INT64}</td> + * <td><code>long, Long</code></td> + * <td>A 64-bit integer value.</td> + * </tr> + * <tr> + * <td><strong>Edm.String</strong></td> + * <td>{@link EdmType#STRING}</td> + * <td><code>String</code></td> + * <td>A UTF-16-encoded value. String values may be up to 64 KB in size.</td> + * </tr> + * </table> + * <p> + * See the MSDN topic <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179338.aspx">Understanding the + * Table Service Data Model</a> for an overview of tables, entities, and properties as used in the Windows Azure Storage + * service. + * <p> + * For an overview of the available EDM primitive data types and names, see the + * + * <a href="http://www.odata.org/developers/protocols/overview#AbstractTypeSystem">Primitive Data Types</a> section of + * the <a href="http://www.odata.org/developers/protocols/overview">OData Protocol Overview</a>. + * <p> + * + * @see EdmType + */ +public class TableServiceEntity implements TableEntity { + /** + * Deserializes the table entity property map into the specified object instance using reflection. + * <p> + * 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. + * <p> + * 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 <code>String</code> 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<String, EntityProperty> properties, final OperationContext opContext) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + final HashMap<String, PropertyPair> props = PropertyPair.generatePropertyPairs(instance.getClass()); + + for (final Entry<String, EntityProperty> 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. + * <p> + * 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. + * <p> + * 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 <code>String</code> 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<String, EntityProperty> writeEntityWithReflection(final Object instance) + throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { + final HashMap<String, PropertyPair> props = PropertyPair.generatePropertyPairs(instance.getClass()); + + final HashMap<String, EntityProperty> retVal = new HashMap<String, EntityProperty>(); + for (final Entry<String, PropertyPair> 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 <code>String</code> containing the Etag for the entity. + */ + @Override + public String getEtag() { + return this.etag; + } + + /** + * Gets the PartitionKey value for the entity. + * + * @return + * A <code>String</code> containing the PartitionKey value for the entity. + */ + @Override + public String getPartitionKey() { + return this.partitionKey; + } + + /** + * Gets the RowKey value for the entity. + * + * @return + * A <code>String</code> containing the RowKey value for the entity. + */ + @Override + public String getRowKey() { + return this.rowKey; + } + + /** + * Gets the Timestamp value for the entity. + * + * @return + * A <code>Date</code> 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. + * <p> + * 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. + * <p> + * Override this method in classes that extend {@link TableServiceEntity} to invoke custom serialization code. + * + * @param properties + * The <code>java.util.HashMap</code> of <code>String</code> 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<String, EntityProperty> 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 <code>String</code> 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 <code>String</code> 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 <code>String</code> 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 <code>Date</code> 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. + * <p> + * 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. + * <p> + * 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 <code>java.util.HashMap</code> of <code>String</code> 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<String, EntityProperty> 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 <code>java.io.InputStream</code> 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 <code>TableServiceException</code> instance using the specified error code, message, status code, + * extended error information and inner exception. + * + * @param errorCode + * A <code>String</code> that represents the error code returned by the table operation. + * @param message + * A <code>String</code> 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 <code>Exception</code> 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 <code>TableServiceException</code> instance using the specified HTTP + * status code, message, operation, and stream reader. + * + * @param httpStatusCode + * The <code>int</code> HTTP Status Code value returned by the table operation that caused the exception. + * @param message + * A <code>String</code> 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 <code>Java.IO.Stream</code> 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 <code>TableServiceException</code> 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 <code>boolean</code> 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 <code>TableServiceException</code> 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 <code>boolean</code> 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/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<TableResult> 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<TableResult> 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<TableResult> 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<TableResult> 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<TableResult> 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<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 6); + + Iterator<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 4); + + Iterator<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + Assert.assertEquals(results.size(), 3); + + Iterator<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + for (TableResult r : results) { + Assert.assertEquals(r.getHttpStatusCode(), HttpURLConnection.HTTP_CREATED); + delBatch.delete((class1) r.getResult()); + } + + ArrayList<TableResult> 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<TableResult> results = tClient.execute(testSuiteTableName, batch); + for (TableResult r : results) { + Assert.assertEquals(r.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + delBatch.delete((class2) r.getResult()); + } + + ArrayList<TableResult> 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<String> tables = new ArrayList<String>(); + 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<String> 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<String> 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<String> 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<String> tables = new ArrayList<String>(); + 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<String> 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<String> 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<String> 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<String> tables = new ArrayList<String>(); + 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("<tag>", false); + doEscapeTest("</entry>", false); + doEscapeTest("!<", false); + doEscapeTest("<!%^&j", false); + } + + @Test + public void xmlTestBatch() throws StorageException { + doEscapeTest("</>", false); + doEscapeTest("<tag>", false); + doEscapeTest("</entry>", false); + doEscapeTest("!<", false); + doEscapeTest("<!%^&j", false); + } + + private void doEscapeTest(String data, boolean useBatch) throws StorageException { + class1 ref = new class1(); + + ref.setA(data); + ref.setPartitionKey("temp"); + ref.setRowKey(UUID.randomUUID().toString()); + if (useBatch) { + TableBatchOperation batch = new TableBatchOperation(); + batch.insert(ref); + tClient.execute(testSuiteTableName, batch); + } + else { + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + } + + TableResult res = null; + + if (useBatch) { + TableBatchOperation batch = new TableBatchOperation(); + batch.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class); + res = tClient.execute(testSuiteTableName, batch).get(0); + } + else { + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + } + + class1 retObj = res.getResultAsType(); + Assert.assertEquals(ref.getA(), retObj.getA()); + + ref.setEtag(retObj.getEtag()); + ref.setB(data); + + // Merge + if (useBatch) { + TableBatchOperation batch = new TableBatchOperation(); + batch.merge(ref); + tClient.execute(testSuiteTableName, batch); + } + else { + tClient.execute(testSuiteTableName, TableOperation.merge(ref)); + } + + if (useBatch) { + TableBatchOperation batch = new TableBatchOperation(); + batch.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class); + res = tClient.execute(testSuiteTableName, batch).get(0); + } + else { + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + } + + retObj = res.getResultAsType(); + Assert.assertEquals(ref.getA(), retObj.getA()); + Assert.assertEquals(ref.getB(), retObj.getB()); + + // Replace + ref.setEtag(retObj.getEtag()); + ref.setC(data); + + if (useBatch) { + TableBatchOperation batch = new TableBatchOperation(); + batch.replace(ref); + tClient.execute(testSuiteTableName, batch); + } + else { + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + } + + if (useBatch) { + TableBatchOperation batch = new TableBatchOperation(); + batch.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class); + res = tClient.execute(testSuiteTableName, batch).get(0); + } + else { + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + } + + retObj = res.getResultAsType(); + Assert.assertEquals(ref.getA(), retObj.getA()); + Assert.assertEquals(ref.getB(), retObj.getB()); + Assert.assertEquals(ref.getC(), retObj.getC()); + } + + private void doQueryEscapeTest(String data) throws StorageException { + class1 ref = new class1(); + + ref.setA(data); + ref.setPartitionKey(UUID.randomUUID().toString()); + ref.setRowKey("foo"); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + TableQuery<class1> 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.<DynamicTableEntity> 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<String>() { + @Override + public String resolve(String partitionKey, String rowKey, Date timeStamp, + HashMap<String, EntityProperty> 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.<DynamicTableEntity> 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.<DynamicTableEntity> 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.<DynamicTableEntity> 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<DynamicTableEntity> 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<class1> 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<ResponseReceivedEvent>() { + + @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<class1> 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<class1> 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<class1> result = tClient.execute(TableQuery.from(testSuiteTableName, TableServiceEntity.class), + new EntityResolver<class1>() { + @Override + public class1 resolve(String partitionKey, String rowKey, Date timeStamp, + HashMap<String, EntityProperty> 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<class1> 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<class1> 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<class1> 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<class1> 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<ComplexEntity> 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<ComplexEntity> 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("-", ""); + } +} From c4206d75d6060e661a9109d3119daa730d34ac6c Mon Sep 17 00:00:00 2001 From: Louis DeJardin <lodejard@microsoft.com> Date: Mon, 13 Feb 2012 16:38:06 -0800 Subject: [PATCH 24/59] Combining listTables and queryTables operations. Fixes #216 Parameterless listTables and queryTables have identical behavior. Add prefix property to QueryTablesOptions. If prefix and query filter are both provided, they are combined by an 'and' expression. --- .../services/table/TableContract.java | 5 -- .../TableExceptionProcessor.java | 27 ---------- .../table/implementation/TableRestProxy.java | 49 +++++++++++-------- .../table/models/ListTablesOptions.java | 20 -------- .../table/models/QueryTablesOptions.java | 10 ++++ .../table/TableServiceIntegrationTest.java | 25 +++------- 6 files changed, 45 insertions(+), 91 deletions(-) delete mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ListTablesOptions.java diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 63ec2c1b2086..233f3d800140 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -24,7 +24,6 @@ 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; @@ -54,10 +53,6 @@ public interface TableContract extends FilterableService<TableContract> { GetTableResult getTable(String table, TableServiceOptions options) throws ServiceException; - QueryTablesResult listTables() throws ServiceException; - - QueryTablesResult listTables(ListTablesOptions options) throws ServiceException; - QueryTablesResult queryTables() throws ServiceException; QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException; 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 index c8bf695b7cdd..8f4bc110d4da 100644 --- 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 @@ -31,7 +31,6 @@ 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; @@ -222,32 +221,6 @@ public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceE } } - @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 { 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 index 096c5b4ec312..2e32f907f46c 100644 --- 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 @@ -67,7 +67,6 @@ 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; @@ -311,23 +310,6 @@ public GetTableResult getTable(String table, TableServiceOptions options) throws 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()); @@ -335,9 +317,36 @@ public QueryTablesResult queryTables() throws ServiceException { @Override public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException { + + Query query = options.getQuery(); + String nextTableName = options.getNextTableName(); + String prefix = options.getPrefix(); + + if (prefix != null) { + // Append Max char to end '{' is 1 + 'z' in AsciiTable ==> upperBound is prefix + '{' + Filter prefixFilter = Filter.and(Filter.ge(Filter.litteral("TableName"), Filter.constant(prefix)), + Filter.le(Filter.litteral("TableName"), Filter.constant(prefix + "{"))); + + // a new query is needed if prefix alone is passed in + if (query == null) { + query = new Query(); + } + + // examine the existing filter on the query + if (query.getFilter() == null) { + // use the prefix filter if the query filter is null + query.setFilter(prefixFilter); + } + else { + // combine and use the prefix filter if the query filter exists + Filter combinedFilter = Filter.and(query.getFilter(), prefixFilter); + query.setFilter(combinedFilter); + } + } + WebResource webResource = getResource(options).path("Tables"); - webResource = addOptionalQuery(webResource, options.getQuery()); - webResource = addOptionalQueryParam(webResource, "NextTableName", options.getNextTableName()); + webResource = addOptionalQuery(webResource, query); + webResource = addOptionalQueryParam(webResource, "NextTableName", nextTableName); WebResource.Builder builder = webResource.getRequestBuilder(); builder = addTableRequestHeaders(builder); 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 deleted file mode 100644 index 25f042b564f0..000000000000 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/ListTablesOptions.java +++ /dev/null @@ -1,20 +0,0 @@ -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/QueryTablesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryTablesOptions.java index d402cd8527cc..b910ff6364d0 100644 --- 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 @@ -3,6 +3,7 @@ public class QueryTablesOptions extends TableServiceOptions { private String nextTableName; private Query query; + private String prefix; @Override public QueryTablesOptions setTimeout(Integer timeout) { @@ -27,4 +28,13 @@ public QueryTablesOptions setNextTableName(String nextTableName) { this.nextTableName = nextTableName; return this; } + + public String getPrefix() { + return prefix; + } + + public QueryTablesOptions setPrefix(String prefix) { + this.prefix = prefix; + return this; + } } 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 index c8143349741d..4a4146a5055d 100644 --- 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 @@ -40,10 +40,10 @@ 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.QueryTablesOptions; import com.microsoft.windowsazure.services.table.models.QueryTablesResult; import com.microsoft.windowsazure.services.table.models.ServiceProperties; import com.microsoft.windowsazure.services.table.models.TableEntry; @@ -117,7 +117,7 @@ private static void createTables(TableContract service, String prefix, String[] // 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<String> containers = listTables(service, prefix); + Set<String> containers = queryTables(service, prefix); for (String item : list) { if (!containers.contains(item)) { service.createTable(item); @@ -126,7 +126,7 @@ private static void createTables(TableContract service, String prefix, String[] } private static void deleteTables(TableContract service, String prefix, String[] list) throws Exception { - Set<String> containers = listTables(service, prefix); + Set<String> containers = queryTables(service, prefix); for (String item : list) { if (containers.contains(item)) { service.deleteTable(item); @@ -145,9 +145,9 @@ private static void deleteAllTables(TableContract service, String[] list) throws } } - private static Set<String> listTables(TableContract service, String prefix) throws Exception { + private static Set<String> queryTables(TableContract service, String prefix) throws Exception { HashSet<String> result = new HashSet<String>(); - QueryTablesResult list = service.listTables(new ListTablesOptions().setPrefix(prefix)); + QueryTablesResult list = service.queryTables(new QueryTablesOptions().setPrefix(prefix)); for (TableEntry item : list.getTables()) { result.add(item.getName()); } @@ -267,19 +267,6 @@ public void queryTablesWorks() throws Exception { 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 @@ -287,7 +274,7 @@ public void queryTablesWithPrefixWorks() throws Exception { TableContract service = TableService.create(config); // Act - QueryTablesResult result = service.listTables(new ListTablesOptions().setPrefix(testTablesPrefix)); + QueryTablesResult result = service.queryTables(new QueryTablesOptions().setPrefix(testTablesPrefix)); // Assert assertNotNull(result); From ff44325b9d916a6132bc46acd0b4ce1478f8707b Mon Sep 17 00:00:00 2001 From: Louis DeJardin <lodejard@microsoft.com> Date: Fri, 24 Feb 2012 14:20:39 -0800 Subject: [PATCH 25/59] Date time for table should have precision only down to seconds. Fixes #228 DateTime parser doesn't appear to round-trip accurately lower than that. --- .../implementation/ISO8601DateConverter.java | 4 ++++ .../table/implementation/AtomReaderWriter.java | 2 +- .../DefaultEdmValueConterter.java | 2 +- .../ISO8601DateConverterTests.java | 17 +++++++++++++++-- 4 files changed, 21 insertions(+), 4 deletions(-) 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 index e796bf00794c..9e6eef462738 100644 --- 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 @@ -33,6 +33,10 @@ public String format(Date date) { return getFormat().format(date); } + public String shortFormat(Date date) { + return getShortFormat().format(date); + } + public Date parse(String date) throws ParseException { if (date == null) return null; 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 index c5803bfb5411..75fdfe4c3a91 100644 --- 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 @@ -179,7 +179,7 @@ private InputStream generateEntry(PropertiesWriter propertiesWriter) { writer.writeEndElement(); // title writer.writeStartElement("updated"); - writer.writeCharacters(iso8601DateConverter.format(dateFactory.getDate())); + writer.writeCharacters(iso8601DateConverter.shortFormat(dateFactory.getDate())); writer.writeEndElement(); // updated writer.writeStartElement("author"); 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 index b062b3394ab9..ac505af93f3d 100644 --- 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 @@ -25,7 +25,7 @@ public String serialize(String edmType, Object value) { String serializedValue; if (value instanceof Date) { - serializedValue = iso8601DateConverter.format((Date) value); + serializedValue = iso8601DateConverter.shortFormat((Date) value); } else { serializedValue = value.toString(); 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 index 6f514dad26d3..bb9e36e07d9c 100644 --- 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 @@ -20,8 +20,6 @@ import org.junit.Test; -import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter; - public class ISO8601DateConverterTests { @Test public void shortFormatWorks() throws Exception { @@ -48,4 +46,19 @@ public void longFormatWorks() throws Exception { // Assert assertNotNull(result); } + + @Test + public void shortFormatRoundTrips() throws Exception { + // Arrange + ISO8601DateConverter converter = new ISO8601DateConverter(); + String value = "2012-01-12T00:35:58Z"; + + // Act + Date result = converter.parse(value); + String value2 = converter.shortFormat(result); + + // Assert + assertNotNull(result); + assertEquals(value, value2); + } } From 46e59a37c7078cfbb51f0e1a8ace582045ae8eb3 Mon Sep 17 00:00:00 2001 From: Louis DeJardin <lodejard@microsoft.com> Date: Fri, 24 Feb 2012 14:25:55 -0800 Subject: [PATCH 26/59] Use X-HTTP-Method for MERGE verb. Fixes #234 Java http libraries assert on known set of verbs --- .../services/table/implementation/TableRestProxy.java | 4 ++++ 1 file changed, 4 insertions(+) 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 index 096c5b4ec312..284fbe475ff7 100644 --- 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 @@ -466,6 +466,10 @@ private UpdateEntityResult putOrMergeEntityCore(String table, Entity entity, Str if (includeEtag) { builder = addIfMatchHeader(builder, entity.getEtag()); } + if (verb == "MERGE") { + builder = builder.header("X-HTTP-Method", "MERGE"); + verb = "POST"; + } builder = builder.entity(atomReaderWriter.generateEntityEntry(entity), "application/atom+xml"); From 2115c8e33a50d951163bf8da2038fad1431585da Mon Sep 17 00:00:00 2001 From: Louis DeJardin <lodejard@microsoft.com> Date: Fri, 24 Feb 2012 14:42:41 -0800 Subject: [PATCH 27/59] Bumping version to 0.2.0 --- microsoft-azure-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/microsoft-azure-api/pom.xml b/microsoft-azure-api/pom.xml index d9b0c4c4f2cf..c83d5dcc1a33 100644 --- a/microsoft-azure-api/pom.xml +++ b/microsoft-azure-api/pom.xml @@ -3,7 +3,7 @@ <modelVersion>4.0.0</modelVersion> <groupId>com.microsoft.windowsazure</groupId> <artifactId>microsoft-windowsazure-api</artifactId> - <version>0.1.3</version> + <version>0.2.0</version> <packaging>jar</packaging> <name>Microsoft Windows Azure Client API</name> From 4af2c067eeb7436768b94c2505d44665bead743e Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Mon, 27 Feb 2012 13:46:32 -0800 Subject: [PATCH 28/59] Adding copyright and license information to new table files. --- .../services/table/EdmValueConverter.java | 14 ++++++++++++++ .../windowsazure/services/table/Exports.java | 2 +- .../services/table/TableConfiguration.java | 2 +- .../services/table/TableContract.java | 2 +- .../services/table/TableService.java | 18 +++++++++--------- .../table/implementation/AtomReaderWriter.java | 14 ++++++++++++++ .../DefaultEdmValueConterter.java | 14 ++++++++++++++ .../DefaultXMLStreamFactory.java | 14 ++++++++++++++ .../table/implementation/HttpReaderWriter.java | 14 ++++++++++++++ .../implementation/InputStreamDataSource.java | 14 ++++++++++++++ .../table/implementation/MimeReaderWriter.java | 14 ++++++++++++++ .../table/implementation/SharedKeyFilter.java | 2 +- .../implementation/SharedKeyLiteFilter.java | 2 +- .../TableExceptionProcessor.java | 2 +- .../table/implementation/TableRestProxy.java | 2 +- .../table/implementation/XMLStreamFactory.java | 14 ++++++++++++++ .../services/table/models/BatchOperations.java | 14 ++++++++++++++ .../services/table/models/BatchResult.java | 14 ++++++++++++++ .../services/table/models/BinaryFilter.java | 14 ++++++++++++++ .../services/table/models/ConstantFilter.java | 14 ++++++++++++++ .../table/models/DeleteEntityOptions.java | 14 ++++++++++++++ .../services/table/models/EdmType.java | 14 ++++++++++++++ .../services/table/models/Entity.java | 14 ++++++++++++++ .../services/table/models/Filter.java | 14 ++++++++++++++ .../services/table/models/GetEntityResult.java | 14 ++++++++++++++ .../models/GetServicePropertiesResult.java | 18 +++++++++--------- .../services/table/models/GetTableResult.java | 14 ++++++++++++++ .../table/models/InsertEntityResult.java | 14 ++++++++++++++ .../services/table/models/LitteralFilter.java | 14 ++++++++++++++ .../services/table/models/Property.java | 14 ++++++++++++++ .../services/table/models/Query.java | 14 ++++++++++++++ .../table/models/QueryEntitiesOptions.java | 14 ++++++++++++++ .../table/models/QueryEntitiesResult.java | 14 ++++++++++++++ .../table/models/QueryTablesOptions.java | 14 ++++++++++++++ .../table/models/QueryTablesResult.java | 14 ++++++++++++++ .../services/table/models/RawStringFilter.java | 14 ++++++++++++++ .../table/models/ServiceProperties.java | 18 +++++++++--------- .../services/table/models/TableEntry.java | 14 ++++++++++++++ .../table/models/TableServiceOptions.java | 18 +++++++++--------- .../services/table/models/UnaryFilter.java | 14 ++++++++++++++ .../table/models/UpdateEntityResult.java | 14 ++++++++++++++ 41 files changed, 463 insertions(+), 43 deletions(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java index e7582255ef59..4a6fd5fb37fb 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/EdmValueConverter.java @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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; public interface EdmValueConverter { diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java index ee52dd47bfb2..ad0334ab03de 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/Exports.java @@ -1,5 +1,5 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java index 49c90e0ddf3d..33391fc9e711 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java index 233f3d800140..2bb4e1a3ac6a 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableContract.java @@ -1,5 +1,5 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java index 2e732e4fc1d6..c37f20da147f 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/TableService.java @@ -1,16 +1,16 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 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.table; 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 index 75fdfe4c3a91..e7b067f5d0ad 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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; 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 index ac505af93f3d..f7df91f533f5 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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.text.ParseException; 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 index 8504c88fa349..6202250942d4 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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.InputStream; 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 index 458acf6e0cd9..a0120eb41984 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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.IOException; 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 index 22c099a52fee..a15d80844155 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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.IOException; 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 index 9d2282ab08e6..30aa13ec6bfe 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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.IOException; 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 index 79305ce758c8..ba12447147c8 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 index 094fe429145a..6f9990616a82 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 index 8f4bc110d4da..29e8bd6dd812 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 index 80f16b99187b..bfeddfdb5c80 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. 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 index 0e97c0193ab5..064becc8b865 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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.InputStream; 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 index f1df873ac4da..5de01c01542a 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 java.util.ArrayList; 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 index 30a0b3beebd9..6ca4400068bb 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 java.util.ArrayList; 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 index 462be9a865c6..2cdb8473a26d 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 BinaryFilter extends Filter { 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 index 9dd16b1a1daa..098b28ae0e39 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 ConstantFilter extends Filter { 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 index 441f3020822b..161087d46834 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 DeleteEntityOptions extends TableServiceOptions { 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 index 5a82435b9337..0f0585f2e22a 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 EdmType { 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 index ce1d6dff3933..f74bda6d7003 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 java.util.Date; 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 index f0a54f58b09b..31a6daf581b2 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 Filter { 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 index 34195d479504..336c436da06c 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 GetEntityResult { 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 index 30ab7196d448..e56a2c8f9368 100644 --- 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 @@ -1,16 +1,16 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 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.table.models; 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 index de199b24f075..0e23a0d752c2 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 GetTableResult { 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 index 666fc9634da3..eca020395496 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 InsertEntityResult { 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 index d67f47162c4d..08a62dbf957b 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 LitteralFilter extends Filter { 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 index 1e90530e04f0..197584f920e6 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 Property { 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 index 57e7f8c30b66..cf3997dd7225 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 java.util.ArrayList; 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 index 1de1cd47426b..fe6842b62082 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 QueryEntitiesOptions extends TableServiceOptions { 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 index 3977ccc9b97d..9756661eef96 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 java.util.ArrayList; 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 index b910ff6364d0..13cc7fcf7002 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 QueryTablesOptions extends TableServiceOptions { 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 index dfd1c1b7f4aa..9c6158382dba 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 java.util.List; 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 index ffc8e38aef00..12aaedf407d4 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 RawStringFilter extends Filter { 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 index 2e068e4fe9df..73bffc9e03b6 100644 --- 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 @@ -1,16 +1,16 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 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.table.models; 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 index 917cecc3409c..2e520bc36db1 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 TableEntry { 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 index e42240885c15..69c19ecba554 100644 --- 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 @@ -1,16 +1,16 @@ /** - * Copyright 2011 Microsoft Corporation + * Copyright 2012 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.table.models; 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 index 42d4b7a870fc..f6da13830a0c 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 UnaryFilter extends Filter { 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 index 2db782e63693..dc18aa98996e 100644 --- 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 @@ -1,3 +1,17 @@ +/** + * Copyright 2012 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 UpdateEntityResult { From fd92be1a9c6bf28f87294e1312834b37c22a6c87 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:07:08 -0800 Subject: [PATCH 29/59] Fix issue 198 --- .../services/table/models/ServiceProperties.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 73bffc9e03b6..01a18543864b 100644 --- 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 @@ -42,9 +42,9 @@ public void setMetrics(Metrics metrics) { public static class Logging { private String version; - private Boolean delete; - private Boolean read; - private Boolean write; + private boolean delete; + private boolean read; + private boolean write; private RetentionPolicy retentionPolicy; @XmlElement(name = "RetentionPolicy") From f239a502e3bf9b9d78dbee3812144de2b81c56a8 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:14:53 -0800 Subject: [PATCH 30/59] Fix #227, #244, and #251 --- .../table/implementation/AtomReaderWriter.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) 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 index e7b067f5d0ad..906c4bdc4a75 100644 --- 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 @@ -80,6 +80,9 @@ public void write(XMLStreamWriter writer) throws XMLStreamException { if (value != null) { writer.writeCharacters(value); } + else { + writer.writeAttribute("m:null", "true"); + } writer.writeEndElement(); // property name @@ -276,12 +279,21 @@ private Map<String, Property> parseEntryProperties(XMLStreamReader xmlr) throws String edmType = xmlr.getAttributeValue(null, "type"); xmlr.next(); - String serializedValue = xmlr.getText(); + + // Use concatenation instead of StringBuilder as most text is just one element. + String serializedValue = ""; + while (!xmlr.isEndElement()) { + serializedValue += xmlr.getText(); + xmlr.next(); + } + Object value = edmValueConverter.deserialize(edmType, serializedValue); result.put(name, new Property().setEdmType(edmType).setValue(value)); - nextSignificant(xmlr); + if (!xmlr.isEndElement()) { + nextSignificant(xmlr); + } expect(xmlr, XMLStreamConstants.END_ELEMENT, name); } From 37e8b0f9ca6e7561c74aeb81dce0e49b1fc22289 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:19:17 -0800 Subject: [PATCH 31/59] Fix #225 --- .../table/implementation/DefaultEdmValueConterter.java | 7 +++++++ 1 file changed, 7 insertions(+) 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 index f7df91f533f5..41b38735afa5 100644 --- 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 @@ -22,6 +22,7 @@ import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter; import com.microsoft.windowsazure.services.table.EdmValueConverter; import com.microsoft.windowsazure.services.table.models.EdmType; +import com.sun.jersey.core.util.Base64; public class DefaultEdmValueConterter implements EdmValueConverter { @@ -41,6 +42,9 @@ public String serialize(String edmType, Object value) { if (value instanceof Date) { serializedValue = iso8601DateConverter.shortFormat((Date) value); } + else if (value instanceof byte[]) { + serializedValue = new String(Base64.encode((byte[]) value)); + } else { serializedValue = value.toString(); } @@ -73,6 +77,9 @@ else if (EdmType.INT32.equals(edmType)) { else if (EdmType.INT64.equals(edmType)) { return Long.parseLong(value); } + else if (EdmType.BINARY.equals(edmType)) { + return Base64.decode(value); + } return value; } From 960abc52657ddc33dca10f6e38a019721eae2d38 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:22:38 -0800 Subject: [PATCH 32/59] Fix #252 --- .../table/implementation/SharedKeyFilter.java | 2 +- .../services/table/implementation/TableRestProxy.java | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) 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 index ba12447147c8..7e2e95bb88f9 100644 --- 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 @@ -77,7 +77,7 @@ public void sign(ClientRequest cr) { private String getCanonicalizedResource(ClientRequest cr) { String result = "/" + this.getAccountName(); - result += cr.getURI().getPath(); + result += cr.getURI().getRawPath(); List<QueryParam> queryParams = SharedKeyUtils.getQueryParams(cr.getURI().getQuery()); for (QueryParam p : queryParams) { 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 index bfeddfdb5c80..e9a5dbc84649 100644 --- 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 @@ -159,7 +159,16 @@ private List<String> encodeODataURIValues(List<String> values) { } private String getEntityPath(String table, String partitionKey, String rowKey) { - return table + "(" + "PartitionKey='" + partitionKey + "',RowKey='" + rowKey + "')"; + String ret = "error"; + try { + ret = table + "(" + "PartitionKey='" + + URLEncoder.encode(partitionKey, "UTF-8").replace("+", "%20").replace("'", "%27") + "',RowKey='" + + URLEncoder.encode(rowKey, "UTF-8").replace("+", "%20").replace("'", "%27") + "')"; + System.out.println("ret : " + ret); + } + catch (UnsupportedEncodingException e) { + } + return ret; } private WebResource addOptionalQueryParam(WebResource webResource, String key, Object value) { From 4ac38a8b412eddb415a295d6121ba43df6fbd1ee Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:28:28 -0800 Subject: [PATCH 33/59] Fix #205 --- .../queue/implementation/QueueRestProxy.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/implementation/QueueRestProxy.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/implementation/QueueRestProxy.java index 4677d0431b81..5816a1bec264 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/implementation/QueueRestProxy.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/implementation/QueueRestProxy.java @@ -152,6 +152,9 @@ public void createQueue(String queue) throws ServiceException { } public void createQueue(String queue, CreateQueueOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue); WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION); @@ -165,6 +168,9 @@ public void deleteQueue(String queue) throws ServiceException { } public void deleteQueue(String queue, QueueServiceOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue); WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION); @@ -195,6 +201,9 @@ public GetQueueMetadataResult getQueueMetadata(String queue) throws ServiceExcep } public GetQueueMetadataResult getQueueMetadata(String queue, QueueServiceOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).queryParam("comp", "metadata"); Builder builder = webResource.header("x-ms-version", API_VERSION); @@ -216,6 +225,9 @@ public void setQueueMetadata(String queue, HashMap<String, String> metadata) thr public void setQueueMetadata(String queue, HashMap<String, String> metadata, QueueServiceOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).queryParam("comp", "metadata"); WebResource.Builder builder = webResource.header("x-ms-version", API_VERSION); @@ -229,6 +241,9 @@ public void createMessage(String queue, String messageText) throws ServiceExcept } public void createMessage(String queue, String messageText, CreateMessageOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).path("messages"); webResource = addOptionalQueryParam(webResource, "visibilitytimeout", options.getVisibilityTimeoutInSeconds()); webResource = addOptionalQueryParam(webResource, "messagettl", options.getTimeToLiveInSeconds()); @@ -249,6 +264,11 @@ public UpdateMessageResult updateMessage(String queue, String messageId, String public UpdateMessageResult updateMessage(String queue, String messageId, String popReceipt, String messageText, int visibilityTimeoutInSeconds, QueueServiceOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + if (messageId == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).path("messages").path(messageId); webResource = addOptionalQueryParam(webResource, "popreceipt", popReceipt); webResource = addOptionalQueryParam(webResource, "visibilitytimeout", visibilityTimeoutInSeconds); @@ -272,6 +292,9 @@ public ListMessagesResult listMessages(String queue) throws ServiceException { } public ListMessagesResult listMessages(String queue, ListMessagesOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).path("messages"); webResource = addOptionalQueryParam(webResource, "visibilitytimeout", options.getVisibilityTimeoutInSeconds()); webResource = addOptionalQueryParam(webResource, "numofmessages", options.getNumberOfMessages()); @@ -286,6 +309,9 @@ public PeekMessagesResult peekMessages(String queue) throws ServiceException { } public PeekMessagesResult peekMessages(String queue, PeekMessagesOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).path("messages").queryParam("peekonly", "true"); webResource = addOptionalQueryParam(webResource, "numofmessages", options.getNumberOfMessages()); @@ -300,6 +326,11 @@ public void deleteMessage(String queue, String messageId, String popReceipt) thr public void deleteMessage(String queue, String messageId, String popReceipt, QueueServiceOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + if (messageId == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).path("messages").path(messageId); webResource = addOptionalQueryParam(webResource, "popreceipt", popReceipt); @@ -313,6 +344,9 @@ public void clearMessages(String queue) throws ServiceException { } public void clearMessages(String queue, QueueServiceOptions options) throws ServiceException { + if (queue == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(queue).path("messages"); Builder builder = webResource.header("x-ms-version", API_VERSION); From c372a0081f528a21f957c16337771a30d1276259 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:32:32 -0800 Subject: [PATCH 34/59] Fix #257 --- .../windowsazure/services/core/utils/pipeline/Exports.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java index 234581b023fb..f7aae2cd9188 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java @@ -22,6 +22,7 @@ import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; +import com.sun.jersey.api.client.filter.LoggingFilter; public class Exports implements Builder.Exports { @@ -52,7 +53,7 @@ public Client create(String profile, Builder builder, Map<String, Object> proper public HttpURLConnectionClient create(String profile, Builder builder, Map<String, Object> properties) { ClientConfig clientConfig = (ClientConfig) properties.get("ClientConfig"); HttpURLConnectionClient client = HttpURLConnectionClient.create(clientConfig); - //client.addFilter(new LoggingFilter()); + client.addFilter(new LoggingFilter()); return client; } }); From 2b3d4bf8bf76739dcc9d7267dfe877e981657a78 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:39:16 -0800 Subject: [PATCH 35/59] Fix #228 --- .../implementation/ISO8601DateConverter.java | 59 ++++++++++++------- .../implementation/AtomReaderWriter.java | 2 +- .../DefaultEdmValueConterter.java | 2 +- .../ISO8601DateConverterTests.java | 2 +- 4 files changed, 41 insertions(+), 24 deletions(-) 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 index 9e6eef462738..9fa415c44c8f 100644 --- 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 @@ -17,6 +17,7 @@ import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.TimeZone; @@ -26,38 +27,54 @@ */ 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'"; + private static final String DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"; + private static final String DATETIME_PATTERN_NO_S = "yyyy-MM-dd'T'HH:mm'Z'"; + private static final String DATETIME_PATTERN_NO_MS = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + private static final String DATETIME_PATTERN_TO_DECIMAL = "yyyy-MM-dd'T'HH:mm:ss."; public String format(Date date) { - return getFormat().format(date); - } - - public String shortFormat(Date date) { - return getShortFormat().format(date); + DateFormat iso8601Format = new SimpleDateFormat(DATETIME_PATTERN, Locale.US); + iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT")); + return iso8601Format.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); - } + int length = date.length(); + if (length == 17) { + // [2012-01-04T23:21Z] length = 17 + return parseDateFromString(date, DATETIME_PATTERN_NO_S); + } + else if (length == 20) { + // [2012-01-04T23:21:59Z] length = 20 + return parseDateFromString(date, DATETIME_PATTERN_NO_MS); + } + else if (length >= 22 && length <= 28) { + // [2012-01-04T23:21:59.1Z] length = 22 + // [2012-01-04T23:21:59.1234567Z] length = 28 + // Need to handle the milliseconds gently. - private DateFormat getFormat() { - DateFormat iso8601Format = new SimpleDateFormat(DATETIME_PATTERN, Locale.US); - iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT")); - return iso8601Format; + Date allExceptMilliseconds = parseDateFromString(date, DATETIME_PATTERN_TO_DECIMAL); + long timeWithSecondGranularity = allExceptMilliseconds.getTime(); + // Decimal point is at 19 + String secondDecimalString = date.substring(19, date.indexOf('Z')); + Float secondDecimal = Float.parseFloat(secondDecimalString); + int milliseconds = Math.round(secondDecimal * 1000); + long timeInMS = timeWithSecondGranularity + milliseconds; + Calendar cal = Calendar.getInstance(); + cal.setTimeInMillis(timeInMS); + return cal.getTime(); + } + else { + throw new IllegalArgumentException(String.format("Invalid Date String: %s", date)); + } } - private DateFormat getShortFormat() { - DateFormat iso8601Format = new SimpleDateFormat(SHORT_DATETIME_PATTERN, Locale.US); + public static Date parseDateFromString(final String value, final String pattern) throws ParseException { + DateFormat iso8601Format = new SimpleDateFormat(pattern, Locale.US); iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT")); - return iso8601Format; + return iso8601Format.parse(value); } } 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 index e7b067f5d0ad..3324a62d68c5 100644 --- 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 @@ -193,7 +193,7 @@ private InputStream generateEntry(PropertiesWriter propertiesWriter) { writer.writeEndElement(); // title writer.writeStartElement("updated"); - writer.writeCharacters(iso8601DateConverter.shortFormat(dateFactory.getDate())); + writer.writeCharacters(iso8601DateConverter.format(dateFactory.getDate())); writer.writeEndElement(); // updated writer.writeStartElement("author"); 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 index f7df91f533f5..a9ec25452eea 100644 --- 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 @@ -39,7 +39,7 @@ public String serialize(String edmType, Object value) { String serializedValue; if (value instanceof Date) { - serializedValue = iso8601DateConverter.shortFormat((Date) value); + serializedValue = iso8601DateConverter.format((Date) value); } else { serializedValue = value.toString(); 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 index bb9e36e07d9c..a777b001c584 100644 --- 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 @@ -55,7 +55,7 @@ public void shortFormatRoundTrips() throws Exception { // Act Date result = converter.parse(value); - String value2 = converter.shortFormat(result); + String value2 = converter.format(result); // Assert assertNotNull(result); From 3bf1d0940c3fce030a865edc3c99bdb5bb965a3f Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:46:31 -0800 Subject: [PATCH 36/59] Fix #221 and #231 --- .../table/implementation/TableRestProxy.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) 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 index bfeddfdb5c80..fb22323fa357 100644 --- 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 @@ -282,6 +282,9 @@ public void setServiceProperties(ServiceProperties serviceProperties) throws Ser @Override public void setServiceProperties(ServiceProperties serviceProperties, TableServiceOptions options) throws ServiceException { + if (serviceProperties == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path("/").queryParam("resType", "service") .queryParam("comp", "properties"); @@ -297,6 +300,9 @@ public GetTableResult getTable(String table) throws ServiceException { @Override public GetTableResult getTable(String table, TableServiceOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path("Tables" + "('" + table + "')"); WebResource.Builder builder = webResource.getRequestBuilder(); @@ -369,6 +375,9 @@ public void createTable(String table) throws ServiceException { @Override public void createTable(String table, TableServiceOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path("Tables"); WebResource.Builder builder = webResource.getRequestBuilder(); @@ -387,6 +396,9 @@ public void deleteTable(String table) throws ServiceException { @Override public void deleteTable(String table, TableServiceOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path("Tables" + "('" + table + "')"); WebResource.Builder builder = webResource.getRequestBuilder(); @@ -405,6 +417,9 @@ public InsertEntityResult insertEntity(String table, Entity entity) throws Servi @Override public InsertEntityResult insertEntity(String table, Entity entity, TableServiceOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(table); WebResource.Builder builder = webResource.getRequestBuilder(); @@ -434,7 +449,7 @@ public UpdateEntityResult updateEntity(String table, Entity entity, TableService @Override public UpdateEntityResult mergeEntity(String table, Entity entity) throws ServiceException { - return updateEntity(table, entity, new TableServiceOptions()); + return mergeEntity(table, entity, new TableServiceOptions()); } @Override @@ -456,7 +471,7 @@ public UpdateEntityResult insertOrReplaceEntity(String table, Entity entity, Tab @Override public UpdateEntityResult insertOrMergeEntity(String table, Entity entity) throws ServiceException { - return insertOrReplaceEntity(table, entity, new TableServiceOptions()); + return insertOrMergeEntity(table, entity, new TableServiceOptions()); } @Override @@ -467,6 +482,9 @@ public UpdateEntityResult insertOrMergeEntity(String table, Entity entity, Table private UpdateEntityResult putOrMergeEntityCore(String table, Entity entity, String verb, boolean includeEtag, TableServiceOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path( getEntityPath(table, entity.getPartitionKey(), entity.getRowKey())); @@ -499,6 +517,9 @@ public void deleteEntity(String table, String partitionKey, String rowKey) throw @Override public void deleteEntity(String table, String partitionKey, String rowKey, DeleteEntityOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(getEntityPath(table, partitionKey, rowKey)); WebResource.Builder builder = webResource.getRequestBuilder(); @@ -517,6 +538,9 @@ public GetEntityResult getEntity(String table, String partitionKey, String rowKe @Override public GetEntityResult getEntity(String table, String partitionKey, String rowKey, TableServiceOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + WebResource webResource = getResource(options).path(getEntityPath(table, partitionKey, rowKey)); WebResource.Builder builder = webResource.getRequestBuilder(); @@ -538,6 +562,11 @@ public QueryEntitiesResult queryEntities(String table) throws ServiceException { @Override public QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions options) throws ServiceException { + if (table == null) + throw new NullPointerException(); + if (options == null) + options = new QueryEntitiesOptions(); + WebResource webResource = getResource(options).path(table); webResource = addOptionalQuery(webResource, options.getQuery()); webResource = addOptionalQueryParam(webResource, "NextPartitionKey", From 62435263d18dd3ca63f2293ca3abb9c84fe73045 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 10:54:15 -0800 Subject: [PATCH 37/59] Fix #213 --- .../table/implementation/TableRestProxy.java | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) 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 index bfeddfdb5c80..c65db1d6bbf0 100644 --- 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 @@ -205,9 +205,47 @@ private void buildFilterExpression(Filter filter, StringBuilder sb) { sb.append(((LitteralFilter) filter).getLitteral()); } else if (filter instanceof ConstantFilter) { - sb.append("'"); - sb.append(((ConstantFilter) filter).getValue()); - sb.append("'"); + // Look up http://www.odata.org/developers/protocols/overview + Object value = ((ConstantFilter) filter).getValue(); + if (value == null) { + sb.append("null"); + } + else if (value.getClass() == Long.class) { + sb.append(value + "L"); + } + else if (value.getClass() == Date.class) { + ISO8601DateConverter dc = new ISO8601DateConverter(); + sb.append("datetime'"); + sb.append(dc.format((Date) value)); + sb.append("'"); + } + else if (value.getClass() == String.class) { + // Need to special case guids, which argues for using UUID. + try { + UUID.fromString((String) value); + // Looks like guid + sb.append("(guid'" + value + "')"); + } + catch (Exception ex) { + // Not guid + sb.append("'"); + sb.append(((String) value).replace("'", "''")); + sb.append("'"); + + } + } + else if (value.getClass() == byte[].class) { + byte[] x = (byte[]) value; + sb.append("binary'"); + for (int j = 0; j < x.length; j++) { + sb.append(BASE_16_CHARS.charAt((x[j] & 0xF0) >>> 4)); + sb.append(BASE_16_CHARS.charAt((x[j] & 0x0F))); + } + sb.append("'"); + } + else { + sb.append(value); + } } else if (filter instanceof UnaryFilter) { sb.append(((UnaryFilter) filter).getOperator()); From 0078bd42acfa70803c3ba71799175afe21137dc5 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 11:23:29 -0800 Subject: [PATCH 38/59] Fix for #243, #245, and #254 --- .../table/implementation/TableRestProxy.java | 60 +++++++++++++++---- 1 file changed, 47 insertions(+), 13 deletions(-) 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 index bfeddfdb5c80..af01e789b06e 100644 --- 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 @@ -22,6 +22,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; +import java.util.HashSet; import java.util.List; import javax.activation.DataSource; @@ -30,6 +31,9 @@ import javax.mail.Header; import javax.mail.internet.InternetHeaders; import javax.mail.internet.MimeMultipart; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; import com.microsoft.windowsazure.services.blob.implementation.ISO8601DateConverter; import com.microsoft.windowsazure.services.blob.implementation.RFC1123DateConverter; @@ -83,6 +87,10 @@ import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource.Builder; import com.sun.jersey.core.header.InBoundHeaders; +import com.sun.jersey.core.spi.component.ProviderFactory; +import com.sun.jersey.core.spi.component.ProviderServices; +import com.sun.jersey.core.spi.factory.MessageBodyFactory; +import com.sun.jersey.spi.inject.ClientSide; public class TableRestProxy implements TableContract { private static final String API_VERSION = "2011-08-18"; @@ -700,14 +708,7 @@ private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations List<DataSource> 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<Entry> result = new ArrayList<Entry>(); + Entry[] entries = new Entry[operations.getOperations().size()]; for (int i = 0; i < parts.size(); i++) { DataSource ds = parts.get(i); Operation operation = operations.getOperations().get(i); @@ -726,7 +727,13 @@ private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations inBoundHeaders.putSingle(header.getName(), header.getValue()); } - ClientResponse dummyResponse = new ClientResponse(status.getStatus(), inBoundHeaders, content, null); + ProviderServices providerServices = new ProviderServices(ClientSide.class, new ProviderFactory(null), + new HashSet<Class<?>>(), new HashSet<Class<?>>()); + MessageBodyFactory bodyContext = new MessageBodyFactory(providerServices, false); + // TODO: This call causes lots of warnings from Jersey. Need to figure out how to silence them. + bodyContext.init(); + ClientResponse dummyResponse = new ClientResponse(status.getStatus(), inBoundHeaders, content, + bodyContext); // Wrap into a ServiceException UniformInterfaceException exception = new UniformInterfaceException(dummyResponse); @@ -734,24 +741,51 @@ private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations serviceException = ServiceExceptionFactory.process("table", serviceException); Error error = new Error().setError(serviceException); - result.add(error); + // Parse the message to find which operation caused this error. + try { + XMLInputFactory xmlStreamFactory = XMLInputFactory.newFactory(); + XMLStreamReader xmlr = xmlStreamFactory.createXMLStreamReader(new ByteArrayInputStream( + serviceException.getRawResponseBody().getBytes())); + while (xmlr.hasNext()) { + xmlr.next(); + if (xmlr.isStartElement() && "message".equals(xmlr.getLocalName())) { + xmlr.next(); + // Process "message" elements only + String message = xmlr.getText(); + int colonIndex = message.indexOf(':'); + String errorOpId = message.substring(0, colonIndex); + int opId = Integer.parseInt(errorOpId); + entries[opId] = error; + } + } + xmlr.close(); + } + catch (XMLStreamException e1) { + // TODO: What to throw here? + } + } else if (operation instanceof InsertEntityOperation) { InsertEntity opResult = new InsertEntity().setEntity(atomReaderWriter.parseEntityEntry(content)); - result.add(opResult); + entries[i] = 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); + entries[i] = opResult; } else if (operation instanceof DeleteEntityOperation) { DeleteEntity opResult = new DeleteEntity(); - result.add(opResult); + entries[i] = opResult; } } + List<Entry> result = new ArrayList<Entry>(); + for (int i = 0; i < entries.length; i++) { + result.add(entries[i]); + } + return result; } From b329c8941e43a28a39f9776ff1a4835d3d262566 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 8 Mar 2012 11:33:01 -0800 Subject: [PATCH 39/59] Fix #209 --- .../services/table/implementation/TableRestProxy.java | 1 - .../services/table/models/DeleteEntityOptions.java | 6 ------ .../services/table/models/QueryEntitiesOptions.java | 6 ------ .../services/table/models/QueryTablesOptions.java | 6 ------ .../services/table/models/TableServiceOptions.java | 11 ----------- 5 files changed, 30 deletions(-) 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 index bfeddfdb5c80..11c59390988f 100644 --- 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 @@ -249,7 +249,6 @@ private WebResource.Builder addIfMatchHeader(WebResource.Builder builder, String 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)); } 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 index 161087d46834..4cce1e14a24a 100644 --- 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 @@ -17,12 +17,6 @@ public class DeleteEntityOptions extends TableServiceOptions { private String etag; - @Override - public DeleteEntityOptions setTimeout(Integer timeout) { - super.setTimeout(timeout); - return this; - } - public String getEtag() { return etag; } 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 index fe6842b62082..31c7d05652bc 100644 --- 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 @@ -19,12 +19,6 @@ public class QueryEntitiesOptions extends TableServiceOptions { public String nextPartitionKey; public String nextRowKey; - @Override - public QueryEntitiesOptions setTimeout(Integer timeout) { - super.setTimeout(timeout); - return this; - } - public Query getQuery() { return query; } 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 index 13cc7fcf7002..36b975d7a690 100644 --- 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 @@ -19,12 +19,6 @@ public class QueryTablesOptions extends TableServiceOptions { private Query query; private String prefix; - @Override - public QueryTablesOptions setTimeout(Integer timeout) { - super.setTimeout(timeout); - return this; - } - public Query getQuery() { return query; } 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 index 69c19ecba554..c4a733a6e3a6 100644 --- 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 @@ -15,15 +15,4 @@ 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; - } } From 1782b8445e1463fb6c1e4f768c9475b45c2e9c68 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 29 Mar 2012 13:27:35 -0700 Subject: [PATCH 40/59] Make fix suggested in code review feedback. --- .../services/table/implementation/AtomReaderWriter.java | 3 --- 1 file changed, 3 deletions(-) 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 index 906c4bdc4a75..525c2adf086a 100644 --- 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 @@ -291,9 +291,6 @@ private Map<String, Property> parseEntryProperties(XMLStreamReader xmlr) throws result.put(name, new Property().setEdmType(edmType).setValue(value)); - if (!xmlr.isEndElement()) { - nextSignificant(xmlr); - } expect(xmlr, XMLStreamConstants.END_ELEMENT, name); } From 84bcfaf6159091bf6ce1a1b4d9a351d6e9d9a362 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Tue, 3 Apr 2012 08:09:51 -0700 Subject: [PATCH 41/59] Update unit tests to cover EDM BINARY type. --- .../table/TableServiceIntegrationTest.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) 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 index 4a4146a5055d..5f14d2d82a1f 100644 --- 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 @@ -300,10 +300,11 @@ public void insertEntityWorks() throws Exception { // Arrange Configuration config = createConfiguration(); TableContract service = TableService.create(config); + byte[] binaryData = new byte[] { 1, 2, 3, 4 }; 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()); + .setProperty("test5", EdmType.DATETIME, new Date()).setProperty("test6", EdmType.BINARY, binaryData); // Act InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity); @@ -331,6 +332,14 @@ public void insertEntityWorks() throws Exception { assertNotNull(result.getEntity().getProperty("test5")); assertTrue(result.getEntity().getProperty("test5").getValue() instanceof Date); + + assertNotNull(result.getEntity().getProperty("test6")); + assertTrue(result.getEntity().getProperty("test6").getValue() instanceof byte[]); + byte[] returnedBinaryData = (byte[]) result.getEntity().getProperty("test6").getValue(); + assertEquals(binaryData.length, returnedBinaryData.length); + for (int i = 0; i < binaryData.length; i++) { + assertEquals(binaryData[i], returnedBinaryData[i]); + } } @Test @@ -465,10 +474,11 @@ public void getEntityWorks() throws Exception { // Arrange Configuration config = createConfiguration(); TableContract service = TableService.create(config); + byte[] binaryData = new byte[] { 1, 2, 3, 4 }; 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()); + .setProperty("test5", EdmType.DATETIME, new Date()).setProperty("test6", EdmType.BINARY, binaryData); // Act InsertEntityResult insertResult = service.insertEntity(TEST_TABLE_2, entity); @@ -498,6 +508,14 @@ public void getEntityWorks() throws Exception { assertNotNull(result.getEntity().getProperty("test5")); assertTrue(result.getEntity().getProperty("test5").getValue() instanceof Date); + + assertNotNull(result.getEntity().getProperty("test6")); + assertTrue(result.getEntity().getProperty("test6").getValue() instanceof byte[]); + byte[] returnedBinaryData = (byte[]) result.getEntity().getProperty("test6").getValue(); + assertEquals(binaryData.length, returnedBinaryData.length); + for (int i = 0; i < binaryData.length; i++) { + assertEquals(binaryData[i], returnedBinaryData[i]); + } } @Test From 6a569ecf939555a9dee642bc15fbe3eba540f437 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Tue, 3 Apr 2012 10:53:51 -0700 Subject: [PATCH 42/59] Replaced manual base16 conversion with Formatter call. Use existing StringBuilder instead of concat when possible. Add case for Byte[] (in addition to byte[]) Replace special case in String for UUID with seperate code path for UUID. Add unit tests. --- .../table/implementation/TableRestProxy.java | 51 +++++++------ .../table/TableServiceIntegrationTest.java | 72 +++++++++++++++++-- 2 files changed, 95 insertions(+), 28 deletions(-) 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 index c65db1d6bbf0..5b7875c0ab98 100644 --- 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 @@ -21,8 +21,11 @@ import java.net.URI; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.Enumeration; +import java.util.Formatter; import java.util.List; +import java.util.UUID; import javax.activation.DataSource; import javax.inject.Inject; @@ -205,41 +208,45 @@ private void buildFilterExpression(Filter filter, StringBuilder sb) { sb.append(((LitteralFilter) filter).getLitteral()); } else if (filter instanceof ConstantFilter) { - // Look up http://www.odata.org/developers/protocols/overview Object value = ((ConstantFilter) filter).getValue(); if (value == null) { sb.append("null"); } else if (value.getClass() == Long.class) { - sb.append(value + "L"); + sb.append(value); + sb.append("L"); } else if (value.getClass() == Date.class) { - ISO8601DateConverter dc = new ISO8601DateConverter(); + ISO8601DateConverter dateConverter = new ISO8601DateConverter(); sb.append("datetime'"); - sb.append(dc.format((Date) value)); + sb.append(dateConverter.format((Date) value)); sb.append("'"); } + else if (value.getClass() == UUID.class) { + sb.append("(guid'"); + sb.append(value); + sb.append("')"); + } else if (value.getClass() == String.class) { - // Need to special case guids, which argues for using UUID. - try { - UUID.fromString((String) value); - // Looks like guid - sb.append("(guid'" + value + "')"); - } - catch (Exception ex) { - // Not guid - sb.append("'"); - sb.append(((String) value).replace("'", "''")); - sb.append("'"); - - } + sb.append("'"); + sb.append(((String) value).replace("'", "''")); + sb.append("'"); } else if (value.getClass() == byte[].class) { - byte[] x = (byte[]) value; - sb.append("binary'"); - for (int j = 0; j < x.length; j++) { - sb.append(BASE_16_CHARS.charAt((x[j] & 0xF0) >>> 4)); - sb.append(BASE_16_CHARS.charAt((x[j] & 0x0F))); + sb.append("X'"); + byte[] byteArray = (byte[]) value; + Formatter formatter = new Formatter(sb); + for (byte b : byteArray) { + formatter.format("%02x", b); + } + sb.append("'"); + } + else if (value.getClass() == Byte[].class) { + sb.append("X'"); + Byte[] byteArray = (Byte[]) value; + Formatter formatter = new Formatter(sb); + for (Byte b : byteArray) { + formatter.format("%02x", b); } sb.append("'"); } 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 index 4a4146a5055d..d5d2ceafc8de 100644 --- 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 @@ -592,13 +592,16 @@ public void queryEntitiesWithFilterWorks() throws Exception { TableContract service = TableService.create(config); String table = TEST_TABLE_5; int numberOfEntries = 5; + Entity[] entities = new Entity[numberOfEntries]; 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); + entities[i] = new Entity().setPartitionKey("001").setRowKey("queryEntitiesWithFilterWorks-" + i) + .setProperty("test", EdmType.BOOLEAN, (i % 2 == 0)) + .setProperty("test2", EdmType.STRING, "'value'" + i).setProperty("test3", EdmType.INT32, i) + .setProperty("test4", EdmType.INT64, 12345678901L + i) + .setProperty("test5", EdmType.DATETIME, new Date(i * 1000)) + .setProperty("test6", EdmType.GUID, UUID.randomUUID()); + + service.insertEntity(table, entities[i]); } { @@ -623,6 +626,63 @@ public void queryEntitiesWithFilterWorks() throws Exception { 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.eq(Filter.litteral("test"), + Filter.constant(true))))); + + // Assert + assertNotNull(result); + assertEquals(3, result.getEntities().size()); + } + + { + // Act + QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query() + .setFilter(Filter.eq(Filter.litteral("test2"), Filter.constant("'value'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.eq(Filter.litteral("test4"), Filter.constant(12345678903L))))); + + // Assert + assertNotNull(result); + assertEquals(1, result.getEntities().size()); + assertEquals("queryEntitiesWithFilterWorks-2", result.getEntities().get(0).getRowKey()); + } + + { + // Act + QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query() + .setFilter(Filter.eq(Filter.litteral("test5"), Filter.constant(new Date(3000)))))); + + // 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.eq(Filter.litteral("test6"), + Filter.constant(entities[3].getPropertyValue("test6")))))); + + // Assert + assertNotNull(result); + assertEquals(1, result.getEntities().size()); + assertEquals("queryEntitiesWithFilterWorks-3", result.getEntities().get(0).getRowKey()); + } } @Test From adcf3ad86c73fa8f28595c240131e19a0295f7f3 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Tue, 3 Apr 2012 12:40:13 -0700 Subject: [PATCH 43/59] Added new ISO8601 unit tests Added back "shortFormat" function, needed by Blob --- .../ContainerACLDateAdapter.java | 2 +- .../implementation/ISO8601DateConverter.java | 12 +++- .../ISO8601DateConverterTests.java | 61 ++++++++++++++++++- 3 files changed, 70 insertions(+), 5 deletions(-) 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 af860e50936d..f1ea12dbaa76 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 @@ -30,6 +30,6 @@ public Date unmarshal(String arg0) throws Exception { @Override public String marshal(Date arg0) throws Exception { - return new ISO8601DateConverter().format(arg0); + return new ISO8601DateConverter().shortFormat(arg0); } } 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 index 9fa415c44c8f..41bb096c7daa 100644 --- 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 @@ -28,8 +28,8 @@ 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.SSS'Z'"; + private static final String SHORT_DATETIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final String DATETIME_PATTERN_NO_S = "yyyy-MM-dd'T'HH:mm'Z'"; - private static final String DATETIME_PATTERN_NO_MS = "yyyy-MM-dd'T'HH:mm:ss'Z'"; private static final String DATETIME_PATTERN_TO_DECIMAL = "yyyy-MM-dd'T'HH:mm:ss."; public String format(Date date) { @@ -38,6 +38,12 @@ public String format(Date date) { return iso8601Format.format(date); } + public String shortFormat(Date date) { + DateFormat iso8601Format = new SimpleDateFormat(SHORT_DATETIME_PATTERN, Locale.US); + iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT")); + return iso8601Format.format(date); + } + public Date parse(String date) throws ParseException { if (date == null) return null; @@ -49,7 +55,7 @@ public Date parse(String date) throws ParseException { } else if (length == 20) { // [2012-01-04T23:21:59Z] length = 20 - return parseDateFromString(date, DATETIME_PATTERN_NO_MS); + return parseDateFromString(date, SHORT_DATETIME_PATTERN); } else if (length >= 22 && length <= 28) { // [2012-01-04T23:21:59.1Z] length = 22 @@ -72,7 +78,7 @@ else if (length >= 22 && length <= 28) { } } - public static Date parseDateFromString(final String value, final String pattern) throws ParseException { + private static Date parseDateFromString(final String value, final String pattern) throws ParseException { DateFormat iso8601Format = new SimpleDateFormat(pattern, Locale.US); iso8601Format.setTimeZone(TimeZone.getTimeZone("GMT")); return iso8601Format.parse(value); 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 index a777b001c584..c69d18d8f1eb 100644 --- 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 @@ -16,7 +16,9 @@ import static org.junit.Assert.*; +import java.util.Calendar; import java.util.Date; +import java.util.TimeZone; import org.junit.Test; @@ -29,9 +31,23 @@ public void shortFormatWorks() throws Exception { // Act Date result = converter.parse(value); + String value2 = converter.format(result); // Assert assertNotNull(result); + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(result); + calendar.setTimeZone(TimeZone.getTimeZone("GMT")); + assertEquals("Year", 2012, calendar.get(Calendar.YEAR)); + assertEquals("Month", 1, calendar.get(Calendar.MONTH) + 1); + assertEquals("Day", 12, calendar.get(Calendar.DAY_OF_MONTH)); + assertEquals("Hour", 0, calendar.get(Calendar.HOUR)); + assertEquals("Minute", 35, calendar.get(Calendar.MINUTE)); + assertEquals("Second", 58, calendar.get(Calendar.SECOND)); + assertEquals("Millisecond", 0, calendar.get(Calendar.MILLISECOND)); + + assertEquals("2012-01-12T00:35:58.000Z", value2); } @Test @@ -42,9 +58,50 @@ public void longFormatWorks() throws Exception { // Act Date result = converter.parse(value); + String value2 = converter.format(result); // Assert assertNotNull(result); + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(result); + calendar.setTimeZone(TimeZone.getTimeZone("GMT")); + assertEquals("Year", 2012, calendar.get(Calendar.YEAR)); + assertEquals("Month", 1, calendar.get(Calendar.MONTH) + 1); + assertEquals("Day", 12, calendar.get(Calendar.DAY_OF_MONTH)); + assertEquals("Hour", 0, calendar.get(Calendar.HOUR)); + assertEquals("Minute", 35, calendar.get(Calendar.MINUTE)); + assertEquals("Second", 58, calendar.get(Calendar.SECOND)); + assertEquals("Millisecond", 123, calendar.get(Calendar.MILLISECOND)); + + assertEquals("2012-01-12T00:35:58.123Z", value2); + } + + @Test + public void mixedFormatWorks() throws Exception { + // Arrange + ISO8601DateConverter converter = new ISO8601DateConverter(); + String value = "2012-01-12T00:35:58.12Z"; + + // Act + Date result = converter.parse(value); + String value2 = converter.format(result); + + // Assert + assertNotNull(result); + + Calendar calendar = Calendar.getInstance(); + calendar.setTime(result); + calendar.setTimeZone(TimeZone.getTimeZone("GMT")); + assertEquals("Year", 2012, calendar.get(Calendar.YEAR)); + assertEquals("Month", 1, calendar.get(Calendar.MONTH) + 1); + assertEquals("Day", 12, calendar.get(Calendar.DAY_OF_MONTH)); + assertEquals("Hour", 0, calendar.get(Calendar.HOUR)); + assertEquals("Minute", 35, calendar.get(Calendar.MINUTE)); + assertEquals("Second", 58, calendar.get(Calendar.SECOND)); + assertEquals("Millisecond", 120, calendar.get(Calendar.MILLISECOND)); + + assertEquals("2012-01-12T00:35:58.120Z", value2); } @Test @@ -55,10 +112,12 @@ public void shortFormatRoundTrips() throws Exception { // Act Date result = converter.parse(value); - String value2 = converter.format(result); + String value2 = converter.shortFormat(result); + String value3 = converter.format(result); // Assert assertNotNull(result); assertEquals(value, value2); + assertEquals("2012-01-12T00:35:58.000Z", value3); } } From 2962f94c79ebbcc8b5db68c26daa18b6c75b27c9 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Tue, 3 Apr 2012 12:50:05 -0700 Subject: [PATCH 44/59] Making variable name more descriptive. --- .../services/blob/implementation/ISO8601DateConverter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 41bb096c7daa..34444b858a03 100644 --- 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 @@ -69,9 +69,9 @@ else if (length >= 22 && length <= 28) { Float secondDecimal = Float.parseFloat(secondDecimalString); int milliseconds = Math.round(secondDecimal * 1000); long timeInMS = timeWithSecondGranularity + milliseconds; - Calendar cal = Calendar.getInstance(); - cal.setTimeInMillis(timeInMS); - return cal.getTime(); + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(timeInMS); + return calendar.getTime(); } else { throw new IllegalArgumentException(String.format("Invalid Date String: %s", date)); From 2116310239a320d9083fc33256948479209212ce Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Tue, 3 Apr 2012 14:14:02 -0700 Subject: [PATCH 45/59] Added unit test Changed catch statement to perform a reasonable fallback if the URLEncoder call fails. Added missing imports statements. --- .../table/implementation/TableRestProxy.java | 15 ++++++----- .../table/TableServiceIntegrationTest.java | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) 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 index e9a5dbc84649..10c343b87bb9 100644 --- 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 @@ -18,7 +18,9 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.URI; +import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; @@ -159,16 +161,17 @@ private List<String> encodeODataURIValues(List<String> values) { } private String getEntityPath(String table, String partitionKey, String rowKey) { - String ret = "error"; + return table + "(" + "PartitionKey='" + safeEncode(partitionKey) + "',RowKey='" + safeEncode(rowKey) + "')"; + } + + private String safeEncode(String input) { + String fixSingleQuotes = input.replace("'", "''"); try { - ret = table + "(" + "PartitionKey='" - + URLEncoder.encode(partitionKey, "UTF-8").replace("+", "%20").replace("'", "%27") + "',RowKey='" - + URLEncoder.encode(rowKey, "UTF-8").replace("+", "%20").replace("'", "%27") + "')"; - System.out.println("ret : " + ret); + return URLEncoder.encode(fixSingleQuotes, "UTF-8").replace("+", "%20"); } catch (UnsupportedEncodingException e) { + return fixSingleQuotes; } - return ret; } private WebResource addOptionalQueryParam(WebResource webResource, String key, Object value) { 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 index 4a4146a5055d..7a7de013076c 100644 --- 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 @@ -437,6 +437,32 @@ public void deleteEntityWorks() throws Exception { // Assert } + @Test + public void deleteEntityTroublesomeKeyWorks() throws Exception { + System.out.println("deleteEntityTroublesomeKeyWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + Entity entity1 = new Entity().setPartitionKey("001").setRowKey("key with spaces"); + Entity entity2 = new Entity().setPartitionKey("001").setRowKey("key'with'quotes"); + Entity entity3 = new Entity().setPartitionKey("001").setRowKey("keyWithUnicode \uB2E4"); + Entity entity4 = new Entity().setPartitionKey("001").setRowKey("key 'with'' \uB2E4"); + + // Act + InsertEntityResult result1 = service.insertEntity(TEST_TABLE_2, entity1); + InsertEntityResult result2 = service.insertEntity(TEST_TABLE_2, entity2); + InsertEntityResult result3 = service.insertEntity(TEST_TABLE_2, entity3); + InsertEntityResult result4 = service.insertEntity(TEST_TABLE_2, entity4); + + service.deleteEntity(TEST_TABLE_2, result1.getEntity().getPartitionKey(), result1.getEntity().getRowKey()); + service.deleteEntity(TEST_TABLE_2, result2.getEntity().getPartitionKey(), result2.getEntity().getRowKey()); + service.deleteEntity(TEST_TABLE_2, result3.getEntity().getPartitionKey(), result3.getEntity().getRowKey()); + service.deleteEntity(TEST_TABLE_2, result4.getEntity().getPartitionKey(), result4.getEntity().getRowKey()); + + // Assert + } + @Test public void deleteEntityWithETagWorks() throws Exception { System.out.println("deleteEntityWithETagWorks()"); From 90442d27eb5ddcc8d38ec61acf6eb4cca4b78a2d Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Tue, 3 Apr 2012 21:36:53 -0700 Subject: [PATCH 46/59] Address root cause of the issue: the default response EntityInputStream not rewindable, yet is is needed in multiple areas of the parseBatchResponse. Revert workaround fix. --- .../services/core/utils/pipeline/Exports.java | 3 +-- .../table/implementation/TableRestProxy.java | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java index f7aae2cd9188..234581b023fb 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/Exports.java @@ -22,7 +22,6 @@ import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig; -import com.sun.jersey.api.client.filter.LoggingFilter; public class Exports implements Builder.Exports { @@ -53,7 +52,7 @@ public Client create(String profile, Builder builder, Map<String, Object> proper public HttpURLConnectionClient create(String profile, Builder builder, Map<String, Object> properties) { ClientConfig clientConfig = (ClientConfig) properties.get("ClientConfig"); HttpURLConnectionClient client = HttpURLConnectionClient.create(clientConfig); - client.addFilter(new LoggingFilter()); + //client.addFilter(new LoggingFilter()); return client; } }); 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 index bfeddfdb5c80..54b58bd8f20a 100644 --- 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 @@ -83,6 +83,7 @@ import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource.Builder; import com.sun.jersey.core.header.InBoundHeaders; +import com.sun.jersey.core.util.ReaderWriter; public class TableRestProxy implements TableContract { private static final String API_VERSION = "2011-08-18"; @@ -577,7 +578,13 @@ public BatchResult batch(BatchOperations operations, TableServiceOptions options ThrowIfError(response); BatchResult result = new BatchResult(); - result.setEntries(parseBatchResponse(response, operations)); + + try { + result.setEntries(parseBatchResponse(response, operations)); + } + catch (IOException e) { + throw new ServiceException(e); + } return result; } @@ -696,7 +703,15 @@ private DataSource createBatchDeleteEntityPart(String table, String partitionKey return bodyPartContent; } - private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations operations) { + private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations operations) throws IOException { + // Default stream cannot be reset, but it is needed by multiple parts of this method. + // Replace the default response stream with one that can be read multiple times. + ByteArrayOutputStream out = new ByteArrayOutputStream(); + InputStream in = response.getEntityInputStream(); + ReaderWriter.writeTo(in, out); + byte[] requestEntity = out.toByteArray(); + response.setEntityInputStream(new ByteArrayInputStream(requestEntity)); + List<DataSource> parts = mimeReaderWriter.parseParts(response.getEntityInputStream(), response.getHeaders() .getFirst("Content-Type")); From bea4d6ce2e7a87b429b56623d0518ff0028c328b Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Wed, 4 Apr 2012 09:22:55 -0700 Subject: [PATCH 47/59] Adding more validation to unit test --- .../table/TableServiceIntegrationTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 index 7a7de013076c..aca02d15bc3b 100644 --- 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 @@ -461,6 +461,27 @@ public void deleteEntityTroublesomeKeyWorks() throws Exception { service.deleteEntity(TEST_TABLE_2, result4.getEntity().getPartitionKey(), result4.getEntity().getRowKey()); // Assert + try { + service.getEntity(TEST_TABLE_2, result1.getEntity().getPartitionKey(), result1.getEntity().getRowKey()); + assertFalse("Expect an exception when getting an entity that does not exist", true); + } + catch (ServiceException e) { + assertEquals("expect getHttpStatusCode", 404, e.getHttpStatusCode()); + + } + + QueryEntitiesResult assertResult2 = service.queryEntities( + TEST_TABLE_2, + new QueryEntitiesOptions().setQuery(new Query().setFilter(Filter.eq(Filter.litteral("RowKey"), + Filter.constant("key'with'quotes"))))); + + assertEquals(0, assertResult2.getEntities().size()); + + QueryEntitiesResult assertResult3 = service.queryEntities(TEST_TABLE_2); + for (Entity entity : assertResult3.getEntities()) { + assertFalse("Entity3 should be removed from the table", entity3.getRowKey().equals(entity.getRowKey())); + assertFalse("Entity4 should be removed from the table", entity4.getRowKey().equals(entity.getRowKey())); + } } @Test From bbf8b07c835d45eaebbd1738def9d35d640044b5 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Wed, 4 Apr 2012 10:14:13 -0700 Subject: [PATCH 48/59] Minor name changes. --- .../services/table/implementation/TableRestProxy.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 index 54b58bd8f20a..0e4467856508 100644 --- 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 @@ -706,11 +706,10 @@ private DataSource createBatchDeleteEntityPart(String table, String partitionKey private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations operations) throws IOException { // Default stream cannot be reset, but it is needed by multiple parts of this method. // Replace the default response stream with one that can be read multiple times. - ByteArrayOutputStream out = new ByteArrayOutputStream(); - InputStream in = response.getEntityInputStream(); - ReaderWriter.writeTo(in, out); - byte[] requestEntity = out.toByteArray(); - response.setEntityInputStream(new ByteArrayInputStream(requestEntity)); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + InputStream inputStream = response.getEntityInputStream(); + ReaderWriter.writeTo(inputStream, byteArrayOutputStream); + response.setEntityInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())); List<DataSource> parts = mimeReaderWriter.parseParts(response.getEntityInputStream(), response.getHeaders() .getFirst("Content-Type")); From dc66cdafe78750f63d73958f621f0aadc36c6f63 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Wed, 4 Apr 2012 14:46:35 -0700 Subject: [PATCH 49/59] Add import statement missing from merge --- .../windowsazure/services/table/TableServiceIntegrationTest.java | 1 + 1 file changed, 1 insertion(+) 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 index daf68d549f11..20ffb6596727 100644 --- 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 @@ -19,6 +19,7 @@ import java.util.Date; import java.util.HashSet; import java.util.Set; +import java.util.UUID; import org.junit.AfterClass; import org.junit.BeforeClass; From 9a503baca68957372845668b108b9be76887357a Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Wed, 4 Apr 2012 23:06:20 -0700 Subject: [PATCH 50/59] Simplify the processing of the batch error messages to avoid Jersey SEVERE warnings. Throw a reasonable error when the Housekeeping: Add suppression markers for unfixable warnings. --- .../implementation/HttpReaderWriter.java | 1 + .../table/implementation/TableRestProxy.java | 46 ++++++++++--------- 2 files changed, 25 insertions(+), 22 deletions(-) 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 index a0120eb41984..347538853151 100644 --- 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 @@ -92,6 +92,7 @@ public void appendMethod(OutputStream stream, String verb, URI uri) { public void appendHeaders(OutputStream stream, InternetHeaders headers) { try { // Headers + @SuppressWarnings("unchecked") Enumeration<Header> e = headers.getAllHeaders(); while (e.hasMoreElements()) { Header header = e.nextElement(); 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 index 013c109da8ed..d0a4e23f6b6d 100644 --- 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 @@ -25,7 +25,6 @@ import java.util.Arrays; import java.util.Date; import java.util.Enumeration; -import java.util.HashSet; import java.util.Formatter; import java.util.List; import java.util.UUID; @@ -92,10 +91,6 @@ import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource.Builder; import com.sun.jersey.core.header.InBoundHeaders; -import com.sun.jersey.core.spi.component.ProviderFactory; -import com.sun.jersey.core.spi.component.ProviderServices; -import com.sun.jersey.core.spi.factory.MessageBodyFactory; -import com.sun.jersey.spi.inject.ClientSide; import com.sun.jersey.core.util.ReaderWriter; public class TableRestProxy implements TableContract { @@ -808,6 +803,13 @@ private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations List<DataSource> parts = mimeReaderWriter.parseParts(response.getEntityInputStream(), response.getHeaders() .getFirst("Content-Type")); + if (parts.size() == 0 || 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); + } + Entry[] entries = new Entry[operations.getOperations().size()]; for (int i = 0; i < parts.size(); i++) { DataSource ds = parts.get(i); @@ -816,24 +818,22 @@ private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations StatusLine status = httpReaderWriter.parseStatusLine(ds); InternetHeaders headers = httpReaderWriter.parseHeaders(ds); InputStream content = httpReaderWriter.parseEntity(ds); + ByteArrayOutputStream contentByteArrayOutputStream = new ByteArrayOutputStream(); + ReaderWriter.writeTo(content, contentByteArrayOutputStream); + content = new ByteArrayInputStream(contentByteArrayOutputStream.toByteArray()); if (status.getStatus() >= 400) { // Create dummy client response with status, headers and content InBoundHeaders inBoundHeaders = new InBoundHeaders(); + @SuppressWarnings("unchecked") Enumeration<Header> e = headers.getAllHeaders(); while (e.hasMoreElements()) { Header header = e.nextElement(); inBoundHeaders.putSingle(header.getName(), header.getValue()); } - ProviderServices providerServices = new ProviderServices(ClientSide.class, new ProviderFactory(null), - new HashSet<Class<?>>(), new HashSet<Class<?>>()); - MessageBodyFactory bodyContext = new MessageBodyFactory(providerServices, false); - // TODO: This call causes lots of warnings from Jersey. Need to figure out how to silence them. - bodyContext.init(); - ClientResponse dummyResponse = new ClientResponse(status.getStatus(), inBoundHeaders, content, - bodyContext); + ClientResponse dummyResponse = new ClientResponse(status.getStatus(), inBoundHeaders, content, null); // Wrap into a ServiceException UniformInterfaceException exception = new UniformInterfaceException(dummyResponse); @@ -844,26 +844,28 @@ private List<Entry> parseBatchResponse(ClientResponse response, BatchOperations // Parse the message to find which operation caused this error. try { XMLInputFactory xmlStreamFactory = XMLInputFactory.newFactory(); - XMLStreamReader xmlr = xmlStreamFactory.createXMLStreamReader(new ByteArrayInputStream( - serviceException.getRawResponseBody().getBytes())); - while (xmlr.hasNext()) { - xmlr.next(); - if (xmlr.isStartElement() && "message".equals(xmlr.getLocalName())) { - xmlr.next(); + content.reset(); + XMLStreamReader xmlStreamReader = xmlStreamFactory.createXMLStreamReader(content); + + while (xmlStreamReader.hasNext()) { + xmlStreamReader.next(); + if (xmlStreamReader.isStartElement() && "message".equals(xmlStreamReader.getLocalName())) { + xmlStreamReader.next(); // Process "message" elements only - String message = xmlr.getText(); + String message = xmlStreamReader.getText(); int colonIndex = message.indexOf(':'); String errorOpId = message.substring(0, colonIndex); int opId = Integer.parseInt(errorOpId); entries[opId] = error; + break; } } - xmlr.close(); + xmlStreamReader.close(); } catch (XMLStreamException e1) { - // TODO: What to throw here? + throw new UniformInterfaceException( + "Batch response from server does not contain XML in the expected format", response); } - } else if (operation instanceof InsertEntityOperation) { InsertEntity opResult = new InsertEntity().setEntity(atomReaderWriter.parseEntityEntry(content)); From 92b1db53d7b62f2856491dc0859629dfff7e39ad Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Wed, 4 Apr 2012 23:32:22 -0700 Subject: [PATCH 51/59] Adding unit test for batches that have errors. --- .../table/TableServiceIntegrationTest.java | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) 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 index 20ffb6596727..88a4e0c1c130 100644 --- 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 @@ -1023,4 +1023,52 @@ public void batchAllOperationsTogetherWorks() throws Exception { assertEquals(UpdateEntity.class, result.getEntries().get(4).getClass()); assertEquals(UpdateEntity.class, result.getEntries().get(5).getClass()); } + + @Test + public void batchNegativeWorks() throws Exception { + System.out.println("batchNegativeWorks()"); + + // Arrange + Configuration config = createConfiguration(); + TableContract service = TableService.create(config); + String table = TEST_TABLE_8; + String partitionKey = "001"; + + // Insert an entity the modify it outside of the batch + Entity entity1 = new Entity().setPartitionKey(partitionKey).setRowKey("batchNegativeWorks1") + .setProperty("test", EdmType.INT32, 1); + Entity entity2 = new Entity().setPartitionKey(partitionKey).setRowKey("batchNegativeWorks2") + .setProperty("test", EdmType.INT32, 2); + Entity entity3 = new Entity().setPartitionKey(partitionKey).setRowKey("batchNegativeWorks3") + .setProperty("test", EdmType.INT32, 3); + + entity1 = service.insertEntity(table, entity1).getEntity(); + entity2 = service.insertEntity(table, entity2).getEntity(); + entity2.setProperty("test", EdmType.INT32, -2); + service.updateEntity(table, entity2); + + // Act + BatchOperations batchOperations = new BatchOperations(); + + // The entity1 still has the original etag from the first submit, + // so this update should fail, because another update was already made. + entity1.setProperty("test", EdmType.INT32, 3); + batchOperations.addDeleteEntity(table, entity1.getPartitionKey(), entity1.getRowKey(), entity1.getEtag()); + batchOperations.addUpdateEntity(table, entity2); + batchOperations.addInsertEntity(table, entity3); + + BatchResult result = service.batch(batchOperations); + + // Assert + assertNotNull(result); + assertEquals(batchOperations.getOperations().size(), result.getEntries().size()); + assertNull("First result should be null", result.getEntries().get(0)); + assertNotNull("Second result should not be null", result.getEntries().get(1)); + assertEquals("Second result type", com.microsoft.windowsazure.services.table.models.BatchResult.Error.class, + result.getEntries().get(1).getClass()); + com.microsoft.windowsazure.services.table.models.BatchResult.Error error = (com.microsoft.windowsazure.services.table.models.BatchResult.Error) result + .getEntries().get(1); + assertEquals("Second result status code", 412, error.getError().getHttpStatusCode()); + assertNull("Third result should be null", result.getEntries().get(2)); + } } From d14c5c9c5a690e382747cd11b7cef81a325e9c59 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 5 Apr 2012 12:17:09 -0700 Subject: [PATCH 52/59] Fix minor spacing issues --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6390ff1f65f7..76a899e45b20 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ #Windows Azure SDK for Java -This SDK allows you to build Windows Azure applications in Java that allow +This SDK allows you to build Windows Azure applications in Java that allow you to take advantage of Azure scalable cloud computing resources: table and blob storage, messaging through Service Bus. @@ -68,9 +68,9 @@ and uploading a file to it. For additional information on using the client libr public class BlobSample { public static final String storageConnectionString = - "DefaultEndpointsProtocol=http;" + - "AccountName=your_account_name;" + - "AccountKey= your_account_name"; + "DefaultEndpointsProtocol=http;" + + "AccountName=your_account_name;" + + "AccountKey= your_account_name"; public static void main(String[] args) { From 564d8aa92e6835c633dd26d3c459ebbc23f87060 Mon Sep 17 00:00:00 2001 From: unknown <gongchen@HEALTHYGCHENG04.redmond.corp.microsoft.com> Date: Thu, 5 Apr 2012 15:17:55 -0700 Subject: [PATCH 53/59] fix the failed java unit test related to getBlob. --- .../blob/implementation/BlobRestProxy.java | 22 +++++++++++-------- .../core/utils/pipeline/PipelineHelpers.java | 14 +++++++++--- 2 files changed, 24 insertions(+), 12 deletions(-) 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 5bba95ef4599..861cd21f3672 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 @@ -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; @@ -122,6 +122,10 @@ private void ThrowIfError(ClientResponse r) { PipelineHelpers.ThrowIfError(r); } + private void ThrowIfNotSuccess(ClientResponse clientResponse) { + PipelineHelpers.ThrowIfNotSuccess(clientResponse); + } + private WebResource addOptionalQueryParam(WebResource webResource, String key, Object value) { return PipelineHelpers.addOptionalQueryParam(webResource, key, value); } @@ -630,7 +634,7 @@ public GetBlobResult getBlob(String container, String blob, GetBlobOptions optio builder = addOptionalAccessContitionHeader(builder, options.getAccessCondition()); ClientResponse response = builder.get(ClientResponse.class); - ThrowIfError(response); + ThrowIfNotSuccess(response); GetBlobPropertiesResult properties = getBlobPropertiesResultFromResponse(response); GetBlobResult blobResult = new GetBlobResult(); diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java index a36de8dc0247..21b95538f6e1 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/utils/pipeline/PipelineHelpers.java @@ -27,9 +27,17 @@ import com.sun.jersey.api.client.WebResource.Builder; public class PipelineHelpers { - public static void ThrowIfError(ClientResponse r) { - if (r.getStatus() >= 400) { - throw new UniformInterfaceException(r); + public static void ThrowIfNotSuccess(ClientResponse clientResponse) { + int statusCode = clientResponse.getStatus(); + + if ((statusCode < 200) || (statusCode >= 300)) { + throw new UniformInterfaceException(clientResponse); + } + } + + public static void ThrowIfError(ClientResponse clientResponse) { + if (clientResponse.getStatus() >= 400) { + throw new UniformInterfaceException(clientResponse); } } From c884a331b186c3e2848c72f2f4b770ebae292e69 Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 5 Apr 2012 15:30:42 -0700 Subject: [PATCH 54/59] Fix 236: Table: EdmType.GUID should map to java.util.UUID --- .../table/implementation/DefaultEdmValueConterter.java | 4 ++++ 1 file changed, 4 insertions(+) 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 index 698859697949..d35293670c1b 100644 --- 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 @@ -16,6 +16,7 @@ import java.text.ParseException; import java.util.Date; +import java.util.UUID; import javax.inject.Inject; @@ -80,6 +81,9 @@ else if (EdmType.INT64.equals(edmType)) { else if (EdmType.BINARY.equals(edmType)) { return Base64.decode(value); } + else if (EdmType.GUID.equals(edmType)) { + return UUID.fromString(value); + } return value; } From 2c82f1eea61e915f5dd104a8b2b06ee8c247a41b Mon Sep 17 00:00:00 2001 From: Jason Cooke <jcooke@microsoft.com> Date: Thu, 5 Apr 2012 15:52:36 -0700 Subject: [PATCH 55/59] Added unit test for round-tripping UUID values --- .../services/table/TableServiceIntegrationTest.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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 index 88a4e0c1c130..55fc4c14fb3a 100644 --- 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 @@ -302,10 +302,12 @@ public void insertEntityWorks() throws Exception { Configuration config = createConfiguration(); TableContract service = TableService.create(config); byte[] binaryData = new byte[] { 1, 2, 3, 4 }; + UUID uuid = UUID.randomUUID(); 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()).setProperty("test6", EdmType.BINARY, binaryData); + .setProperty("test5", EdmType.DATETIME, new Date()).setProperty("test6", EdmType.BINARY, binaryData) + .setProperty("test7", EdmType.GUID, uuid); // Act InsertEntityResult result = service.insertEntity(TEST_TABLE_2, entity); @@ -341,6 +343,10 @@ public void insertEntityWorks() throws Exception { for (int i = 0; i < binaryData.length; i++) { assertEquals(binaryData[i], returnedBinaryData[i]); } + + assertNotNull(result.getEntity().getProperty("test7")); + assertTrue(result.getEntity().getProperty("test7").getValue() instanceof UUID); + assertEquals(uuid.toString(), result.getEntity().getProperty("test7").getValue().toString()); } @Test From f338598b11d7fadde72deaa2b5319efbda00ed95 Mon Sep 17 00:00:00 2001 From: Albert Cheng <gongchen@microsoft.com> Date: Thu, 5 Apr 2012 17:34:13 -0700 Subject: [PATCH 56/59] Fix issue #217 Table: QueryTablesOptions.Query not fully used. --- .../table/implementation/TableRestProxy.java | 20 ++----------------- .../table/models/QueryTablesOptions.java | 10 ---------- 2 files changed, 2 insertions(+), 28 deletions(-) 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 index 604f51149d59..ce6c5e34a1fc 100644 --- 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 @@ -380,8 +380,7 @@ public QueryTablesResult queryTables() throws ServiceException { @Override public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException { - - Query query = options.getQuery(); + Query query = new Query(); String nextTableName = options.getNextTableName(); String prefix = options.getPrefix(); @@ -389,22 +388,7 @@ public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceE // Append Max char to end '{' is 1 + 'z' in AsciiTable ==> upperBound is prefix + '{' Filter prefixFilter = Filter.and(Filter.ge(Filter.litteral("TableName"), Filter.constant(prefix)), Filter.le(Filter.litteral("TableName"), Filter.constant(prefix + "{"))); - - // a new query is needed if prefix alone is passed in - if (query == null) { - query = new Query(); - } - - // examine the existing filter on the query - if (query.getFilter() == null) { - // use the prefix filter if the query filter is null - query.setFilter(prefixFilter); - } - else { - // combine and use the prefix filter if the query filter exists - Filter combinedFilter = Filter.and(query.getFilter(), prefixFilter); - query.setFilter(combinedFilter); - } + query.setFilter(prefixFilter); } WebResource webResource = getResource(options).path("Tables"); 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 index 36b975d7a690..f115f9ed52d8 100644 --- 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 @@ -16,18 +16,8 @@ public class QueryTablesOptions extends TableServiceOptions { private String nextTableName; - private Query query; private String prefix; - public Query getQuery() { - return query; - } - - public QueryTablesOptions setQuery(Query query) { - this.query = query; - return this; - } - public String getNextTableName() { return nextTableName; } From 8dd4620beea6df082b17dc4fd7cb4c8bff409cf0 Mon Sep 17 00:00:00 2001 From: Albert Cheng <gongchen@microsoft.com> Date: Fri, 6 Apr 2012 18:11:44 -0700 Subject: [PATCH 57/59] Making filter immutable, add filter to TableQuery, Rename RawStringFilter to QueryStringFilter, rename LiteralFilter to PropertyNameFilter. --- .../table/implementation/TableRestProxy.java | 54 +++++++------ .../services/table/models/BinaryFilter.java | 26 +++--- .../services/table/models/ConstantFilter.java | 11 ++- .../services/table/models/Filter.java | 28 +++---- ...ingFilter.java => PropertyNameFilter.java} | 14 ++-- .../services/table/models/Query.java | 81 ------------------- .../table/models/QueryEntitiesOptions.java | 75 ++++++++++++++--- ...eralFilter.java => QueryStringFilter.java} | 13 ++- .../table/models/QueryTablesOptions.java | 10 +++ .../services/table/models/UnaryFilter.java | 18 ++--- .../table/TableServiceIntegrationTest.java | 50 +++++++----- 11 files changed, 183 insertions(+), 197 deletions(-) rename microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/{RawStringFilter.java => PropertyNameFilter.java} (72%) delete mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Query.java rename microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/{LitteralFilter.java => QueryStringFilter.java} (73%) 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 index ce6c5e34a1fc..7d74bbecba2a 100644 --- 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 @@ -72,13 +72,12 @@ 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.LitteralFilter; -import com.microsoft.windowsazure.services.table.models.Query; +import com.microsoft.windowsazure.services.table.models.PropertyNameFilter; import com.microsoft.windowsazure.services.table.models.QueryEntitiesOptions; import com.microsoft.windowsazure.services.table.models.QueryEntitiesResult; +import com.microsoft.windowsazure.services.table.models.QueryStringFilter; 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; @@ -182,26 +181,29 @@ private WebResource addOptionalQueryParam(WebResource webResource, String key, O return PipelineHelpers.addOptionalQueryParam(webResource, key, value); } - private WebResource addOptionalQuery(WebResource webResource, Query query) { - if (query == null) + private WebResource addOptionalQueryEntitiesOptions(WebResource webResource, + QueryEntitiesOptions queryEntitiesOptions) { + if (queryEntitiesOptions == null) return webResource; - if (query.getSelectFields() != null && query.getSelectFields().size() > 0) { + if (queryEntitiesOptions.getSelectFields() != null && queryEntitiesOptions.getSelectFields().size() > 0) { webResource = addOptionalQueryParam(webResource, "$select", - CommaStringBuilder.join(encodeODataURIValues(query.getSelectFields()))); + CommaStringBuilder.join(encodeODataURIValues(queryEntitiesOptions.getSelectFields()))); } - if (query.getTop() != null) { - webResource = addOptionalQueryParam(webResource, "$top", encodeODataURIValue(query.getTop().toString())); + if (queryEntitiesOptions.getTop() != null) { + webResource = addOptionalQueryParam(webResource, "$top", encodeODataURIValue(queryEntitiesOptions.getTop() + .toString())); } - if (query.getFilter() != null) { - webResource = addOptionalQueryParam(webResource, "$filter", buildFilterExpression(query.getFilter())); + if (queryEntitiesOptions.getFilter() != null) { + webResource = addOptionalQueryParam(webResource, "$filter", + buildFilterExpression(queryEntitiesOptions.getFilter())); } - if (query.getOrderByFields() != null) { + if (queryEntitiesOptions.getOrderByFields() != null) { webResource = addOptionalQueryParam(webResource, "$orderby", - CommaStringBuilder.join(encodeODataURIValues(query.getOrderByFields()))); + CommaStringBuilder.join(encodeODataURIValues(queryEntitiesOptions.getOrderByFields()))); } return webResource; @@ -217,8 +219,8 @@ private void buildFilterExpression(Filter filter, StringBuilder sb) { if (filter == null) return; - if (filter instanceof LitteralFilter) { - sb.append(((LitteralFilter) filter).getLitteral()); + if (filter instanceof PropertyNameFilter) { + sb.append(((PropertyNameFilter) filter).getPropertyName()); } else if (filter instanceof ConstantFilter) { Object value = ((ConstantFilter) filter).getValue(); @@ -282,8 +284,8 @@ else if (filter instanceof BinaryFilter) { buildFilterExpression(((BinaryFilter) filter).getRight(), sb); sb.append(")"); } - else if (filter instanceof RawStringFilter) { - sb.append(((RawStringFilter) filter).getRawString()); + else if (filter instanceof QueryStringFilter) { + sb.append(((QueryStringFilter) filter).getQueryString()); } } @@ -380,19 +382,25 @@ public QueryTablesResult queryTables() throws ServiceException { @Override public QueryTablesResult queryTables(QueryTablesOptions options) throws ServiceException { - Query query = new Query(); + Filter queryFilter = options.getFilter(); String nextTableName = options.getNextTableName(); String prefix = options.getPrefix(); if (prefix != null) { // Append Max char to end '{' is 1 + 'z' in AsciiTable ==> upperBound is prefix + '{' - Filter prefixFilter = Filter.and(Filter.ge(Filter.litteral("TableName"), Filter.constant(prefix)), - Filter.le(Filter.litteral("TableName"), Filter.constant(prefix + "{"))); - query.setFilter(prefixFilter); + Filter prefixFilter = Filter.and(Filter.ge(Filter.propertyName("TableName"), Filter.constant(prefix)), + Filter.le(Filter.propertyName("TableName"), Filter.constant(prefix + "{"))); + + if (queryFilter == null) { + queryFilter = prefixFilter; + } + else { + queryFilter = Filter.and(queryFilter, prefixFilter); + } } WebResource webResource = getResource(options).path("Tables"); - webResource = addOptionalQuery(webResource, query); + webResource = addOptionalQueryParam(webResource, "$filter", buildFilterExpression(queryFilter)); webResource = addOptionalQueryParam(webResource, "NextTableName", nextTableName); WebResource.Builder builder = webResource.getRequestBuilder(); @@ -609,7 +617,7 @@ public QueryEntitiesResult queryEntities(String table, QueryEntitiesOptions opti options = new QueryEntitiesOptions(); WebResource webResource = getResource(options).path(table); - webResource = addOptionalQuery(webResource, options.getQuery()); + webResource = addOptionalQueryEntitiesOptions(webResource, options); webResource = addOptionalQueryParam(webResource, "NextPartitionKey", encodeODataURIValue(options.getNextPartitionKey())); webResource = addOptionalQueryParam(webResource, "NextRowKey", encodeODataURIValue(options.getNextRowKey())); 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 index 2cdb8473a26d..7666da869e05 100644 --- 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 @@ -15,34 +15,26 @@ package com.microsoft.windowsazure.services.table.models; public class BinaryFilter extends Filter { - private String operator; - private Filter left; - private Filter right; + private final String operator; + private final Filter left; + private final Filter right; - public String getOperator() { - return operator; + public BinaryFilter(Filter left, String operator, Filter right) { + this.left = left; + this.operator = operator; + this.right = right; } - public BinaryFilter setOperator(String operator) { - this.operator = operator; - return this; + public String getOperator() { + return operator; } 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 index 098b28ae0e39..aa3dd041be7c 100644 --- 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 @@ -15,14 +15,13 @@ package com.microsoft.windowsazure.services.table.models; public class ConstantFilter extends Filter { - private Object value; + private final Object value; - public Object getValue() { - return value; + public ConstantFilter(Object value) { + this.value = value; } - public ConstantFilter setValue(Object value) { - this.value = value; - return this; + public Object getValue() { + return value; } } 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 index 31a6daf581b2..86dfafb309f6 100644 --- 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 @@ -16,50 +16,50 @@ public class Filter { public static UnaryFilter not(Filter operand) { - return new UnaryFilter().setOperator("not").setOperand(operand); + return new UnaryFilter("not", operand); } public static BinaryFilter and(Filter left, Filter right) { - return new BinaryFilter().setOperator("and").setLeft(left).setRight(right); + return new BinaryFilter(left, "and", right); } public static BinaryFilter or(Filter left, Filter right) { - return new BinaryFilter().setOperator("or").setLeft(left).setRight(right); + return new BinaryFilter(left, "or", right); } public static BinaryFilter eq(Filter left, Filter right) { - return new BinaryFilter().setOperator("eq").setLeft(left).setRight(right); + return new BinaryFilter(left, "eq", right); } public static BinaryFilter ne(Filter left, Filter right) { - return new BinaryFilter().setOperator("ne").setLeft(left).setRight(right); + return new BinaryFilter(left, "ne", right); } public static BinaryFilter ge(Filter left, Filter right) { - return new BinaryFilter().setOperator("ge").setLeft(left).setRight(right); + return new BinaryFilter(left, "ge", right); } public static BinaryFilter gt(Filter left, Filter right) { - return new BinaryFilter().setOperator("gt").setLeft(left).setRight(right); + return new BinaryFilter(left, "gt", right); } public static BinaryFilter lt(Filter left, Filter right) { - return new BinaryFilter().setOperator("lt").setLeft(left).setRight(right); + return new BinaryFilter(left, "lt", right); } public static BinaryFilter le(Filter left, Filter right) { - return new BinaryFilter().setOperator("le").setLeft(left).setRight(right); + return new BinaryFilter(left, "le", right); } public static ConstantFilter constant(Object value) { - return new ConstantFilter().setValue(value); + return new ConstantFilter(value); } - public static LitteralFilter litteral(String value) { - return new LitteralFilter().setLitteral(value); + public static PropertyNameFilter propertyName(String value) { + return new PropertyNameFilter(value); } - public static RawStringFilter rawString(String value) { - return new RawStringFilter().setRawString(value); + public static QueryStringFilter QueryString(String value) { + return new QueryStringFilter(value); } } 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/PropertyNameFilter.java similarity index 72% rename from microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/RawStringFilter.java rename to microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/PropertyNameFilter.java index 12aaedf407d4..49cb129d86e0 100644 --- 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/PropertyNameFilter.java @@ -14,15 +14,15 @@ */ package com.microsoft.windowsazure.services.table.models; -public class RawStringFilter extends Filter { - private String rawString; +public class PropertyNameFilter extends Filter { + private final String propertyName; - public String getRawString() { - return rawString; + public PropertyNameFilter(String propertyName) { + this.propertyName = propertyName; } - public RawStringFilter setRawString(String rawString) { - this.rawString = rawString; - return this; + public String getPropertyName() { + return propertyName; } + } 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 deleted file mode 100644 index cf3997dd7225..000000000000 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/Query.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright 2012 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 java.util.ArrayList; -import java.util.List; - -public class Query { - private List<String> selectFields = new ArrayList<String>(); - private String from; - private Filter filter; - private List<String> orderByFields = new ArrayList<String>(); - private Integer top; - - public List<String> getSelectFields() { - return selectFields; - } - - public Query setSelectFields(List<String> 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<String> getOrderByFields() { - return orderByFields; - } - - public Query setOrderByFields(List<String> 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 index 31c7d05652bc..e5d7a7c51151 100644 --- 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 @@ -14,19 +14,19 @@ */ package com.microsoft.windowsazure.services.table.models; +import java.util.ArrayList; +import java.util.List; + public class QueryEntitiesOptions extends TableServiceOptions { - private Query query; - public String nextPartitionKey; - public String nextRowKey; - public Query getQuery() { - return query; - } + private List<String> selectFields = new ArrayList<String>(); + private String from; + private Filter filter; + private List<String> orderByFields = new ArrayList<String>(); + private Integer top; - public QueryEntitiesOptions setQuery(Query query) { - this.query = query; - return this; - } + public String nextPartitionKey; + public String nextRowKey; public String getNextPartitionKey() { return nextPartitionKey; @@ -45,4 +45,59 @@ public QueryEntitiesOptions setNextRowKey(String nextRowKey) { this.nextRowKey = nextRowKey; return this; } + + public List<String> getSelectFields() { + return selectFields; + } + + public QueryEntitiesOptions setSelectFields(List<String> selectFields) { + this.selectFields = selectFields; + return this; + } + + public QueryEntitiesOptions addSelectField(String selectField) { + this.selectFields.add(selectField); + return this; + } + + public String getFrom() { + return from; + } + + public QueryEntitiesOptions setFrom(String from) { + this.from = from; + return this; + } + + public Filter getFilter() { + return filter; + } + + public QueryEntitiesOptions setFilter(Filter filter) { + this.filter = filter; + return this; + } + + public List<String> getOrderByFields() { + return orderByFields; + } + + public QueryEntitiesOptions setOrderByFields(List<String> orderByFields) { + this.orderByFields = orderByFields; + return this; + } + + public QueryEntitiesOptions addOrderByField(String orderByField) { + this.orderByFields.add(orderByField); + return this; + } + + public Integer getTop() { + return top; + } + + public QueryEntitiesOptions setTop(Integer top) { + this.top = top; + 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/QueryStringFilter.java similarity index 73% rename from microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/LitteralFilter.java rename to microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/models/QueryStringFilter.java index 08a62dbf957b..77341e2477c5 100644 --- 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/QueryStringFilter.java @@ -14,15 +14,14 @@ */ package com.microsoft.windowsazure.services.table.models; -public class LitteralFilter extends Filter { - private String litteral; +public class QueryStringFilter extends Filter { + private final String queryString; - public String getLitteral() { - return litteral; + public QueryStringFilter(String queryString) { + this.queryString = queryString; } - public LitteralFilter setLitteral(String litteral) { - this.litteral = litteral; - return this; + public String getQueryString() { + return queryString; } } 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 index f115f9ed52d8..d72c580a402f 100644 --- 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 @@ -15,9 +15,19 @@ package com.microsoft.windowsazure.services.table.models; public class QueryTablesOptions extends TableServiceOptions { + private Filter filter; private String nextTableName; private String prefix; + public Filter getFilter() { + return filter; + } + + public QueryTablesOptions setFilter(Filter filter) { + this.filter = filter; + return this; + } + public String getNextTableName() { return nextTableName; } 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 index f6da13830a0c..99aa90409830 100644 --- 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 @@ -15,24 +15,20 @@ package com.microsoft.windowsazure.services.table.models; public class UnaryFilter extends Filter { - private String operator; - private Filter operand; + private final String operator; + private final Filter operand; - public String getOperator() { - return operator; + public UnaryFilter(String operator, Filter operand) { + this.operator = operator; + this.operand = operand; } - public UnaryFilter setOperator(String operator) { - this.operator = operator; - return this; + public String getOperator() { + return operator; } public Filter getOperand() { return operand; } - public UnaryFilter setOperand(Filter operand) { - this.operand = operand; - return this; - } } 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 index 20ffb6596727..cb298162dc1d 100644 --- 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 @@ -41,7 +41,6 @@ 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.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; @@ -482,8 +481,8 @@ public void deleteEntityTroublesomeKeyWorks() throws Exception { QueryEntitiesResult assertResult2 = service.queryEntities( TEST_TABLE_2, - new QueryEntitiesOptions().setQuery(new Query().setFilter(Filter.eq(Filter.litteral("RowKey"), - Filter.constant("key'with'quotes"))))); + new QueryEntitiesOptions().setFilter(Filter.eq(Filter.propertyName("RowKey"), + Filter.constant("key'with'quotes")))); assertEquals(0, assertResult2.getEntities().size()); @@ -672,9 +671,10 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act - QueryEntitiesResult result = service.queryEntities(table, - new QueryEntitiesOptions().setQuery(new Query().setFilter(Filter.eq(Filter.litteral("RowKey"), - Filter.constant("queryEntitiesWithFilterWorks-3"))))); + QueryEntitiesResult result = service.queryEntities( + table, + new QueryEntitiesOptions().setFilter(Filter.eq(Filter.propertyName("RowKey"), + Filter.constant("queryEntitiesWithFilterWorks-3")))); // Assert assertNotNull(result); @@ -684,8 +684,8 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act - QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query() - .setFilter(Filter.rawString("RowKey eq 'queryEntitiesWithFilterWorks-3'")))); + QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setFilter(Filter + .QueryString("RowKey eq 'queryEntitiesWithFilterWorks-3'"))); // Assert assertNotNull(result); @@ -695,10 +695,11 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act - QueryEntitiesResult result = service.queryEntities( - table, - new QueryEntitiesOptions().setQuery(new Query().setFilter(Filter.eq(Filter.litteral("test"), - Filter.constant(true))))); + QueryEntitiesResult result = service + .queryEntities( + table, + new QueryEntitiesOptions().setFilter(Filter.eq(Filter.propertyName("test"), + Filter.constant(true)))); // Assert assertNotNull(result); @@ -707,8 +708,10 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act - QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query() - .setFilter(Filter.eq(Filter.litteral("test2"), Filter.constant("'value'3"))))); + QueryEntitiesResult result = service.queryEntities( + table, + new QueryEntitiesOptions().setFilter(Filter.eq(Filter.propertyName("test2"), + Filter.constant("'value'3")))); // Assert assertNotNull(result); @@ -718,8 +721,10 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act - QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query() - .setFilter(Filter.eq(Filter.litteral("test4"), Filter.constant(12345678903L))))); + QueryEntitiesResult result = service.queryEntities( + table, + new QueryEntitiesOptions().setFilter(Filter.eq(Filter.propertyName("test4"), + Filter.constant(12345678903L)))); // Assert assertNotNull(result); @@ -729,8 +734,10 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act - QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query() - .setFilter(Filter.eq(Filter.litteral("test5"), Filter.constant(new Date(3000)))))); + QueryEntitiesResult result = service.queryEntities( + table, + new QueryEntitiesOptions().setFilter(Filter.eq(Filter.propertyName("test5"), + Filter.constant(new Date(3000))))); // Assert assertNotNull(result); @@ -740,9 +747,10 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act - QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setQuery(new Query() - .setFilter(Filter.eq(Filter.litteral("test6"), - Filter.constant(entities[3].getPropertyValue("test6")))))); + QueryEntitiesResult result = service.queryEntities( + table, + new QueryEntitiesOptions().setFilter(Filter.eq(Filter.propertyName("test6"), + Filter.constant(entities[3].getPropertyValue("test6"))))); // Assert assertNotNull(result); From 4d824146bab6291247881c042722e3fc3bb09eeb Mon Sep 17 00:00:00 2001 From: Albert Cheng <gongchen@microsoft.com> Date: Mon, 9 Apr 2012 12:15:39 -0700 Subject: [PATCH 58/59] Addressing code review feedback... --- .../microsoft/windowsazure/services/table/models/Filter.java | 2 +- .../services/table/TableServiceIntegrationTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 86dfafb309f6..1e410d2adfb4 100644 --- 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 @@ -59,7 +59,7 @@ public static PropertyNameFilter propertyName(String value) { return new PropertyNameFilter(value); } - public static QueryStringFilter QueryString(String value) { + public static QueryStringFilter queryString(String value) { return new QueryStringFilter(value); } } 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 index cb298162dc1d..9301537ca18f 100644 --- 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 @@ -685,7 +685,7 @@ public void queryEntitiesWithFilterWorks() throws Exception { { // Act QueryEntitiesResult result = service.queryEntities(table, new QueryEntitiesOptions().setFilter(Filter - .QueryString("RowKey eq 'queryEntitiesWithFilterWorks-3'"))); + .queryString("RowKey eq 'queryEntitiesWithFilterWorks-3'"))); // Assert assertNotNull(result); From 40c5baa16e7e025b8ca608f5233c372a8972f6b4 Mon Sep 17 00:00:00 2001 From: "U-REDMOND\\gongchen" <gongchen@HEALTHYGCHENG04.redmond.corp.microsoft.com> Date: Mon, 9 Apr 2012 12:25:02 -0700 Subject: [PATCH 59/59] update the version number to 0.2.1 --- microsoft-azure-api/pom.xml | 456 ++++++++++++++++++------------------ 1 file changed, 228 insertions(+), 228 deletions(-) diff --git a/microsoft-azure-api/pom.xml b/microsoft-azure-api/pom.xml index c83d5dcc1a33..01567851ee74 100644 --- a/microsoft-azure-api/pom.xml +++ b/microsoft-azure-api/pom.xml @@ -1,228 +1,228 @@ -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <modelVersion>4.0.0</modelVersion> - <groupId>com.microsoft.windowsazure</groupId> - <artifactId>microsoft-windowsazure-api</artifactId> - <version>0.2.0</version> - <packaging>jar</packaging> - - <name>Microsoft Windows Azure Client API</name> - <description>API for Microsoft Windows Azure Clients</description> - <url>https://github.com/WindowsAzure/azure-sdk-for-java</url> - - <licenses> - <license> - <name>The Apache Software License, Version 2.0</name> - <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> - <distribution>repo</distribution> - </license> - </licenses> - - <scm> - <url>scm:git:https://github.com/WindowsAzure/azure-sdk-for-java</url> - <connection>scm:git:git://github.com/WindowsAzure/azure-sdk-for-java.git</connection> - </scm> - - <properties> - <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> - <legal><![CDATA[[INFO] Any downloads listed may be third party software. Microsoft grants you no rights for third party software.]]></legal> - </properties> - - <developers> - <developer> - <id>microsoft</id> - <name>Microsoft</name> - </developer> - </developers> - - <dependencies> - <dependency> - <groupId>com.sun.jersey</groupId> - <artifactId>jersey-client</artifactId> - <version>1.10-b02</version> - </dependency> - <dependency> - <groupId>javax.xml.bind</groupId> - <artifactId>jaxb-api</artifactId> - <version>2.1</version> - <scope>provided</scope> - </dependency> - <dependency> - <groupId>junit</groupId> - <artifactId>junit</artifactId> - <version>4.8</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.hamcrest</groupId> - <artifactId>hamcrest-all</artifactId> - <version>1.1</version> - <scope>test</scope> - </dependency> - <dependency> - <groupId>org.mockito</groupId> - <artifactId>mockito-all</artifactId> - <scope>test</scope> - <version>1.9.0-rc1</version> - </dependency> - <dependency> - <groupId>javax.inject</groupId> - <artifactId>javax.inject</artifactId> - <version>1</version> - </dependency> - <dependency> - <groupId>com.sun.jersey</groupId> - <artifactId>jersey-json</artifactId> - <version>1.10-b02</version> - </dependency> - <dependency> - <groupId>commons-logging</groupId> - <artifactId>commons-logging</artifactId> - <version>1.1.1</version> - </dependency> - <dependency> - <groupId>javax.mail</groupId> - <artifactId>mail</artifactId> - <version>1.4</version> - </dependency> - <dependency> - <groupId>org.apache.commons</groupId> - <artifactId>commons-lang3</artifactId> - <version>3.1</version> - </dependency> - </dependencies> - - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-help-plugin</artifactId> - <version>2.1.1</version> - <executions> - <execution> - <phase>validate</phase> - <goals> - <goal>evaluate</goal> - </goals> - <configuration> - <expression>legal</expression> - </configuration> - </execution> - </executions> - </plugin> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-compiler-plugin</artifactId> - <version>2.3.2</version> - <configuration> - <source>1.6</source> - <target>1.6</target> - </configuration> - </plugin> - <plugin> - <groupId>org.jvnet.jaxb2.maven2</groupId> - <artifactId>maven-jaxb2-plugin</artifactId> - <version>0.8.0</version> - <executions> - <execution> - <phase>generate-sources</phase> - <goals> - <goal>generate</goal> - </goals> - </execution> - </executions> - <configuration> - <extension>true</extension> - <plugins> - <plugin> - <groupId>org.jvnet.jaxb2_commons</groupId> - <artifactId>jaxb2-basics</artifactId> - <version>0.6.0</version> - </plugin> - <plugin> - <groupId>org.jvnet.jaxb2_commons</groupId> - <artifactId>jaxb2-basics-annotate</artifactId> - <version>0.6.0</version> - </plugin> - </plugins> - - </configuration> - </plugin> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-javadoc-plugin</artifactId> - <version>2.8</version> - <configuration> - <excludePackageNames>*.implementation.*;*.utils.*;com.microsoft.schemas._2003._10.serialization</excludePackageNames> - <bottom><![CDATA[<code>/** -<br/>* Copyright 2011 Microsoft Corporation -<br/>* -<br/>* Licensed under the Apache License, Version 2.0 (the "License"); -<br/>* you may not use this file except in compliance with the License. -<br/>* You may obtain a copy of the License at -<br/>* http://www.apache.org/licenses/LICENSE-2.0 -<br/>* -<br/>* Unless required by applicable law or agreed to in writing, software -<br/>* distributed under the License is distributed on an "AS IS" BASIS, -<br/>* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -<br/>* See the License for the specific language governing permissions and -<br/>* limitations under the License. -<br/>*/</code>]]></bottom> - </configuration> - </plugin> - - <plugin> - <groupId>org.codehaus.mojo</groupId> - <artifactId>findbugs-maven-plugin</artifactId> - <version>2.3.2</version> - <configuration> - <xmlOutput>true</xmlOutput> - <findbugsXmlOutput>true</findbugsXmlOutput> - <findbugsXmlWithMessages>true</findbugsXmlWithMessages> - </configuration> - </plugin> - - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-checkstyle-plugin</artifactId> - <version>2.8</version> - <configuration> - <configLocation>src/config/checkstyle.xml</configLocation> - </configuration> - </plugin> - - - </plugins> - <pluginManagement> - <plugins> - <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.--> - <plugin> - <groupId>org.eclipse.m2e</groupId> - <artifactId>lifecycle-mapping</artifactId> - <version>1.0.0</version> - <configuration> - <lifecycleMappingMetadata> - <pluginExecutions> - <pluginExecution> - <pluginExecutionFilter> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-help-plugin</artifactId> - <versionRange>[2.1.1,)</versionRange> - <goals> - <goal>evaluate</goal> - </goals> - </pluginExecutionFilter> - <action> - <ignore></ignore> - </action> - </pluginExecution> - </pluginExecutions> - </lifecycleMappingMetadata> - </configuration> - </plugin> - </plugins> - </pluginManagement> - </build> -</project> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <groupId>com.microsoft.windowsazure</groupId> + <artifactId>microsoft-windowsazure-api</artifactId> + <version>0.2.1</version> + <packaging>jar</packaging> + + <name>Microsoft Windows Azure Client API</name> + <description>API for Microsoft Windows Azure Clients</description> + <url>https://github.com/WindowsAzure/azure-sdk-for-java</url> + + <licenses> + <license> + <name>The Apache Software License, Version 2.0</name> + <url>http://www.apache.org/licenses/LICENSE-2.0.txt</url> + <distribution>repo</distribution> + </license> + </licenses> + + <scm> + <url>scm:git:https://github.com/WindowsAzure/azure-sdk-for-java</url> + <connection>scm:git:git://github.com/WindowsAzure/azure-sdk-for-java.git</connection> + </scm> + + <properties> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + <legal><![CDATA[[INFO] Any downloads listed may be third party software. Microsoft grants you no rights for third party software.]]></legal> + </properties> + + <developers> + <developer> + <id>microsoft</id> + <name>Microsoft</name> + </developer> + </developers> + + <dependencies> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-client</artifactId> + <version>1.10-b02</version> + </dependency> + <dependency> + <groupId>javax.xml.bind</groupId> + <artifactId>jaxb-api</artifactId> + <version>2.1</version> + <scope>provided</scope> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <version>4.8</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.hamcrest</groupId> + <artifactId>hamcrest-all</artifactId> + <version>1.1</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-all</artifactId> + <scope>test</scope> + <version>1.9.0-rc1</version> + </dependency> + <dependency> + <groupId>javax.inject</groupId> + <artifactId>javax.inject</artifactId> + <version>1</version> + </dependency> + <dependency> + <groupId>com.sun.jersey</groupId> + <artifactId>jersey-json</artifactId> + <version>1.10-b02</version> + </dependency> + <dependency> + <groupId>commons-logging</groupId> + <artifactId>commons-logging</artifactId> + <version>1.1.1</version> + </dependency> + <dependency> + <groupId>javax.mail</groupId> + <artifactId>mail</artifactId> + <version>1.4</version> + </dependency> + <dependency> + <groupId>org.apache.commons</groupId> + <artifactId>commons-lang3</artifactId> + <version>3.1</version> + </dependency> + </dependencies> + + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-help-plugin</artifactId> + <version>2.1.1</version> + <executions> + <execution> + <phase>validate</phase> + <goals> + <goal>evaluate</goal> + </goals> + <configuration> + <expression>legal</expression> + </configuration> + </execution> + </executions> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <version>2.3.2</version> + <configuration> + <source>1.6</source> + <target>1.6</target> + </configuration> + </plugin> + <plugin> + <groupId>org.jvnet.jaxb2.maven2</groupId> + <artifactId>maven-jaxb2-plugin</artifactId> + <version>0.8.0</version> + <executions> + <execution> + <phase>generate-sources</phase> + <goals> + <goal>generate</goal> + </goals> + </execution> + </executions> + <configuration> + <extension>true</extension> + <plugins> + <plugin> + <groupId>org.jvnet.jaxb2_commons</groupId> + <artifactId>jaxb2-basics</artifactId> + <version>0.6.0</version> + </plugin> + <plugin> + <groupId>org.jvnet.jaxb2_commons</groupId> + <artifactId>jaxb2-basics-annotate</artifactId> + <version>0.6.0</version> + </plugin> + </plugins> + + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-javadoc-plugin</artifactId> + <version>2.8</version> + <configuration> + <excludePackageNames>*.implementation.*;*.utils.*;com.microsoft.schemas._2003._10.serialization</excludePackageNames> + <bottom><![CDATA[<code>/** +<br/>* Copyright 2011 Microsoft Corporation +<br/>* +<br/>* Licensed under the Apache License, Version 2.0 (the "License"); +<br/>* you may not use this file except in compliance with the License. +<br/>* You may obtain a copy of the License at +<br/>* http://www.apache.org/licenses/LICENSE-2.0 +<br/>* +<br/>* Unless required by applicable law or agreed to in writing, software +<br/>* distributed under the License is distributed on an "AS IS" BASIS, +<br/>* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +<br/>* See the License for the specific language governing permissions and +<br/>* limitations under the License. +<br/>*/</code>]]></bottom> + </configuration> + </plugin> + + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>findbugs-maven-plugin</artifactId> + <version>2.3.2</version> + <configuration> + <xmlOutput>true</xmlOutput> + <findbugsXmlOutput>true</findbugsXmlOutput> + <findbugsXmlWithMessages>true</findbugsXmlWithMessages> + </configuration> + </plugin> + + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-checkstyle-plugin</artifactId> + <version>2.8</version> + <configuration> + <configLocation>src/config/checkstyle.xml</configLocation> + </configuration> + </plugin> + + + </plugins> + <pluginManagement> + <plugins> + <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.--> + <plugin> + <groupId>org.eclipse.m2e</groupId> + <artifactId>lifecycle-mapping</artifactId> + <version>1.0.0</version> + <configuration> + <lifecycleMappingMetadata> + <pluginExecutions> + <pluginExecution> + <pluginExecutionFilter> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-help-plugin</artifactId> + <versionRange>[2.1.1,)</versionRange> + <goals> + <goal>evaluate</goal> + </goals> + </pluginExecutionFilter> + <action> + <ignore></ignore> + </action> + </pluginExecution> + </pluginExecutions> + </lifecycleMappingMetadata> + </configuration> + </plugin> + </plugins> + </pluginManagement> + </build> +</project>