From 6a25ef7d48334d8a891d54b30ae9f086e42737ac Mon Sep 17 00:00:00 2001 From: Renaud Paquay Date: Wed, 7 Dec 2011 10:34:55 -0800 Subject: [PATCH 01/63] 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 0000000000000..484cd066f2a09 --- /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 0000000000000..49c90e0ddf3d2 --- /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 0000000000000..9a849047abc04 --- /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 0000000000000..2e732e4fc1d6e --- /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 0000000000000..9cfbe21f43ff2 --- /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 0000000000000..094fe429145a7 --- /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 0000000000000..6aadc0650f3c3 --- /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 0000000000000..5e965269030ab --- /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 0000000000000..30ab7196d4489 --- /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 0000000000000..2e068e4fe9dff --- /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 0000000000000..e42240885c15b --- /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/63] 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 6481932c7cde0..9cbc9ae72bf22 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 0134195d5c3aa..37ff1321d5d96 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 a562b7c2122e7..f7aae2cd9188f 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 9a849047abc04..6e759d01d4334 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 9cfbe21f43ff2..ffa302d156405 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 6aadc0650f3c3..a771f2e57c70c 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 5e965269030ab..9e1fdd24e27b8 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 0000000000000..e60e10bfe4ef5 --- /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 0000000000000..0215444e944bb --- /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 cc7087393981f..07fdb1c4e7b67 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 0000000000000..8b7b18382d59d --- /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 0000000000000..2c67bbce8c0ea --- /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 e7bf06b034de2..7739acd3e7333 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/63] 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 ffa302d156405..19a5cc737bc54 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 9e1fdd24e27b8..52b3eb6bbdac5 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/63] 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 197537a9db826..8d9d63bc8972d 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 3907ac20db407..5bba95ef4599c 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 39e67d931a384..af860e50936dd 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 209a345d26a0a..a5f02e0256372 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 9cbc9ae72bf22..9072ead435645 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 0000000000000..3623028e5ef16 --- /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 0610eb8d7bc94..20d9046c5358c 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 95fce9e9dbeb2..9cebe855ee2a6 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 484cd066f2a09..c36841928b064 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 6e759d01d4334..4d9776bde42cb 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 0000000000000..bd8439e5ee430 --- /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 0000000000000..9c44869f23f91 --- /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 a771f2e57c70c..48e2f4e675da5 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 52b3eb6bbdac5..17326178a9527 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 0000000000000..0e97c0193ab5b --- /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 0000000000000..25547090206ad --- /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 0000000000000..4c38168ca2955 --- /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 0000000000000..655a53838e755 --- /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 0000000000000..2978c465cebd2 --- /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 0000000000000..25f042b564f0f --- /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 0000000000000..8c4d857624e3b --- /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 0000000000000..7c915cf53cb49 --- /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 e60e10bfe4ef5..d4896fa674988 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 0215444e944bb..3816ffe5899af 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 0000000000000..917cecc3409c5 --- /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 0000000000000..3e0efd984bffa --- /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 0000000000000..3cf03c788639f --- /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 2c67bbce8c0ea..434b10be1be36 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/63] 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 a5f02e0256372..08d7d6ea9ce8a 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 0000000000000..e7582255ef590 --- /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 c36841928b064..21ad0784df4b4 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 4d9776bde42cb..8fc4caefeadc2 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 bd8439e5ee430..c1943ebdcd1eb 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 0000000000000..b062b3394ab9d --- /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 9c44869f23f91..8504c88fa349f 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 19a5cc737bc54..79305ce758c84 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 48e2f4e675da5..64215de5c6042 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 17326178a9527..4a2dbc92c29f5 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 0000000000000..5a82435b93375 --- /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 0000000000000..de13eee4740fe --- /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 2978c465cebd2..de199b24f075f 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 0000000000000..666fc9634da3e --- /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 0000000000000..1e90530e04f06 --- /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 3cf03c788639f..9aebd223e75bd 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 434b10be1be36..3ecfde9f2b5a1 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/63] 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 3ecfde9f2b5a1..f07d3a1bb4685 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/63] 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 8fc4caefeadc2..4b1611c61e86e 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 c1943ebdcd1eb..0f85fcede8cb1 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 64215de5c6042..8737038654b6a 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 4a2dbc92c29f5..852545c8b7752 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 de13eee4740fe..ce1d6dff3933c 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 0000000000000..2db782e636934 --- /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 f07d3a1bb4685..254678ed9088a 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/63] 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 4b1611c61e86e..c3a2b2724aefc 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 8737038654b6a..a13a871752853 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 852545c8b7752..e54e867a89a7f 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 0000000000000..441f3020822b4 --- /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 254678ed9088a..98e6346cf89f8 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/63] 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 c3a2b2724aefc..7e5d968d08749 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 a13a871752853..abde4ec8cbe51 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 e54e867a89a7f..30178256e6912 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 98e6346cf89f8..f5ab25bf9e37c 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/63] 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 7e5d968d08749..88526099a933e 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 abde4ec8cbe51..df7384900baf4 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 30178256e6912..db969f5c182a5 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 f5ab25bf9e37c..5962a2b35bc8e 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/63] 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 88526099a933e..412c9afbdea50 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 df7384900baf4..fc1ac6473d7de 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 db969f5c182a5..603a0c04f1d5a 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 5962a2b35bc8e..35bac81f30cc4 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/63] 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 412c9afbdea50..c214333e8a634 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 0f85fcede8cb1..c5803bfb54111 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 fc1ac6473d7de..93a99570fdbaf 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 603a0c04f1d5a..51fc33bbeddd1 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 0000000000000..34195d4795047 --- /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 0000000000000..55652baec98d9 --- /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 0000000000000..3977ccc9b97d4 --- /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 35bac81f30cc4..27420b286d082 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/63] 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 51fc33bbeddd1..95558d7021dd3 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 0000000000000..462be9a865c63 --- /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 25547090206ad..0000000000000 --- 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 4c38168ca2955..9dd16b1a1daa8 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 0000000000000..fcb3560758a2e --- /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 655a53838e755..0000000000000 --- 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 8c4d857624e3b..d67f47162c4d6 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 0000000000000..57e7f8c30b660 --- /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 7c915cf53cb49..0000000000000 --- 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 55652baec98d9..1de1cd47426b5 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 d4896fa674988..d402cd8527cce 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 3816ffe5899af..dfd1c1b7f4aae 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 3e0efd984bffa..42d4b7a870fc4 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/63] 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 08d7d6ea9ce8a..e796bf00794cc 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 0000000000000..6f514dad26d3c --- /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/63] 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 27420b286d082..cf45713dc8fd6 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/63] 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 95558d7021dd3..3f18d447b788d 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 fcb3560758a2e..f0a54f58b09b7 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 0000000000000..ffc8e38aef00c --- /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 cf45713dc8fd6..8e12ebac2b045 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/63] 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 5da3a62a9e6ce..36651b165c7c5 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 21ad0784df4b4..59322db2264df 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 c214333e8a634..63ec2c1b20865 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 0000000000000..933d3f8aac55d --- /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 93a99570fdbaf..c8bf695b7cdda 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 3f18d447b788d..39deca1589bf7 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 0000000000000..43c93c22b0f85 --- /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 0000000000000..462dc4d10ef63 --- /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 8e12ebac2b045..c489c675735ad 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 9aebd223e75bd..aa14a71f8597c 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 0000000000000..c01fdeaebc3cf --- /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/63] 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 9cebe855ee2a6..a36de8dc02476 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 59322db2264df..ee52dd47bfb29 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 0000000000000..458acf6e0cd96 --- /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 0000000000000..22c099a52fee3 --- /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 933d3f8aac55d..9d2282ab08e68 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 39deca1589bf7..944bb8214424a 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 43c93c22b0f85..12eb2a98314e3 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 462dc4d10ef63..30a0b3beebd94 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 c489c675735ad..aadff98044810 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/63] 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 aadff98044810..8fd16a9e2bb6e 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/63] 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 944bb8214424a..a18ffe066a21e 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 12eb2a98314e3..f60bfa8fb005f 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/63] 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 a18ffe066a21e..096c5b4ec312e 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 f60bfa8fb005f..f1df873ac4da0 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 8fd16a9e2bb6e..c8143349741d4 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/63] 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 f7aae2cd9188f..234581b023fb8 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/63] =?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 3e7dec871f279..d9b0c4c4f2cf0 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 970cc3850ead5..c2a741ef3ed2d 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/AccessCondition.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/AccessCondition.java @@ -40,9 +40,8 @@ public static AccessCondition generateEmptyCondition() { * Returns an access condition such that an operation will be performed only if the resource's ETag value matches * the specified ETag value. * <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 8818215a944ee..8beb56163e4cc 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/CloudStorageAccount.java @@ -24,6 +24,7 @@ import com.microsoft.windowsazure.services.blob.client.CloudBlobClient; import com.microsoft.windowsazure.services.core.storage.utils.Utility; import com.microsoft.windowsazure.services.queue.client.CloudQueueClient; +import com.microsoft.windowsazure.services.table.client.CloudTableClient; /** * Represents a Windows Azure storage account. @@ -538,6 +539,26 @@ public CloudQueueClient createCloudQueueClient() { return new CloudQueueClient(this.getQueueEndpoint(), this.getCredentials()); } + /** + * Creates a new table service client. + * + * @return A client object that uses the Table service endpoint. + */ + public CloudTableClient createCloudTableClient() { + if (this.getTableEndpoint() == null) { + throw new IllegalArgumentException("No table endpoint configured."); + } + + if (this.credentials == null) { + throw new IllegalArgumentException("No credentials provided."); + } + + if (!this.credentials.canCredentialsSignRequest()) { + throw new IllegalArgumentException("CloudTableClient requires a credential that can sign request"); + } + return new CloudTableClient(this.getTableEndpoint(), this.getCredentials()); + } + /** * Returns the endpoint for the Blob service, as configured for the storage account. This method is not supported * when using shared access signature credentials. diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java index 492320796e708..7676efe058daa 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/Constants.java @@ -287,7 +287,7 @@ public static class HeaderConstants { /** * Specifies the value to use for UserAgent header. */ - public static final String USER_AGENT_VERSION = "Client v0.1.1"; + public static final String USER_AGENT_VERSION = "Client v0.1.2"; } /** diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java index debb82c4c6a89..39d2c8b737068 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ResultSegment.java @@ -39,9 +39,9 @@ public class ResultSegment<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 e9d0b3f6ad373..baa4c1db04659 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/ServiceClient.java @@ -242,13 +242,13 @@ public void setRetryPolicyFactory(final RetryPolicyFactory retryPolicyFactory) { /** * Sets the timeout to use when making requests to the storage service. * <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 84d4c055976ac..84b4c194ac8fb 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentials.java @@ -39,8 +39,8 @@ public abstract class StorageCredentials { * Either include an account name with an account key (specifying values for * {@link CloudStorageAccount#ACCOUNT_NAME_NAME} and {@link CloudStorageAccount#ACCOUNT_KEY_NAME} ), or a * shared access signature (specifying a value for - * {@link CloudStorageAccount#SHARED_ACCESS_SIGNATURE_NAME} ). If you use an account name and account - * key, do not include a shared access signature, and vice versa. + * {@link CloudStorageAccount#SHARED_ACCESS_SIGNATURE_NAME} ). If you use an account name and account key, + * do not include a shared access signature, and vice versa. * * @return A {@link StorageCredentials} object representing the storage credentials determined from the name/value * pairs. @@ -81,8 +81,8 @@ protected static StorageCredentials tryParseCredentials(final 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 c33ce78d64590..2f3e44a380ed1 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageCredentialsSharedAccessSignature.java @@ -96,8 +96,8 @@ public String computeHmac256(final String value) { /** * Computes a signature for the specified string using the HMAC-SHA256 algorithm with the specified operation - * context. This is not a valid operation for objects of type <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 5e37420554264..624a31467311d 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/StorageErrorCodeStrings.java @@ -188,6 +188,11 @@ public final class StorageErrorCodeStrings { */ public static final String SERVER_BUSY = "ServerBusy"; + /** + * Table Already Exists + */ + public static final String TABLE_ALREADY_EXISTS = "TableAlreadyExists"; + /** * One or more header values are not supported. */ diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java index 02e3554479058..e30d5f1e45440 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/ExecutionEngine.java @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.microsoft.windowsazure.services.core.storage.utils.implementation; import java.io.IOException; @@ -34,6 +35,7 @@ import com.microsoft.windowsazure.services.core.storage.RetryResult; import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.table.client.TableServiceException; /** * RESERVED FOR INTERNAL USE. A class that handles execution of StorageOperations and enforces retry policies. @@ -166,6 +168,17 @@ public static <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 049bf75757fd5..90a2713c57935 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/core/storage/utils/implementation/StorageErrorResponse.java @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.microsoft.windowsazure.services.core.storage.utils.implementation; import java.io.InputStream; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java new file mode 100644 index 0000000000000..4b462cb334137 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/AtomPubParser.java @@ -0,0 +1,562 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.InputStream; +import java.io.OutputStream; +import java.text.ParseException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map.Entry; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.commons.lang3.StringEscapeUtils; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * Reserved for internal use. A class used to read and write Table entities in OData AtomPub format requests and + * responses. + * <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 0000000000000..b471594261c68 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/CloudTableClient.java @@ -0,0 +1,1359 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.InvalidKeyException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; + +import javax.xml.stream.XMLStreamException; + +import com.microsoft.windowsazure.services.core.storage.DoesServiceRequest; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.ResultContinuation; +import com.microsoft.windowsazure.services.core.storage.ResultContinuationType; +import com.microsoft.windowsazure.services.core.storage.ResultSegment; +import com.microsoft.windowsazure.services.core.storage.ServiceClient; +import com.microsoft.windowsazure.services.core.storage.StorageCredentials; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.LazySegmentedIterator; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.SegmentedStorageOperation; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * Provides a service client for accessing the Windows Azure Table service. + * <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 0000000000000..cfe58e086f67e --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/DynamicTableEntity.java @@ -0,0 +1,104 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.HashMap; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * A {@link TableEntity} type which allows callers direct access to the property map of the entity. This class extends + * {@link TableServiceEntity} to eliminate the use of reflection for serialization and deserialization. + * + */ +public class DynamicTableEntity extends TableServiceEntity { + private HashMap<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 0000000000000..bf246941e4ce5 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EdmType.java @@ -0,0 +1,203 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import com.microsoft.windowsazure.services.core.storage.Constants; + +/** + * A enumeration used to represent the primitive types of the Entity Data Model (EDM) in the Open Data Protocol (OData). + * The EDM is the underlying abstract data model used by OData services. The {@link EdmType} enumeration includes a + * {@link #parse(String)} method to convert EDM data type names to the enumeration type, and overrides the + * {@link #toString()} method to produce an EDM data type name. + * <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 0000000000000..b449c874abb87 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityProperty.java @@ -0,0 +1,496 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.text.ParseException; +import java.util.Date; +import java.util.UUID; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.utils.Base64; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * A class which represents a single typed property value in a table entity. An {@link EntityProperty} stores the data + * type as an {@link EdmType}. The value, which may be <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 0000000000000..1f7d95d3a812a --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/EntityResolver.java @@ -0,0 +1,61 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.Date; +import java.util.HashMap; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * An interface to perform client side projection on a retrieved entity. An {@link EntityResolver} instance must + * implement a <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 0000000000000..53653dc951505 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/Ignore.java @@ -0,0 +1,37 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation set on a method to prevent its use in serializing or deserializing a property by reflection. Apply the + * <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 0000000000000..bffc95cdd4276 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHeader.java @@ -0,0 +1,25 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +/** + * Reserved for internal use. A class that represents a given MIME Header. + */ +class MimeHeader { + protected String boundary; + protected String contentType; + protected String contentTransferEncoding; + protected String subBoundary; +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java new file mode 100644 index 0000000000000..143a4022e3740 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimeHelper.java @@ -0,0 +1,510 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import java.net.URISyntaxException; +import java.util.ArrayList; + +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * Reserved for internal use. A class used to read and write MIME requests and responses. + */ +class MimeHelper { + /** + * Reserved for internal use. A static factory method that generates a {@link StorageException} for invalid MIME + * responses. + * + * @return + * The {@link StorageException} for the invalid MIME response. + */ + protected static StorageException generateMimeParseException() { + return new StorageException(StorageErrorCodeStrings.OUT_OF_RANGE_INPUT, "Invalid MIME response received.", + Constants.HeaderConstants.HTTP_UNUSED_306, null, null); + } + + /** + * Reserved for internal use. Returns the HTTP verb for a table operation. + * + * @param operation + * The {@link TableOperation} instance to get the HTTP verb for. + * @return + * A <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 0000000000000..254581502e2b8 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/MimePart.java @@ -0,0 +1,28 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.HashMap; + +/** + * Reserved for internal use. A class that represents a given MIME Part. + */ +class MimePart { + protected int httpStatusCode = -1; + protected String httpStatusMessage; + protected HashMap<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 0000000000000..ed89049797d11 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataConstants.java @@ -0,0 +1,183 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +/** + * Reserved for internal use. A class that holds relevant constants for interacting with OData feeds. + */ +class ODataConstants { + /** + * The <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 0000000000000..9f0f5c573becb --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/ODataPayload.java @@ -0,0 +1,42 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.ArrayList; + +/** + * Reserved for internal use. A class that represents an OData payload and resulting entities. + */ +class ODataPayload<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 0000000000000..24da5f2adc585 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/PropertyPair.java @@ -0,0 +1,277 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.Map.Entry; +import java.util.UUID; + +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * Reserved for internal use. A class used internally during the reflection process to determine which properties should + * be serialized. + */ +class PropertyPair { + /** + * Reserved for internal use. A static factory method to generate a map of property names to {@link PropertyPair} + * instances for the specified class type. Uses reflection to find pairs of getter and setter methods that are + * annotated with {@link StoreAs} with a common property name, or of the form <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 0000000000000..52dbf5ecdccb3 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/QueryTableOperation.java @@ -0,0 +1,268 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.text.ParseException; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * A class that extends {@link TableOperation} to implement a query to retrieve a single table entity. To execute a + * {@link QueryTableOperation} instance, call the <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 0000000000000..7918530c85f34 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/StoreAs.java @@ -0,0 +1,49 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * An annotation used to override the name a property is serialized and deserialized with using reflection. Use this + * annotation to specify the property name to associate with the data stored by a setter method or retrieved by a getter + * method in a class implementing {@link TableEntity} that uses reflection-based serialization and deserialization. Note + * that the names "PartitionKey", "RowKey", "Timestamp", and "Etag" are reserved and will be ignored if set with the + * <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 0000000000000..596e519612c30 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableBatchOperation.java @@ -0,0 +1,514 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.InputStream; +import java.io.StringReader; +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.UUID; + +import javax.xml.stream.XMLStreamReader; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * A class which represents a batch operation. A batch operation is a collection of table operations which are executed + * by the Storage Service REST API as a single atomic operation, by invoking an <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 0000000000000..48d3f3d56ea09 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableConstants.java @@ -0,0 +1,143 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +/** + * Holds the constants used for the Table Service. + */ +public final class TableConstants { + /** + * The constants used in HTML header fields for Table service requests. + */ + public static class HeaderConstants { + /** + * The ETag header field label. + */ + public static final String ETAG = "ETag"; + + /** + * The Accept header value to send. + */ + public static final String ACCEPT_TYPE = "application/atom+xml,application/xml"; + + /** + * The Content-Type header value to send for single operations. + */ + public static final String ATOMPUB_TYPE = "application/atom+xml"; + + /** + * The Content-Type header value to send for batch operations. + */ + public static final String MULTIPART_MIXED_FORMAT = "multipart/mixed; boundary=%s"; + + /** + * The DataServiceVersion header field label. + */ + public static final String DATA_SERVICE_VERSION = "DataServiceVersion"; + + /** + * The DataServiceVersion header value to send. + */ + public static final String DATA_SERVICE_VERSION_VALUE = "1.0;NetFx"; + + /** + * The MaxDataServiceVersion header field label. + */ + public static final String MAX_DATA_SERVICE_VERSION = "MaxDataServiceVersion"; + + /** + * The MaxDataServiceVersion header value to send. + */ + public static final String MAX_DATA_SERVICE_VERSION_VALUE = "2.0;NetFx"; + } + + /** + * Default client side timeout, in milliseconds, for table clients. + */ + public static final int TABLE_DEFAULT_TIMEOUT_IN_MS = 60 * 1000; + + /** + * Stores the header prefix for continuation information. + */ + public static final String TABLE_SERVICE_PREFIX_FOR_TABLE_CONTINUATION = "x-ms-continuation-"; + + /** + * Stores the header suffix for the next partition key. + */ + public static final String TABLE_SERVICE_NEXT_PARTITION_KEY = "NextPartitionKey"; + + /** + * Stores the header suffix for the next row key. + */ + public static final String TABLE_SERVICE_NEXT_ROW_KEY = "NextRowKey"; + + /** + * Stores the header suffix for the next marker. + */ + public static final String TABLE_SERVICE_NEXT_MARKER = "NextMarker"; + + /** + * Stores the table suffix for the next table name. + */ + public static final String TABLE_SERVICE_NEXT_TABLE_NAME = "NextTableName"; + + /** + * The name of the partition key property. + */ + public static final String PARTITION_KEY = "PartitionKey"; + + /** + * The name of the row key property. + */ + public static final String ROW_KEY = "RowKey"; + + /** + * The name of the Timestamp property. + */ + public static final String TIMESTAMP = "Timestamp"; + + /** + * The name of the special table used to store tables. + */ + public static final String TABLES_SERVICE_TABLES_NAME = "Tables"; + + /** + * The name of the property that stores the table name. + */ + public static final String TABLE_NAME = "TableName"; + + /** + * The query filter clause name. + */ + public static final String FILTER = "$filter"; + + /** + * The query top clause name. + */ + public static final String TOP = "$top"; + + /** + * The query select clause name. + */ + public static final String SELECT = "$select"; + + /** + * Private Default Constructor. + */ + private TableConstants() { + // No op + } +} diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableEntity.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableEntity.java new file mode 100644 index 0000000000000..85dc20a8c37bf --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableEntity.java @@ -0,0 +1,170 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.Date; +import java.util.HashMap; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * An interface required for table entity types. The {@link TableEntity} interface declares getter and setter methods + * for the common entity properties, and <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 0000000000000..c9c72e0856ddc --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperation.java @@ -0,0 +1,699 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.text.ParseException; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.ExecutionEngine; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageOperation; + +/** + * A class which represents a single table operation. + * <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 0000000000000..a1ceccabd3320 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableOperationType.java @@ -0,0 +1,43 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +/** + * Reserved for internal use. An enumeration type which represents the type of operation a {@link TableOperation} + * represents. + */ +enum TableOperationType { + INSERT, DELETE, REPLACE, RETRIEVE, MERGE, INSERT_OR_REPLACE, INSERT_OR_MERGE; + + /** + * Gets the {@link TableUpdateType} associated the operation type, if applicable. Applies to + * {@link #INSERT_OR_MERGE} and {@link #INSERT_OR_REPLACE} values. + * + * @return + * The applicable {@link TableUpdateType}, or <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 0000000000000..a54279ae0ce17 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableQuery.java @@ -0,0 +1,773 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.Date; +import java.util.Formatter; +import java.util.UUID; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.UriQueryBuilder; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; + +/** + * A class which represents a query against a specified table. A {@link TableQuery} instance aggregates the query + * parameters to use when the query is executed. One of the <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 0000000000000..4642bec1b298a --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequest.java @@ -0,0 +1,432 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.ResultContinuation; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.utils.PathUtility; +import com.microsoft.windowsazure.services.core.storage.utils.UriQueryBuilder; +import com.microsoft.windowsazure.services.core.storage.utils.Utility; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.BaseRequest; + +/** + * Reserved for internal use. A class used to generate requests for Table objects. + */ +final class TableRequest { + /** + * Reserved for internal use. Adds continuation token values to the specified query builder, if set. + * + * @param builder + * The {@link UriQueryBuilder} object to apply the continuation token properties to. + * @param continuationToken + * The {@link ResultContinuation} object containing the continuation token values to apply to the query + * builder. Specify <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 0000000000000..1a4d210526f20 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableRequestOptions.java @@ -0,0 +1,35 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import com.microsoft.windowsazure.services.core.storage.RequestOptions; + +/** + * Represents a set of timeout and retry policy options that may be specified for a table operation request. + */ +public class TableRequestOptions extends RequestOptions { + /** + * Reserved for internal use. Initializes the timeout and retry policy for this <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 0000000000000..848e7b2afc0bc --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResponse.java @@ -0,0 +1,75 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.net.HttpURLConnection; + +import com.microsoft.windowsazure.services.core.storage.ResultContinuation; +import com.microsoft.windowsazure.services.core.storage.ResultContinuationType; + +/** + * Reserved for internal use. A class used to help parse responses from the Table service. + */ +class TableResponse { + /** + * Reserved for internal use. A static factory method that constructs a {@link ResultContinuation} instance from the + * continuation token information in a table operation response, if any. + * + * @param queryRequest + * The <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 0000000000000..95e0621b63ed9 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableResult.java @@ -0,0 +1,179 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.util.HashMap; + +/** + * A class which represents the result of a table operation. The {@link TableResult} class encapsulates the HTTP + * response + * and any table entity results returned by the Storage Service REST API operation called for a particular + * {@link TableOperation}. + * + */ +public class TableResult { + private Object result; + + private int httpStatusCode = -1; + + private String id; + + private String etag; + + private HashMap<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 0000000000000..d958bb328444b --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceEntity.java @@ -0,0 +1,414 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.lang.reflect.InvocationTargetException; +import java.util.Date; +import java.util.HashMap; +import java.util.Map.Entry; + +import com.microsoft.windowsazure.services.core.storage.Constants; +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.StorageErrorCodeStrings; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * The {@link TableServiceEntity} class represents the base object type for a table entity in the Storage service. + * {@link TableServiceEntity} provides a base implementation for the {@link TableEntity} interface that provides + * <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 0000000000000..04f065a4407df --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableServiceException.java @@ -0,0 +1,172 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; + +import javax.xml.stream.XMLStreamException; + +import com.microsoft.windowsazure.services.core.storage.RequestResult; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.core.storage.StorageExtendedErrorInformation; +import com.microsoft.windowsazure.services.core.storage.utils.implementation.StorageErrorResponse; + +/** + * An exception that results when a table storage service operation fails to complete successfully. + */ +public class TableServiceException extends StorageException { + + private static final long serialVersionUID = 6037366449663934891L; + + /** + * Reserved for internal use. A static factory method to create a {@link TableServiceException} instance using + * the specified parameters. + * + * @param retryable + * A flag indicating the table operation can be retried. + * @param res + * A {@link RequestResult} containing the result of the table storage service operation. + * @param op + * The {@link TableOperation} representing the table operation that caused the exception. + * @param inStream + * The <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 0000000000000..3fbf519729c44 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/table/client/TableUpdateType.java @@ -0,0 +1,31 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.microsoft.windowsazure.services.table.client; + +/** + * Reserved for internal use. An enum that represents the type of update a given upsert operation will perform. + */ +enum TableUpdateType { + /** + * The table operation updates an existing entity. + */ + MERGE, + + /** + * The table operation replaces an existing entity. + */ + REPLACE; +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableBatchOperationTests.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableBatchOperationTests.java new file mode 100644 index 0000000000000..671ac7c308a75 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableBatchOperationTests.java @@ -0,0 +1,762 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +import static org.junit.Assert.*; + +import java.net.HttpURLConnection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.Random; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +public class TableBatchOperationTests extends TableTestBase { + @Test + public void batchDelete() throws StorageException { + TableBatchOperation batch = new TableBatchOperation(); + + // insert entity + class1 ref = generateRandomEnitity("jxscl_odata"); + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + batch.delete(ref); + + ArrayList<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 0000000000000..7dc7bd3c4dbd6 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableClientTests.java @@ -0,0 +1,268 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.text.DecimalFormat; +import java.util.ArrayList; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.ResultSegment; +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Client Tests + */ +public class TableClientTests extends TableTestBase { + @Test + public void listTablesSegmented() throws IOException, URISyntaxException, StorageException { + String tableBaseName = generateRandomTableName(); + ArrayList<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 0000000000000..ce90b73f1af4b --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableEscapingTests.java @@ -0,0 +1,231 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Escaping Tests + */ +public class TableEscapingTests extends TableTestBase { + @Test + public void emptyString() throws StorageException { + doEscapeTest("", false); + } + + @Test + public void emptyStringBatch() throws StorageException { + doEscapeTest("", true); + } + + @Test + public void randomChars() throws StorageException { + doEscapeTest("!$'\"()*+,;=", false); + } + + @Test + public void randomCharsBatch() throws StorageException { + doEscapeTest("!$'\"()*+,;=", true); + } + + @Test + public void regularPKInQuery() throws StorageException { + doQueryEscapeTest("data"); + } + + @Test + public void specialChars() throws StorageException { + doEscapeTest("\\ // @ ? <?", 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 0000000000000..4beb6ebc4ed21 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableOperationTests.java @@ -0,0 +1,591 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +import static org.junit.Assert.*; + +import java.net.HttpURLConnection; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Operation Tests + */ +public class TableOperationTests extends TableTestBase { + @Test + public void delete() throws StorageException { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + TableOperation op = TableOperation.insert(ref); + + tClient.execute(testSuiteTableName, op); + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + + TableResult res2 = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertTrue(res2.getResult() == null); + } + + @Test + public void deleteFail() throws StorageException { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + String oldEtag = ref.getEtag(); + + // update entity + ref.setA("updated"); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + ref.setEtag(oldEtag); + + try { + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Precondition Failed"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The update condition specified in the request was not satisfied.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "UpdateConditionNotSatisfied"); + } + + TableResult res2 = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + ref = res2.getResultAsType(); + // actually delete it + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + + // now try to delete it and fail + try { + tClient.execute(testSuiteTableName, TableOperation.delete(ref)); + fail(); + } + catch (TableServiceException ex) { + Assert.assertEquals(ex.getMessage(), "Not Found"); + Assert.assertTrue(ex.getExtendedErrorInformation().getErrorMessage() + .startsWith("The specified resource does not exist.")); + Assert.assertEquals(ex.getExtendedErrorInformation().getErrorCode(), "ResourceNotFound"); + } + } + + @Test + public void emptyRetrieve() throws StorageException { + class1 ref = new class1(); + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertNull(res.getResult()); + Assert.assertEquals(res.getHttpStatusCode(), HttpURLConnection.HTTP_NOT_FOUND); + } + + @Test + public void insertOrMerge() throws StorageException { + class1 baseEntity = new class1(); + baseEntity.setA("foo_A"); + baseEntity.setB("foo_B"); + baseEntity.setC("foo_C"); + baseEntity.setD(new byte[] { 0, 1, 2 }); + baseEntity.setPartitionKey("jxscl_odata"); + baseEntity.setRowKey(UUID.randomUUID().toString()); + + class2 secondEntity = new class2(); + secondEntity.setL("foo_L"); + secondEntity.setM("foo_M"); + secondEntity.setN("foo_N"); + secondEntity.setO("foo_O"); + secondEntity.setPartitionKey(baseEntity.getPartitionKey()); + secondEntity.setRowKey(baseEntity.getRowKey()); + secondEntity.setEtag(baseEntity.getEtag()); + + // Insert or merge Entity - ENTITY DOES NOT EXIST NOW. + TableResult insertResult = tClient.execute(testSuiteTableName, TableOperation.insertOrMerge(baseEntity)); + + Assert.assertEquals(insertResult.getHttpStatusCode(), HttpURLConnection.HTTP_NO_CONTENT); + + // Insert or replace Entity - ENTITY EXISTS -> WILL REPLACE + tClient.execute(testSuiteTableName, TableOperation.insertOrMerge(secondEntity)); + + // Retrieve entity + TableResult queryResult = tClient + .execute(testSuiteTableName, TableOperation.retrieve(baseEntity.getPartitionKey(), + baseEntity.getRowKey(), DynamicTableEntity.class)); + + DynamicTableEntity retrievedEntity = queryResult.<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 0000000000000..dd46b99bae991 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableQueryTests.java @@ -0,0 +1,427 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URISyntaxException; +import java.security.InvalidKeyException; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.BeforeClass; +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.OperationContext; +import com.microsoft.windowsazure.services.core.storage.ResponseReceivedEvent; +import com.microsoft.windowsazure.services.core.storage.ResultSegment; +import com.microsoft.windowsazure.services.core.storage.StorageEvent; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.table.client.TableQuery.QueryComparisons; + +/** + * Table Query Tests + */ +public class TableQueryTests extends TableTestBase { + @BeforeClass + public static void setup() throws URISyntaxException, StorageException, InvalidKeyException { + TableTestBase.setup(); + + // Insert 500 entities in Batches to query + for (int i = 0; i < 5; i++) { + TableBatchOperation batch = new TableBatchOperation(); + + for (int j = 0; j < 100; j++) { + class1 ent = generateRandomEnitity("javatables_batch_" + Integer.toString(i)); + ent.setRowKey(String.format("%06d", j)); + batch.insert(ent); + } + + tClient.execute(testSuiteTableName, batch); + } + } + + @Test + public void tableQueryWithDynamicEntity() { + // Create entity to check against + class1 randEnt = TableTestBase.generateRandomEnitity(null); + + final Iterable<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 0000000000000..e7380c67c6fe6 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableSerializerTests.java @@ -0,0 +1,269 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.Test; + +import com.microsoft.windowsazure.services.core.storage.StorageException; + +/** + * Table Serializer Tests + */ +public class TableSerializerTests extends TableTestBase { + @Test + public void testComplexEntityInsert() throws IOException, URISyntaxException, StorageException { + ComplexEntity ref = new ComplexEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.populateEntity(); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ComplexEntity retrievedComplexRef = res.getResultAsType(); + ref.assertEquality(retrievedComplexRef); + } + + @Test + public void testIgnoreAnnotation() throws IOException, URISyntaxException, StorageException { + // Ignore On Getter + IgnoreOnGetter ignoreGetter = new IgnoreOnGetter(); + ignoreGetter.setPartitionKey("jxscl_odata"); + ignoreGetter.setRowKey(UUID.randomUUID().toString()); + ignoreGetter.setIgnoreString("ignore data"); + + tClient.execute(testSuiteTableName, TableOperation.insert(ignoreGetter)); + + TableResult res = tClient + .execute(testSuiteTableName, TableOperation.retrieve(ignoreGetter.getPartitionKey(), + ignoreGetter.getRowKey(), IgnoreOnGetter.class)); + + IgnoreOnGetter retrievedIgnoreG = res.getResultAsType(); + Assert.assertEquals(retrievedIgnoreG.getIgnoreString(), null); + + // Ignore On Setter + IgnoreOnSetter ignoreSetter = new IgnoreOnSetter(); + ignoreSetter.setPartitionKey("jxscl_odata"); + ignoreSetter.setRowKey(UUID.randomUUID().toString()); + ignoreSetter.setIgnoreString("ignore data"); + + tClient.execute(testSuiteTableName, TableOperation.insert(ignoreSetter)); + + res = tClient + .execute(testSuiteTableName, TableOperation.retrieve(ignoreSetter.getPartitionKey(), + ignoreSetter.getRowKey(), IgnoreOnSetter.class)); + + IgnoreOnSetter retrievedIgnoreS = res.getResultAsType(); + Assert.assertEquals(retrievedIgnoreS.getIgnoreString(), null); + + // Ignore On Getter AndSetter + IgnoreOnGetterAndSetter ignoreGetterSetter = new IgnoreOnGetterAndSetter(); + ignoreGetterSetter.setPartitionKey("jxscl_odata"); + ignoreGetterSetter.setRowKey(UUID.randomUUID().toString()); + ignoreGetterSetter.setIgnoreString("ignore data"); + + tClient.execute(testSuiteTableName, TableOperation.insert(ignoreGetterSetter)); + + res = tClient.execute(testSuiteTableName, TableOperation.retrieve(ignoreGetterSetter.getPartitionKey(), + ignoreGetterSetter.getRowKey(), IgnoreOnGetterAndSetter.class)); + + IgnoreOnGetterAndSetter retrievedIgnoreGS = res.getResultAsType(); + Assert.assertEquals(retrievedIgnoreGS.getIgnoreString(), null); + } + + @Test + public void testNulls() throws IOException, URISyntaxException, StorageException { + ComplexEntity ref = new ComplexEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.populateEntity(); + + // Binary object + ref.setBinary(null); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + ref = res.getResultAsType(); + + Assert.assertNull("Binary should be null", ref.getBinary()); + + // Bool + ref.setBool(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Bool should be null", ref.getBool()); + + // Date + ref.setDateTime(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Date should be null", ref.getDateTime()); + + // Double + ref.setDouble(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Double should be null", ref.getDouble()); + + // UUID + ref.setGuid(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("UUID should be null", ref.getGuid()); + + // Int32 + ref.setInt32(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Int32 should be null", ref.getInt32()); + + // Int64 + ref.setInt64(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("Int64 should be null", ref.getInt64()); + + // String + ref.setString(null); + tClient.execute(testSuiteTableName, TableOperation.replace(ref)); + + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ref = res.getResultAsType(); + + Assert.assertNull("String should be null", ref.getString()); + } + + @Test + public void testStoreAsAnnotation() throws IOException, URISyntaxException, StorageException { + StoreAsEntity ref = new StoreAsEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.setStoreAsString("StoreAsOverride Data"); + ref.populateEntity(); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), StoreAsEntity.class)); + + StoreAsEntity retrievedStoreAsRef = res.getResultAsType(); + Assert.assertEquals(retrievedStoreAsRef.getStoreAsString(), ref.getStoreAsString()); + + // Same query with a class without the storeAs annotation + res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), ComplexEntity.class)); + + ComplexEntity retrievedComplexRef = res.getResultAsType(); + Assert.assertEquals(retrievedComplexRef.getString(), ref.getStoreAsString()); + + tClient.execute(testSuiteTableName, TableOperation.delete(retrievedComplexRef)); + } + + @Test + public void testInvalidStoreAsAnnotation() throws IOException, URISyntaxException, StorageException { + InvalidStoreAsEntity ref = new InvalidStoreAsEntity(); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + ref.setStoreAsString("StoreAsOverride Data"); + ref.populateEntity(); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), InvalidStoreAsEntity.class)); + + InvalidStoreAsEntity retrievedStoreAsRef = res.getResultAsType(); + Assert.assertEquals(retrievedStoreAsRef.getStoreAsString(), null); + } + + @Test + public void whitespaceTest() throws StorageException { + class1 ref = new class1(); + + ref.setA("B "); + ref.setB(" A "); + ref.setC(" "); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertEquals(((class1) res.getResult()).getA(), ref.getA()); + } + + @Test + public void newLineTest() throws StorageException { + class1 ref = new class1(); + + ref.setA("B "); + ref.setB(" A "); + ref.setC("\r\n"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey("jxscl_odata"); + ref.setRowKey(UUID.randomUUID().toString()); + + tClient.execute(testSuiteTableName, TableOperation.insert(ref)); + + TableResult res = tClient.execute(testSuiteTableName, + TableOperation.retrieve(ref.getPartitionKey(), ref.getRowKey(), class1.class)); + + Assert.assertEquals(((class1) res.getResult()).getA(), ref.getA()); + } +} diff --git a/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableTestBase.java b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableTestBase.java new file mode 100644 index 0000000000000..b30f200fa61a9 --- /dev/null +++ b/microsoft-azure-api/src/test/java/com/microsoft/windowsazure/services/table/client/TableTestBase.java @@ -0,0 +1,599 @@ +/** + * Copyright 2011 Microsoft Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.microsoft.windowsazure.services.table.client; + +import java.net.URISyntaxException; +import java.security.InvalidKeyException; +import java.util.Arrays; +import java.util.Date; +import java.util.UUID; + +import junit.framework.Assert; + +import org.junit.AfterClass; +import org.junit.BeforeClass; + +import com.microsoft.windowsazure.services.blob.client.CloudBlobClient; +import com.microsoft.windowsazure.services.core.storage.CloudStorageAccount; +import com.microsoft.windowsazure.services.core.storage.StorageException; +import com.microsoft.windowsazure.services.queue.client.CloudQueueClient; + +/** + * Table Test Base + */ +public class TableTestBase { + public static boolean USE_DEV_FABRIC = false; + public static final String CLOUD_ACCOUNT_HTTP = "DefaultEndpointsProtocol=http;AccountName=[ACCOUNT NAME];AccountKey=[ACCOUNT KEY]"; + public static final String CLOUD_ACCOUNT_HTTPS = "DefaultEndpointsProtocol=https;AccountName=[ACCOUNT NAME];AccountKey=[ACCOUNT KEY]"; + + public static class class1 extends TableServiceEntity { + public String A; + + public String B; + + public String C; + + public byte[] D; + + public class1() { + // empty ctor + } + + public synchronized String getA() { + return this.A; + } + + public synchronized String getB() { + return this.B; + } + + public synchronized String getC() { + return this.C; + } + + public synchronized byte[] getD() { + return this.D; + } + + public synchronized void setA(final String a) { + this.A = a; + } + + public synchronized void setB(final String b) { + this.B = b; + } + + public synchronized void setC(final String c) { + this.C = c; + } + + public synchronized void setD(final byte[] d) { + this.D = d; + } + } + + public class class2 extends TableServiceEntity { + private String L; + private String M; + + private String N; + + private String O; + + /** + * @return the l + */ + public String getL() { + return this.L; + } + + /** + * @return the m + */ + public String getM() { + return this.M; + } + + /** + * @return the n + */ + public String getN() { + return this.N; + } + + /** + * @return the o + */ + public String getO() { + return this.O; + } + + /** + * @param l + * the l to set + */ + public void setL(String l) { + this.L = l; + } + + /** + * @param m + * the m to set + */ + public void setM(String m) { + this.M = m; + } + + /** + * @param n + * the n to set + */ + public void setN(String n) { + this.N = n; + } + + /** + * @param o + * the o to set + */ + public void setO(String o) { + this.O = o; + } + } + + public static class ComplexEntity extends TableServiceEntity { + private Date dateTime = null; + private Boolean Bool = null; + private boolean BoolPrimitive = false; + private Byte[] Binary = null; + private byte[] binaryPrimitive = null; + private double DoublePrimitive = -1; + private Double Double = null; + private UUID Guid = null; + private int IntegerPrimitive = -1; + private Integer Int32 = null; + private long LongPrimitive = -1L; + private Long Int64 = null; + private String String = null; + + public ComplexEntity() { + // Empty Ctor + } + + public void assertEquality(ComplexEntity other) { + Assert.assertEquals(this.getPartitionKey(), other.getPartitionKey()); + Assert.assertEquals(this.getRowKey(), other.getRowKey()); + + Assert.assertEquals(this.getDateTime(), other.getDateTime()); + Assert.assertEquals(this.getGuid(), other.getGuid()); + Assert.assertEquals(this.getString(), other.getString()); + + Assert.assertEquals(this.getDouble(), other.getDouble()); + Assert.assertEquals(this.getDoublePrimitive(), other.getDoublePrimitive()); + Assert.assertEquals(this.getInt32(), other.getInt32()); + Assert.assertEquals(this.getIntegerPrimitive(), other.getIntegerPrimitive()); + Assert.assertEquals(this.getBool(), other.getBool()); + Assert.assertEquals(this.getBoolPrimitive(), other.getBoolPrimitive()); + Assert.assertEquals(this.getInt64(), other.getInt64()); + Assert.assertEquals(this.getIntegerPrimitive(), other.getIntegerPrimitive()); + Assert.assertTrue(Arrays.equals(this.getBinary(), other.getBinary())); + Assert.assertTrue(Arrays.equals(this.getBinaryPrimitive(), other.getBinaryPrimitive())); + } + + /** + * @return the binary + */ + public Byte[] getBinary() { + return this.Binary; + } + + /** + * @return the binaryPrimitive + */ + public byte[] getBinaryPrimitive() { + return this.binaryPrimitive; + } + + /** + * @return the bool + */ + public Boolean getBool() { + return this.Bool; + } + + /** + * @return the bool + */ + public boolean getBoolPrimitive() { + return this.BoolPrimitive; + } + + /** + * @return the dateTime + */ + public Date getDateTime() { + return this.dateTime; + } + + /** + * @return the double + */ + public Double getDouble() { + return this.Double; + } + + /** + * @return the doublePrimitive + */ + public double getDoublePrimitive() { + return this.DoublePrimitive; + } + + /** + * @return the guid + */ + public UUID getGuid() { + return this.Guid; + } + + /** + * @return the int32 + */ + public Integer getInt32() { + return this.Int32; + } + + /** + * @return the int64 + */ + public Long getInt64() { + return this.Int64; + } + + /** + * @return the integerPrimitive + */ + public int getIntegerPrimitive() { + return this.IntegerPrimitive; + } + + /** + * @return the longPrimitive + */ + public long getLongPrimitive() { + return this.LongPrimitive; + } + + /** + * @return the string + */ + public String getString() { + return this.String; + } + + public void populateEntity() { + this.setBinary(new Byte[] { 1, 2, 3, 4 }); + this.setBinaryPrimitive(new byte[] { 1, 2, 3, 4 }); + this.setBool(true); + this.setBoolPrimitive(true); + this.setDateTime(new Date()); + this.setDouble(2342.2342); + this.setDoublePrimitive(2349879.2342); + this.setInt32(2342); + this.setInt64((long) 87987987); + this.setIntegerPrimitive(2342); + this.setLongPrimitive(87987987); + this.setGuid(UUID.randomUUID()); + this.setString("foo"); + } + + /** + * @param binary + * the binary to set + */ + public void setBinary(final Byte[] binary) { + this.Binary = binary; + } + + /** + * @param binaryPrimitive + * the binaryPrimitive to set + */ + public void setBinaryPrimitive(byte[] binaryPrimitive) { + this.binaryPrimitive = binaryPrimitive; + } + + /** + * @param bool + * the bool to set + */ + public void setBool(final Boolean bool) { + this.Bool = bool; + } + + /** + * @param boolPrimitive + * the boolPrimitive to set + */ + public void setBoolPrimitive(boolean boolPrimitive) { + this.BoolPrimitive = boolPrimitive; + } + + /** + * @param dateTime + * the dateTime to set + */ + public void setDateTime(final Date dateTime) { + this.dateTime = dateTime; + } + + /** + * @param d + * the double to set + */ + public void setDouble(final Double d) { + this.Double = d; + } + + /** + * @param doublePrimitive + * the doublePrimitive to set + */ + public void setDoublePrimitive(double doublePrimitive) { + this.DoublePrimitive = doublePrimitive; + } + + /** + * @param guid + * the guid to set + */ + public void setGuid(final UUID guid) { + this.Guid = guid; + } + + /** + * @param int32 + * the int32 to set + */ + public void setInt32(final Integer int32) { + this.Int32 = int32; + } + + /** + * @param int64 + * the int64 to set + */ + public void setInt64(final Long int64) { + this.Int64 = int64; + } + + /** + * @param integerPrimitive + * the integerPrimitive to set + */ + public void setIntegerPrimitive(int integerPrimitive) { + this.IntegerPrimitive = integerPrimitive; + } + + /** + * @param longPrimitive + * the longPrimitive to set + */ + public void setLongPrimitive(long longPrimitive) { + this.LongPrimitive = longPrimitive; + } + + /** + * @param string + * the string to set + */ + public void setString(final String string) { + this.String = string; + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(java.lang.String.format("%s:%s\n", "PK", this.getPartitionKey())); + builder.append(java.lang.String.format("%s:%s\n", "RK", this.getRowKey())); + builder.append(java.lang.String.format("%s:%s\n", "Timestamp", this.getTimestamp())); + builder.append(java.lang.String.format("%s:%s\n", "etag", this.getEtag())); + + builder.append(java.lang.String.format("%s:%s\n", "DateTime", this.getDateTime())); + builder.append(java.lang.String.format("%s:%s\n", "Bool", this.getBool())); + builder.append(java.lang.String.format("%s:%s\n", "Binary", this.getBinary())); + builder.append(java.lang.String.format("%s:%s\n", "Double", this.getDouble())); + builder.append(java.lang.String.format("%s:%s\n", "Guid", this.getGuid())); + builder.append(java.lang.String.format("%s:%s\n", "Int32", this.getInt32())); + builder.append(java.lang.String.format("%s:%s\n", "Int64", this.getInt64())); + builder.append(java.lang.String.format("%s:%s\n", "String", this.getString())); + + return builder.toString(); + } + } + + public static class IgnoreOnGetter extends class1 { + private String tString = null; + + /** + * @return the string + */ + @Ignore + public String getIgnoreString() { + return this.tString; + } + + /** + * @param string + * the string to set + */ + + public void setIgnoreString(final String string) { + this.tString = string; + } + } + + public static class IgnoreOnGetterAndSetter extends class1 { + private String tString = null; + + /** + * @return the string + */ + @Ignore + public String getIgnoreString() { + return this.tString; + } + + /** + * @param string + * the string to set + */ + @Ignore + public void setIgnoreString(final String string) { + this.tString = string; + } + } + + public static class IgnoreOnSetter extends class1 { + private String tString = null; + + /** + * @return the string + */ + public String getIgnoreString() { + return this.tString; + } + + /** + * @param string + * the string to set + */ + @Ignore + public void setIgnoreString(final String string) { + this.tString = string; + } + } + + public static class StoreAsEntity extends ComplexEntity { + private String storeAsString = null; + + /** + * @return the string + */ + @StoreAs(name = "String") + public String getStoreAsString() { + return this.storeAsString; + } + + /** + * @param string + * the string to set + */ + @StoreAs(name = "String") + public void setStoreAsString(final String string) { + this.storeAsString = string; + } + } + + public static class InvalidStoreAsEntity extends ComplexEntity { + private String storeAsString = null; + + /** + * @return the string + */ + @StoreAs(name = "PartitionKey") + public String getStoreAsString() { + return this.storeAsString; + } + + /** + * @param string + * the string to set + */ + @StoreAs(name = "PartitionKey") + public void setStoreAsString(final String string) { + this.storeAsString = string; + } + } + + public static class TableEnt extends TableServiceEntity { + String TableName; + + /** + * @return the tableName + */ + public String getTableName() { + return this.TableName; + } + + /** + * @param tableName + * the tableName to set + */ + public void setTableName(String tableName) { + this.TableName = tableName; + } + } + + protected static CloudStorageAccount httpAcc; + protected static CloudBlobClient bClient; + protected static CloudQueueClient qClient; + protected static CloudTableClient tClient; + protected static String testSuiteTableName = generateRandomTableName(); + + public static class1 generateRandomEnitity(String pk) { + class1 ref = new class1(); + + ref.setA("foo_A"); + ref.setB("foo_B"); + ref.setC("foo_C"); + ref.setD(new byte[] { 0, 1, 2 }); + ref.setPartitionKey(pk); + ref.setRowKey(UUID.randomUUID().toString()); + return ref; + } + + @BeforeClass + public static void setup() throws URISyntaxException, StorageException, InvalidKeyException { + + // UNCOMMENT TO USE FIDDLER + // System.setProperty("http.proxyHost", "localhost"); + // System.setProperty("http.proxyPort", "8888"); + // System.setProperty("https.proxyHost", "localhost"); + // System.setProperty("https.proxyPort", "8888"); + if (USE_DEV_FABRIC) { + httpAcc = CloudStorageAccount.getDevelopmentStorageAccount(); + } + else { + httpAcc = CloudStorageAccount.parse(CLOUD_ACCOUNT_HTTP); + } + + bClient = httpAcc.createCloudBlobClient(); + tClient = httpAcc.createCloudTableClient(); + qClient = httpAcc.createCloudQueueClient(); + testSuiteTableName = generateRandomTableName(); + tClient.createTable(testSuiteTableName); + } + + @AfterClass + public static void teardown() throws StorageException { + tClient.deleteTable(testSuiteTableName); + } + + protected static String generateRandomTableName() { + String tableName = "table" + UUID.randomUUID().toString(); + return tableName.replace("-", ""); + } +} 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/63] 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 63ec2c1b20865..233f3d8001405 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 c8bf695b7cdda..8f4bc110d4da5 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 096c5b4ec312e..2e32f907f46c0 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 25f042b564f0f..0000000000000 --- 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 d402cd8527cce..b910ff6364d03 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 c8143349741d4..4a4146a5055db 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/63] 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 e796bf00794cc..9e6eef462738d 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 c5803bfb54111..75fdfe4c3a91c 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 b062b3394ab9d..ac505af93f3d0 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 6f514dad26d3c..bb9e36e07d9c0 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/63] 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 096c5b4ec312e..284fbe475ff7c 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/63] 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 d9b0c4c4f2cf0..c83d5dcc1a333 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/63] 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 e7582255ef590..4a6fd5fb37fbc 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 ee52dd47bfb29..ad0334ab03de4 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 49c90e0ddf3d2..33391fc9e711f 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 233f3d8001405..2bb4e1a3ac6ae 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 2e732e4fc1d6e..c37f20da147f8 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 75fdfe4c3a91c..e7b067f5d0ad0 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 ac505af93f3d0..f7df91f533f51 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 8504c88fa349f..6202250942d4f 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 458acf6e0cd96..a0120eb419846 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 22c099a52fee3..a15d80844155d 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 9d2282ab08e68..30aa13ec6bfe8 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 79305ce758c84..ba12447147c89 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 094fe429145a7..6f9990616a822 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 8f4bc110d4da5..29e8bd6dd8129 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 80f16b99187be..bfeddfdb5c807 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 0e97c0193ab5b..064becc8b8650 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 f1df873ac4da0..5de01c01542a8 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 30a0b3beebd94..6ca4400068bb0 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 462be9a865c63..2cdb8473a26d3 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 9dd16b1a1daa8..098b28ae0e397 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 441f3020822b4..161087d46834a 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 5a82435b93375..0f0585f2e22a9 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 ce1d6dff3933c..f74bda6d7003f 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 f0a54f58b09b7..31a6daf581b22 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 34195d4795047..336c436da06c8 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 30ab7196d4489..e56a2c8f9368d 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 de199b24f075f..0e23a0d752c20 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 666fc9634da3e..eca0203954967 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 d67f47162c4d6..08a62dbf957b5 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 1e90530e04f06..197584f920e6d 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 57e7f8c30b660..cf3997dd7225c 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 1de1cd47426b5..fe6842b62082d 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 3977ccc9b97d4..9756661eef96b 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 b910ff6364d03..13cc7fcf70024 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 dfd1c1b7f4aae..9c6158382dba3 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 ffc8e38aef00c..12aaedf407d4e 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 2e068e4fe9dff..73bffc9e03b67 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 917cecc3409c5..2e520bc36db17 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 e42240885c15b..69c19ecba5547 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 42d4b7a870fc4..f6da13830a0cb 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 2db782e636934..dc18aa98996ec 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 b880047b40e818eddd7ecd21e0f8649e3f7b3ddb Mon Sep 17 00:00:00 2001 From: Joost de Nijs <joostden@microsoft.com> Date: Wed, 7 Mar 2012 14:23:34 -0800 Subject: [PATCH 29/63] Switched from HTML to Markdown and added Tables to the Features section --- README.md | 244 +++++++++++++++++++++++++++--------------------------- 1 file changed, 121 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 4e942bcd59b5f..6390ff1f65f7b 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,135 @@ -<h1>Windows Azure SDK for Java</h1> -<p>This SDK allows you to build Windows Azure applications in Java that allow +#Windows Azure SDK for Java + +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.</p> - -<p>For documentation please see the <a href="http://www.windowsazure.com/en-us/develop/java/"> -Windows Azure Java Developer Center</a></p> - -<h1>Features</h1> -<ul> -<li>Blob -<ul> -<li>Create/Read/Update/Delete Blobs</li> -</ul></li> -<li>Queue -<ul> -<li>Create/Delete Queues</li> -<li>Insert/Peek Queue Messages</li> -<li>Advanced Queue Operations</li> -</ul></li> -<li>Service Bus -<ul> -<li>Use either the Queue or Topic/Subscription Model</li> -</ul></li> -<li>Service Runtime -<ul> -<li>Retrieve information about the state of your Azure Compute instances</li> -</ul></li> -</ul> - -<h1>Getting Started</h1> -<h2>Download</h2> -<h3>Option 1: Via Git</h3> -<p>To get the source code of the SDK via git just type:<br/> -<pre>git clone git://github.com/WindowsAzure/azure-sdk-for-java.git -cd ./azure-sdk-for-java -mvn compile</pre> - -<h3>Option 2: Via Maven</h3> -<p>To get the binaries of this library as distributed by Microsoft, ready for use -within your project you can also have them installed by the Java package manager Maven.<br/> -<pre><dependency> - <groupId>com.microsoft.windowsazure</groupId> - <artifactId>microsoft-windowsazure-api</artifactId> - <version>0.1.0</version> -</dependency></pre></p> - -<h2>Minimum Requirements</h2> -<ul> -<li>Java 1.6</li> -<li>(Optional) Maven</li> -</ul> - -<h2>Usage</h2> -<p>To use this SDK to call Windows Azure services, you need to first create an +storage, messaging through Service Bus. + +For documentation please see the [Windows Azure Java Developer Center](http://www.windowsazure.com/en-us/develop/java/) + +#Features +* Blob + * Create/Read/Update/Delete Blobs +* Queue + * Create/Delete Queues + * Insert/Peek Queue Messages + * Advanced Queue Operations +* Service Bus + * Use either the Queue or Topic/Subscription Model +* Service Runtime + * Retrieve information about the state of your Azure Compute instances +* Table + * Manage Tables + * Work with Table Entities (CRUD) + * Entity Group Transactions (Batch) + +#Getting Started + +##Download +###Option 1: Via Git + +To get the source code of the SDK via git just type: + + git clone git://github.com/WindowsAzure/azure-sdk-for-java.git + cd ./azure-sdk-for-java + mvn compile + +###Option 2: Via Maven + +To get the binaries of this library as distributed by Microsoft, ready for use +within your project you can also have them installed by the Java package manager Maven. + + <dependency> + <groupId>com.microsoft.windowsazure</groupId> + <artifactId>microsoft-windowsazure-api</artifactId> + <version>0.1.0</version> + </dependency> + +##Minimum Requirements + +* Java 1.6 +* (Optional) Maven + + +##Usage + +To use this SDK to call Windows Azure services, you need to first create an account. To host your Java code in Windows Azure, you additionally need to download the full Windows Azure SDK for Java - which includes packaging, emulation, and -deployment tools.</p> +deployment tools. + +##Code Samples -<h2>Code Samples</h2> -<p>The following is a quick example on how to set up a Azure blob using the API -and uploading a file to it. For additional information on using the client libraries to access Azure services see the How To guides listed <a href="http://www.windowsazure.com/en-us/develop/java/"> -here</a>.<br/> +The following is a quick example on how to set up a Azure blob using the API +and uploading a file to it. For additional information on using the client libraries to access Azure services see the How To guides listed [here](http://www.windowsazure.com/en-us/develop/java/). -<pre>import com.microsoft.windowsazure.services.core.storage.*; -import com.microsoft.windowsazure.services.blob.client.*; + import com.microsoft.windowsazure.services.core.storage.*; + import com.microsoft.windowsazure.services.blob.client.*; -public class BlobSample { + public class BlobSample { - public static final String storageConnectionString = - "DefaultEndpointsProtocol=http;" + - "AccountName=your_account_name;" + - "AccountKey= your_account_name"; + public static final String storageConnectionString = + "DefaultEndpointsProtocol=http;" + + "AccountName=your_account_name;" + + "AccountKey= your_account_name"; - public static void main(String[] args) - { - try + public static void main(String[] args) { - CloudStorageAccount account; - CloudBlobClient serviceClient; - CloudBlobContainer container; - CloudBlockBlob blob; + try + { + CloudStorageAccount account; + CloudBlobClient serviceClient; + CloudBlobContainer container; + CloudBlockBlob blob; - account = CloudStorageAccount.parse(storageConnectionString); - serviceClient = account.createCloudBlobClient(); - // Container name must be lower case. - container = serviceClient.getContainerReference("blobsample"); - container.createIfNotExist(); + account = CloudStorageAccount.parse(storageConnectionString); + serviceClient = account.createCloudBlobClient(); + // Container name must be lower case. + container = serviceClient.getContainerReference("blobsample"); + container.createIfNotExist(); - // Set anonymous access on the container. - BlobContainerPermissions containerPermissions; - containerPermissions = new BlobContainerPermissions(); - - // Upload an image file. - blob = container.getBlockBlobReference("image1.jpg"); - File fileReference = new File ("c:\\myimages\\image1.jpg"); - blob.upload(new FileInputStream(fileReference), fileReference.length()); - } - catch (FileNotFoundException fileNotFoundException) - { - System.out.print("FileNotFoundException encountered: "); - System.out.println(fileNotFoundException.getMessage()); - System.exit(-1); - } - catch (StorageException storageException) - { - System.out.print("StorageException encountered: "); - System.out.println(storageException.getMessage()); - System.exit(-1); - } - catch (Exception e) - { - System.out.print("Exception encountered: "); - System.out.println(e.getMessage()); - System.exit(-1); - } + // Set anonymous access on the container. + BlobContainerPermissions containerPermissions; + containerPermissions = new BlobContainerPermissions(); + + // Upload an image file. + blob = container.getBlockBlobReference("image1.jpg"); + File fileReference = new File ("c:\\myimages\\image1.jpg"); + blob.upload(new FileInputStream(fileReference), fileReference.length()); + } + catch (FileNotFoundException fileNotFoundException) + { + System.out.print("FileNotFoundException encountered: "); + System.out.println(fileNotFoundException.getMessage()); + System.exit(-1); + } + catch (StorageException storageException) + { + System.out.print("StorageException encountered: "); + System.out.println(storageException.getMessage()); + System.exit(-1); + } + catch (Exception e) + { + System.out.print("Exception encountered: "); + System.out.println(e.getMessage()); + System.exit(-1); + } + } } -} -</pre></p> - -<h1>Need Help?</h1> -<p>Be sure to check out the Windows Azure <a href="http://go.microsoft.com/fwlink/?LinkId=234489"> -Developer Forums on Stack Overflow</a> if you have trouble with the provided code.</p> - -<h1>Contribute Code or Provide Feedback</h1> -<p>If you would like to become an active contributor to this project please follow the instructions provided in <a href="http://windowsazure.github.com/guidelines.html">Windows Azure Projects Contribution Guidelines</a>.</p> -<p>If you encounter any bugs with the library please file an issue in the <a href="https://github.com/WindowsAzure/azure-sdk-for-java/issues">Issues</a> section of the project.</p> - -<h1>Learn More</h1> -<ul> - <li><a href="http://www.windowsazure.com/en-us/develop/java/">Windows Azure Java - Developer Center</a></li> - <li><a href="http://dl.windowsazure.com/javadoc/"> - JavaDocs</a></li> -</ul> + +#Need Help? + +Be sure to check out the Windows Azure [Developer Forums on Stack Overflow](http://go.microsoft.com/fwlink/?LinkId=234489) if you have trouble with the provided code. + +#Contribute Code or Provide Feedback + +If you would like to become an active contributor to this project please follow the instructions provided in [Windows Azure Projects Contribution Guidelines](http://windowsazure.github.com/guidelines.html). + +If you encounter any bugs with the library please file an issue in the [Issues](https://github.com/WindowsAzure/azure-sdk-for-java/issues) section of the project. + +#Learn More + +* [Windows Azure Java Developer Center](http://www.windowsazure.com/en-us/develop/java/) +* [JavaDocs](http://dl.windowsazure.com/javadoc/) 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 30/63] 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 73bffc9e03b67..01a18543864b1 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 31/63] 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 e7b067f5d0ad0..906c4bdc4a757 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 32/63] 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 f7df91f533f51..41b38735afa51 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 33/63] 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 ba12447147c89..7e2e95bb88f9a 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 bfeddfdb5c807..e9a5dbc846493 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 34/63] 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 4677d0431b818..5816a1bec264d 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 35/63] 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 234581b023fb8..f7aae2cd9188f 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 36/63] 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 9e6eef462738d..9fa415c44c8f1 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 e7b067f5d0ad0..3324a62d68c55 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 f7df91f533f51..a9ec25452eea8 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 bb9e36e07d9c0..a777b001c5841 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 37/63] 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 bfeddfdb5c807..fb22323fa3573 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 38/63] 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 bfeddfdb5c807..c65db1d6bbf06 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 39/63] 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 bfeddfdb5c807..af01e789b06e6 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 40/63] 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 bfeddfdb5c807..11c59390988fa 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 161087d46834a..4cce1e14a24a9 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 fe6842b62082d..31c7d05652bc7 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 13cc7fcf70024..36b975d7a6900 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 69c19ecba5547..c4a733a6e3a6b 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 c18bddb9b58fb8fde3257b76c66ccd8211aee966 Mon Sep 17 00:00:00 2001 From: Colin Robertson <a-colinr@microsoft.com> Date: Thu, 15 Mar 2012 16:26:56 -0700 Subject: [PATCH 41/63] Add Javadocs comments to /queue and /queue/models --- .../services/queue/QueueConfiguration.java | 22 +- .../services/queue/QueueContract.java | 354 +++++++++++++++++- .../services/queue/QueueService.java | 66 +++- .../queue/models/CreateMessageOptions.java | 80 +++- .../queue/models/CreateQueueOptions.java | 70 +++- .../queue/models/GetQueueMetadataResult.java | 55 ++- .../models/GetServicePropertiesResult.java | 47 ++- .../queue/models/ListMessagesOptions.java | 71 +++- .../queue/models/ListMessagesResult.java | 146 +++++++- .../queue/models/ListQueuesOptions.java | 121 +++++- .../queue/models/ListQueuesResult.java | 183 ++++++++- .../queue/models/PeekMessagesOptions.java | 54 ++- .../queue/models/PeekMessagesResult.java | 115 +++++- .../queue/models/QueueServiceOptions.java | 42 ++- .../queue/models/ServiceProperties.java | 246 +++++++++++- .../queue/models/UpdateMessageResult.java | 55 ++- 16 files changed, 1599 insertions(+), 128 deletions(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueConfiguration.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueConfiguration.java index 165df11b234b0..1fad44dfe643f 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueConfiguration.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueConfiguration.java @@ -2,18 +2,24 @@ * 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.queue; +/** + * A class that contains static strings used to identify parts of a service configuration instance associated with the + * Windows Azure Queue service. + * <p> + * These values must not be altered. + */ public class QueueConfiguration { public final static String ACCOUNT_NAME = "queue.accountName"; public final static String ACCOUNT_KEY = "queue.accountKey"; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.java index 62a6e2b5fb801..7abab4aebde5d 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.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.queue; @@ -32,61 +32,399 @@ import com.microsoft.windowsazure.services.queue.models.ServiceProperties; import com.microsoft.windowsazure.services.queue.models.UpdateMessageResult; +/** + * Defines the methods available on the Windows Azure storage queue service. Construct an object instance implementing + * <strong>QueueContract</strong> with one of the static <em>create</em> methods on {@link QueueService}. These methods + * associate a {@link Configuration} with the implementation, so the methods on the instance of + * <strong>QueueContract</strong> all work with a particular storage account. + */ public interface QueueContract extends FilterableService<QueueContract> { + /** + * Gets the service properties of the queue. + * + * @return + * A {@link GetServicePropertiesResult} reference to the queue service properties. + * + * @throws ServiceException + * if an error occurs accessing the storage service. + */ GetServicePropertiesResult getServiceProperties() throws ServiceException; + /** + * Gets the service properties of the queue, using the specified options. Use the {@link QueueServiceOptions + * options} parameter to specify the server timeout for the operation. + * + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @return + * A {@link GetServicePropertiesResult} reference to the queue service properties. + * + * @throws ServiceException + * if an error occurs accessing the storage service. + */ GetServicePropertiesResult getServiceProperties(QueueServiceOptions options) throws ServiceException; + /** + * Sets the service properties of the queue. + * + * @param serviceProperties + * A {@link ServiceProperties} instance containing the queue service properties to set. + * + * @throws ServiceException + * if an error occurs accessing the storage service. + */ void setServiceProperties(ServiceProperties serviceProperties) throws ServiceException; + /** + * Sets the service properties of the queue, using the specified options. Use the {@link QueueServiceOptions + * options} parameter to specify the server timeout for the operation. + * + * @param serviceProperties + * A {@link ServiceProperties} instance containing the queue service properties to set. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @throws ServiceException + * if an error occurs accessing the storage service. + */ void setServiceProperties(ServiceProperties serviceProperties, QueueServiceOptions options) throws ServiceException; + /** + * Creates a queue in the storage account with the specified queue name. + * + * @param queue + * A {@link String} containing the name of the queue to create. + * + * @throws ServiceException + * if an error occurs in the storage service. + */ void createQueue(String queue) throws ServiceException; + /** + * Creates a queue in the storage account with the specified queue name, using the specified options. Use the + * {@link QueueServiceOptions options} parameter to specify the server timeout for the operation. + * + * @param queue + * A {@link String} containing the name of the queue to create. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @throws ServiceException + * if an error occurs in the storage service. + */ void createQueue(String queue, CreateQueueOptions options) throws ServiceException; + /** + * Deletes the queue in the storage account with the specified queue name. + * + * @param queue + * A {@link String} containing the name of the queue to delete. + * + * @throws ServiceException + * if an error occurs in the storage service. + */ void deleteQueue(String queue) throws ServiceException; + /** + * Deletes the queue in the storage account with the specified queue name, using the specified options. Use the + * {@link QueueServiceOptions options} parameter to specify the server timeout for the operation. + * + * @param queue + * A {@link String} containing the name of the queue to delete. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @throws ServiceException + * if an error occurs in the storage service. + */ void deleteQueue(String queue, QueueServiceOptions options) throws ServiceException; + /** + * Gets a list of the queues in the storage account. + * + * @return + * A {@link ListQueuesResult} reference to the queues returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ ListQueuesResult listQueues() throws ServiceException; + /** + * Gets a list of the queues in the storage account, using the specified options. Use the {@link ListQueuesOptions + * options} parameter to specify the server timeout for the operation, the prefix for queue names to match, the + * marker for the beginning of the queues to list, the maximum number of results to return, and whether to include + * queue metadata with the results. + * + * @param options + * A {@link ListQueuesOptions} instance containing options for the request. + * @return + * A {@link ListQueuesResult} reference to the queues returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ ListQueuesResult listQueues(ListQueuesOptions options) throws ServiceException; + /** + * Gets the metadata for the named queue in the storage account. Queue metadata is a user-defined collection of + * key-value {@link String} pairs that is opaque to the server. + * + * @param queue + * A {@link String} containing the name of the queue to get the metadata for. + * @return + * A {@link GetQueueMetadataResult} reference to the metadata for the queue. + * @throws ServiceException + * if an error occurs in the storage service. + */ GetQueueMetadataResult getQueueMetadata(String queue) throws ServiceException; + /** + * Gets the metadata for the named queue in the storage account, using the specified options. Use the + * {@link QueueServiceOptions options} parameter to specify the server timeout for the operation. Queue metadata is + * a user-defined collection of key-value {@link String} pairs that is opaque to the server. + * + * @param queue + * A {@link String} containing the name of the queue to get the metadata for. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @return + * A {@link ListQueuesResult} reference to the queues returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ GetQueueMetadataResult getQueueMetadata(String queue, QueueServiceOptions options) throws ServiceException; + /** + * Sets the specified metadata on the named queue in the storage account. Queue metadata is a user-defined + * collection of key-value {@link String} pairs that is opaque to the server. + * + * @param queue + * A {@link String} containing the name of the queue to set the metadata on. + * @param metadata + * A {@link java.util.HashMap} of metadata key-value {@link String} pairs to set on the queue. + * @throws ServiceException + * if an error occurs in the storage service. + */ void setQueueMetadata(String queue, HashMap<String, String> metadata) throws ServiceException; + /** + * Sets the specified metadata on the named queue in the storage account, using the specified options. Use the + * {@link QueueServiceOptions options} parameter to specify the server timeout for the operation. Queue metadata is + * a user-defined collection of key-value {@link String} pairs that is opaque to the server. + * + * @param queue + * A {@link String} containing the name of the queue to set the metadata on. + * @param metadata + * A {@link java.util.HashMap} of metadata key-value {@link String} pairs to set on the queue. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @throws ServiceException + * if an error occurs in the storage service. + */ void setQueueMetadata(String queue, HashMap<String, String> metadata, QueueServiceOptions options) throws ServiceException; + /** + * Appends a message with the specified text to the tail of the named queue in the storage account. + * + * @param queue + * A {@link String} containing the name of the queue to append the message to. + * @param messageText + * A {@link String} containing the text of the message to append to the queue. + * @throws ServiceException + * if an error occurs in the storage service. + */ void createMessage(String queue, String messageText) throws ServiceException; + /** + * Appends a message with the specified text to the tail of the named queue in the storage account, using the + * specified options. Use the {@link CreateMessageOptions options} parameter to specify the server timeout for the + * operation, the message visibility timeout, and the message time to live in the queue. + * + * @param queue + * A {@link String} containing the name of the queue to append the message to. + * @param messageText + * A {@link String} containing the text of the message to append to the queue. + * @param options + * A {@link CreateMessageOptions} instance containing options for the request. + * @throws ServiceException + * if an error occurs in the storage service. + */ void createMessage(String queue, String messageText, CreateMessageOptions options) throws ServiceException; + /** + * Updates the message in the named queue with the specified message ID and pop receipt value to have the specified + * message text and visibility timeout value. + * + * @param queue + * A {@link String} containing the name of the queue with the message to update. + * @param messageId + * A {@link String} containing the ID of the message to update. + * @param popReceipt + * A {@link String} containing the pop receipt for the message returned by a call to updateMessage or + * listMessages. + * @param messageText + * A {@link String} containing the updated text to set for the message. + * @param visibilityTimeoutInSeconds + * The new visibility timeout to set on the message, in seconds. + * @return + * An {@link UpdateMessageResult} reference to the updated message result returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ UpdateMessageResult updateMessage(String queue, String messageId, String popReceipt, String messageText, int visibilityTimeoutInSeconds) throws ServiceException; + /** + * Updates the message in the named queue with the specified message ID and pop receipt value to have the specified + * message text and visibility timeout value, using the specified options. Use the {@link QueueServiceOptions + * options} parameter to specify the server timeout for the operation. + * + * @param queue + * A {@link String} containing the name of the queue with the message to update. + * @param messageId + * A {@link String} containing the ID of the message to update. + * @param popReceipt + * A {@link String} containing the pop receipt for the message returned by a call to updateMessage or + * listMessages. + * @param messageText + * A {@link String} containing the updated text to set for the message. + * @param visibilityTimeoutInSeconds + * The new visibility timeout to set on the message, in seconds. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @return + * An {@link UpdateMessageResult} reference to the updated message result returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ UpdateMessageResult updateMessage(String queue, String messageId, String popReceipt, String messageText, int visibilityTimeoutInSeconds, QueueServiceOptions options) throws ServiceException; + /** + * Deletes the message in the named queue with the specified message ID and pop receipt value. + * + * @param queue + * A {@link String} containing the name of the queue with the message to delete. + * @param messageId + * A {@link String} containing the ID of the message to delete. + * @param popReceipt + * A {@link String} containing the pop receipt for the message returned by a call to updateMessage or + * listMessages. + * @throws ServiceException + * if an error occurs in the storage service. + */ void deleteMessage(String queue, String messageId, String popReceipt) throws ServiceException; + /** + * Deletes the message in the named queue with the specified message ID and popReceipt value, using the specified + * options. Use the {@link QueueServiceOptions options} parameter to specify the server timeout for the operation. + * + * @param queue + * A {@link String} containing the name of the queue with the message to delete. + * @param messageId + * A {@link String} containing the ID of the message to delete. + * @param popReceipt + * A {@link String} containing the pop receipt for the message returned by a call to updateMessage or + * listMessages. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @throws ServiceException + * if an error occurs in the storage service. + */ void deleteMessage(String queue, String messageId, String popReceipt, QueueServiceOptions options) throws ServiceException; + /** + * Retrieves the first message from head of the named queue in the storage account. This marks the message as + * invisible for the default visibility timeout period. When message processing is complete, the message must be + * deleted with a call to {@link QueueContract#deleteMessage(String, String, String, QueueServiceOptions) + * deleteMessage}. If message processing will take longer than the visibility timeout period, + * use the {@link QueueContract#updateMessage(String, String, String, String, int, QueueServiceOptions) + * updateMessage} method to extend the visibility timeout. The message will become visible in the queue again + * when the timeout completes if it is not deleted. + * <p> + * To get a list of multiple messages from the head of the queue, call the + * {@link QueueContract#listMessages(String, ListMessagesOptions)} method with options set specifying the number of + * messages to return. + * + * @param queue + * A {@link String} containing the name of the queue to get the message from. + * @return + * A {@link ListMessagesResult} reference to the message result returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ ListMessagesResult listMessages(String queue) throws ServiceException; + /** + * Retrieves up to 32 messages from the head of the named queue in the storage account, using the specified options. + * Use the {@link ListMessagesOptions options} parameter to specify the server timeout for the operation, the number + * of messages to retrieve, and the visibility timeout to set on the retrieved messages. When message processing is + * complete, each message must be deleted with a call to + * {@link QueueContract#deleteMessage(String, String, String, QueueServiceOptions) deleteMessage}. If message + * processing takes longer than the default timeout period, use the + * {@link QueueContract#updateMessage(String, String, String, String, int, QueueServiceOptions) updateMessage} + * method to extend the visibility timeout. Each message will become + * visible in the queue again when the timeout completes if it is not deleted. + * + * @param queue + * A {@link String} containing the name of the queue to get the messages from. + * @param options + * A {@link ListMessagesOptions} instance containing options for the request. + * @return + * A {@link ListMessagesResult} reference to the message result returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ ListMessagesResult listMessages(String queue, ListMessagesOptions options) throws ServiceException; + /** + * Peeks a message from the named queue. A peek request retrieves a message from the head of the queue without + * changing its visibility. + * + * @param queue + * A {@link String} containing the name of the queue to peek the message from. + * @return + * A {@link PeekMessagesResult} reference to the message result returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ PeekMessagesResult peekMessages(String queue) throws ServiceException; + /** + * Peeks messages from the named queue, using the specified options. A peek request retrieves messages from the + * head of the queue without changing their visibility. Use the {@link PeekMessagesOptions options} parameter to + * specify the server timeout for the operation and the number of messages to retrieve. + * + * @param queue + * A {@link String} containing the name of the queue to peek the message from. + * @param options + * A {@link PeekMessagesOptions} instance containing options for the request. + * @return + * A {@link PeekMessagesResult} reference to the message result returned. + * @throws ServiceException + * if an error occurs in the storage service. + */ PeekMessagesResult peekMessages(String queue, PeekMessagesOptions options) throws ServiceException; + /** + * Deletes all messages in the named queue. + * + * @param queue + * A {@link String} containing the name of the queue to delete all messages from. + * @throws ServiceException + * if an error occurs in the storage service. + */ void clearMessages(String queue) throws ServiceException; + /** + * Deletes all messages in the named queue, using the specified options. Use the {@link QueueServiceOptions options} + * parameter to specify the server timeout for the operation. + * + * @param queue + * A {@link String} containing the name of the queue to delete all messages from. + * @param options + * A {@link QueueServiceOptions} instance containing options for the request. + * @throws ServiceException + * if an error occurs in the storage service. + */ void clearMessages(String queue, QueueServiceOptions options) throws ServiceException; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueService.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueService.java index c457b6b34d39f..a610b75007065 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueService.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueService.java @@ -2,36 +2,86 @@ * 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.queue; import com.microsoft.windowsazure.services.core.Configuration; +/** + * A class for static factory methods that return instances implementing {@link QueueContract}. + */ public class QueueService { + /** + * Private default constructor. + */ private QueueService() { } + /** + * A static factory method that returns an instance implementing {@link QueueContract} using default values for + * initializing a {@link Configuration} instance. Note that the returned interface will not work unless storage + * account credentials have been added to the "META-INF/com.microsoft.windowsazure.properties" resource file. + * + * @return + * An instance implementing {@link QueueContract} for interacting with the queue service. + */ public static QueueContract create() { return create(null, Configuration.getInstance()); } + /** + * A static factory method that returns an instance implementing {@link QueueContract} using the specified + * {@link Configuration} instance. The {@link Configuration} instance must have storage account information and + * credentials set before this method is called for the returned interface to work. + * + * @param config + * A {@link Configuration} instance configured with storage account information and credentials. + * + * @return + * An instance implementing {@link QueueContract} for interacting with the queue service. + */ public static QueueContract create(Configuration config) { return create(null, config); } + /** + * A static factory method that returns an instance implementing {@link QueueContract} using default values for + * initializing a {@link Configuration} instance, and using the specified profile prefix for service settings. Note + * that the returned interface will not work unless storage account settings and credentials have been added to the + * "META-INF/com.microsoft.windowsazure.properties" resource file with the specified profile prefix. + * + * @param profile + * A string prefix for the account name and credentials settings in the {@link Configuration} instance. + * @return + * An instance implementing {@link QueueContract} for interacting with the queue service. + */ public static QueueContract create(String profile) { return create(profile, Configuration.getInstance()); } + /** + * A static factory method that returns an instance implementing {@link QueueContract} using the specified + * {@link Configuration} instance and profile prefix for service settings. The {@link Configuration} instance must + * have storage account information and credentials set with the specified profile prefix before this method is + * called for the returned interface to work. + * + * @param profile + * A string prefix for the account name and credentials settings in the {@link Configuration} instance. + * @param config + * A {@link Configuration} instance configured with storage account information and credentials. + * + * @return + * An instance implementing {@link QueueContract} for interacting with the queue service. + */ public static QueueContract create(String profile, Configuration config) { return config.create(profile, QueueContract.class); } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java index f227771970f49..f318e7700b26c 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java @@ -2,41 +2,105 @@ * 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.queue.models; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * Represents the options that may be set on the Queue service for + * {@link QueueContract#createMessage(String, String, CreateMessageOptions) createMessage} requests. These options + * include a server response timeout for the request, the visibility timeout to set on the created message, and the + * time-to-live value to set on the message. + */ public class CreateMessageOptions extends QueueServiceOptions { private Integer visibilityTimeoutInSeconds; private Integer timeToLiveInSeconds; + /** + * Sets the server request timeout value associated with this {@link CreateMessageOptions} instance. + * <p> + * The <em>timeout</em> value only affects calls made on methods where this {@link CreateMessageOptions} instance is + * passed as a parameter. + * + * @param timeout + * The server request timeout value to set in milliseconds. + * @return + * A reference to this {@link CreateMessageOptions} instance. + */ @Override public CreateMessageOptions setTimeout(Integer timeout) { super.setTimeout(timeout); return this; } + /** + * Gets the message visibility timeout in seconds value in this {@link CreateMessageOptions} instance. + * to set on messages when making a {@link QueueContract#createMessage(String, String, CreateMessageOptions) + * createMessage} request. + * + * @return + * The message visibility timeout in seconds. + */ public Integer getVisibilityTimeoutInSeconds() { return visibilityTimeoutInSeconds; } + /** + * Sets the message visibility timeout in seconds value to set on messages when making a + * {@link QueueContract#createMessage(String, String, CreateMessageOptions) createMessage} request. This allows + * messages to be loaded into the queue but not become visible until the visibility timeout has passed. + * Valid visibility timeout values range from 0 to 604800 seconds (0 to 7 days), and must be less than the + * time-to-live value. + * <p> + * The <em>visibilityTimeoutInSeconds</em> value only affects calls made on methods where this + * {@link CreateMessageOptions} instance is passed as a parameter. + * + * @param visibilityTimeoutInSeconds + * The length of time during which the message will be invisible, starting when it is added to the queue, + * or 0 to make the message visible immediately. This value must be greater than or equal to zero and + * less than or equal to the time-to-live value. + * @return + * A reference to this {@link CreateMessageOptions} instance. + */ public CreateMessageOptions setVisibilityTimeoutInSeconds(Integer visibilityTimeoutInSeconds) { this.visibilityTimeoutInSeconds = visibilityTimeoutInSeconds; return this; } + /** + * Gets the message time-to-live in seconds value associated with this {@link CreateMessageOptions} instance. + * + * @return + * The message time-to-live value in seconds. + */ public Integer getTimeToLiveInSeconds() { return timeToLiveInSeconds; } + /** + * Sets the message time-to-live timeout value to set on messages when making a + * {@link QueueContract#createMessage(String, String, CreateMessageOptions) createMessage} request. This is the + * maximum duration in seconds for the message to remain in the queue after it is created. + * Valid <em>timeToLiveInSeconds</em> values range from 0 to 604800 seconds (0 to 7 days), with the default value + * set to seven days. + * <p> + * The <em>timeToLiveInSeconds</em> value only affects calls made on methods where this {@link CreateMessageOptions} + * instance is passed as a parameter. + * + * @param timeToLiveInSeconds + * The maximum time to allow the message to be in the queue, in seconds. + * @return + */ public CreateMessageOptions setTimeToLiveInSeconds(Integer timeToLiveInSeconds) { this.timeToLiveInSeconds = timeToLiveInSeconds; return this; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateQueueOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateQueueOptions.java index cb79876571241..81ff8debedf30 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateQueueOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateQueueOptions.java @@ -2,38 +2,92 @@ * 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.queue.models; import java.util.HashMap; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * Represents the options that may be set on a queue when created in the storage service with a + * {@link QueueContract#createQueue(String, CreateQueueOptions) createQueue} request. These options include a server + * response timeout for the request and the metadata to associate with the created queue. + */ public class CreateQueueOptions extends QueueServiceOptions { private HashMap<String, String> metadata = new HashMap<String, String>(); + /** + * Sets the server request timeout value associated with this {@link CreateQueueOptions} instance. + * <p> + * The <em>timeout</em> value only affects calls made on methods where this {@link CreateQueueOptions} instance is + * passed as a parameter. + * + * @param timeout + * The server request timeout value to set in milliseconds. + * @return + * A reference to this {@link CreateQueueOptions} instance. + */ @Override public CreateQueueOptions setTimeout(Integer timeout) { super.setTimeout(timeout); return this; } + /** + * Gets the metadata collection of key-value {@link String} pairs to set on a queue when the queue is created. + * + * @return + * A {@link java.util.HashMap} of key-value {@link String} pairs containing the metadata to set on the + * queue. + */ public HashMap<String, String> getMetadata() { return metadata; } + /** + * Sets the metadata collection of key-value {@link String} pairs to set on a queue when the queue is created. Queue + * metadata is a user-defined collection of key-value pairs that is opaque to the server. + * <p> + * The <em>metadata</em> value is only added to a newly created queue where this {@link CreateQueueOptions} instance + * is passed as a parameter. + * + * @param metadata + * The {@link java.util.HashMap} of key-value {@link String} pairs containing the metadata to set on the + * queue. + * @return + * A reference to this {@link CreateQueueOptions} instance. + */ public CreateQueueOptions setMetadata(HashMap<String, String> metadata) { this.metadata = metadata; return this; } + /** + * Adds a key-value pair of {@link String} to the metadata collection to set on a queue when the queue is created. + * Queue metadata is a user-defined collection of key-value pairs that is opaque to the server. If the key already + * exists in the metadata collection, the value parameter will overwrite the existing value paired with that key + * without notification. + * <p> + * The updated metadata is only added to a newly created queue where this {@link CreateQueueOptions} instance is + * passed as a parameter. + * + * @param key + * A {@link String} containing the key part of the key-value pair to add to the metadata. + * @param value + * A {@link String} containing the value part of the key-value pair to add to the metadata. + * @return + * A reference to this {@link CreateQueueOptions} instance. + */ public CreateQueueOptions addMetadata(String key, String value) { this.metadata.put(key, value); return this; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetQueueMetadataResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetQueueMetadataResult.java index e412e00ece63a..b522a7c589e90 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetQueueMetadataResult.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetQueueMetadataResult.java @@ -2,36 +2,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 + * 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.queue.models; import java.util.HashMap; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * A wrapper class for the result returned from a Queue Service REST API operation to get queue metadata. This is + * returned by calls to implementations of {@link QueueContract#getQueueMetadata(String)} and + * {@link QueueContract#getQueueMetadata(String, QueueServiceOptions)}. + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179384.aspx">Get Queue Metadata</a> + * documentation on MSDN for details of the underlying Queue Service REST API operation. + */ public class GetQueueMetadataResult { private long approximateMessageCount; private HashMap<String, String> metadata; + /** + * Gets the queue's approximate message count, as reported by the server. + * + * @return + * The queue's approximate message count. + */ public long getApproximateMessageCount() { return approximateMessageCount; } + /** + * Reserved for internal use. This method is invoked by the API as part of the response generation from the + * Queue Service REST API operation to set the value for the approximate message count returned by the server. + * + * @param approximateMessageCount + * The queue's approximate message count to set. + */ public void setApproximateMessageCount(long approximateMessageCount) { this.approximateMessageCount = approximateMessageCount; } + /** + * Gets the metadata collection of key-value {@link String} pairs currently set on a queue. Queue metadata is a + * user-defined collection of key-value pairs that is opaque to the server. + * + * @return + * A {@link java.util.HashMap} of key-value {@link String} pairs containing the metadata set on the + * queue. + */ public HashMap<String, String> getMetadata() { return metadata; } + /** + * Reserved for internal use. This method is invoked by the API as part of the response generation from the + * Queue Service REST API operation to set the value from the queue metadata returned by the server. + * + * @param metadata + * A {@link java.util.HashMap} of key-value {@link String} pairs containing the metadata set on the + * queue. + */ public void setMetadata(HashMap<String, String> metadata) { this.metadata = metadata; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetServicePropertiesResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetServicePropertiesResult.java index 2a7e788205659..4dff09479acf2 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetServicePropertiesResult.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/GetServicePropertiesResult.java @@ -2,25 +2,56 @@ * 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.queue.models; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * A wrapper class for the service properties returned in response to Queue Service REST API operations. This is + * returned by calls to implementations of {@link QueueContract#getServiceProperties()} and + * {@link QueueContract#getServiceProperties(QueueServiceOptions)}. + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/hh452243.aspx">Get Queue Service Properties</a> + * documentation on MSDN for details of the underlying Queue Service REST API operation. + */ public class GetServicePropertiesResult { private ServiceProperties value; + /** + * Gets a {@link ServiceProperties} instance containing the service property values associated with the storage + * account. + * <p> + * Modifying the values in the {@link ServiceProperties} instance returned does not affect the values associated + * with the storage account. To change the values in the storage account, call the + * {@link QueueContract#setServiceProperties} method and pass the modified {@link ServiceProperties} instance as a + * parameter. + * + * @return + * A {@link ServiceProperties} instance containing the property values associated with the storage account. + */ public ServiceProperties getValue() { return value; } + /** + * Reserved for internal use. Sets the value of the {@link ServiceProperties} instance associated with a + * storage service call result. This method is invoked by the API to store service properties returned by + * a call to a REST operation and is not intended for public use. + * + * @param value + * A {@link ServiceProperties} instance containing the property values associated with the storage + * account. + */ public void setValue(ServiceProperties value) { this.value = value; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesOptions.java index 2aa19b6409441..0fb34021981c2 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesOptions.java @@ -2,41 +2,96 @@ * 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.queue.models; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * Represents the options that may be set on a {@link QueueContract#listMessages(String, ListMessagesOptions) + * listMessages} request. These options include a server response timeout for the request, the number of messages to + * retrieve from the queue, and the visibility timeout to set on the retrieved messages. + */ public class ListMessagesOptions extends QueueServiceOptions { private Integer numberOfMessages; private Integer visibilityTimeoutInSeconds; + /** + * Sets the server request timeout value associated with this {@link ListMessagesOptions} instance. + * <p> + * The <em>timeout</em> value only affects calls made on methods where this {@link ListMessagesOptions} instance is + * passed as a parameter. + * + * @param timeout + * The server request timeout value to set in milliseconds. + * @return + * A reference to this {@link ListMessagesOptions} instance. + */ @Override public ListMessagesOptions setTimeout(Integer timeout) { super.setTimeout(timeout); return this; } + /** + * Gets the number of messages to request from the queue with this {@link ListMessagesOptions} instance. + * + * @return + * The number of messages requested. + */ public Integer getNumberOfMessages() { return numberOfMessages; } + /** + * Sets the number of messages to request from the queue with this {@link ListMessagesOptions} instance. + * <p> + * The <em>numberOfMessages</em> value is only used for requests where this {@link ListMessagesOptions} instance is + * passed as a parameter. + * + * @param numberOfMessages + * The number of messages to request. The valid range of values is 0 to 32. + * @return + * A reference to this {@link ListMessagesOptions} instance. + */ public ListMessagesOptions setNumberOfMessages(Integer numberOfMessages) { this.numberOfMessages = numberOfMessages; return this; } + /** + * Gets the visibility timeout to set on the messages requested from the queue with this {@link ListMessagesOptions} + * instance. + * + * @return + * The visibility timeout to set on the messages requested from the queue. + */ public Integer getVisibilityTimeoutInSeconds() { return visibilityTimeoutInSeconds; } + /** + * Sets the visibility timeout value to set on the messages requested from the queue with this + * {@link ListMessagesOptions} instance. + * <p> + * The <em>visibilityTimeoutInSeconds</em> value is only used for requests where this {@link ListMessagesOptions} + * instance is passed as a parameter. + * + * @param visibilityTimeoutInSeconds + * The visibility timeout to set on the messages requested from the queue. The valid range of values is 0 + * to 604800 seconds. + * @return + * A reference to this {@link ListMessagesOptions} instance. + */ public ListMessagesOptions setVisibilityTimeoutInSeconds(Integer visibilityTimeoutInSeconds) { this.visibilityTimeoutInSeconds = visibilityTimeoutInSeconds; return this; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesResult.java index af7c53f87ff18..facfb73283440 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesResult.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListMessagesResult.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.queue.models; @@ -23,20 +23,50 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import com.microsoft.windowsazure.services.blob.implementation.RFC1123DateAdapter; +import com.microsoft.windowsazure.services.queue.QueueContract; +/** + * A wrapper class for the result returned from a Queue Service REST API operation to get a list of messages. This is + * returned by calls to implementations of {@link QueueContract#listMessages(String)} and + * {@link QueueContract#listMessages(String, ListMessagesOptions)}. + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179474.aspx">Get Messages</a> documentation + * on MSDN for details of the underlying Queue Service REST API operation. + */ @XmlRootElement(name = "QueueMessagesList") public class ListMessagesResult { private List<QueueMessage> queueMessages = new ArrayList<QueueMessage>(); + /** + * Gets the list of queue messages returned by a {@link QueueContract}<em>.listMessages</em> request. The queue + * messages returned have their visibility timeout set to allow for processing by the client. The client must + * delete the messages once processing is complete, or they will become visible in the queue when the visibility + * timeout period is over. + * + * @return + * A {@link List} of {@link QueueMessage} instances representing the messages returned by the request. + */ @XmlElement(name = "QueueMessage") public List<QueueMessage> getQueueMessages() { return queueMessages; } + /** + * Reserved for internal use. Sets the list of queue messages returned by a {@link QueueContract} + * <em>.listMessages</em> request. This method is invoked by the API as part of the response generation from the + * Queue Service REST API operation to set the value from the queue message list returned by the server. + * + * @param queueMessages + * A {@link List} of {@link QueueMessage} instances representing the messages returned by the request. + */ public void setQueueMessages(List<QueueMessage> queueMessages) { this.queueMessages = queueMessages; } + /** + * Represents a message in the queue returned by the server. A {@link QueueMessage} instance contains a copy of the + * queue message data in the storage service as of the time the message was requested. + */ public static class QueueMessage { private String messageId; private Date insertionDate; @@ -46,68 +76,168 @@ public static class QueueMessage { private int dequeueCount; private String messageText; + /** + * Gets the message ID for the message in the queue. The message ID is a value that is opaque to the client + * that must be used along with the pop receipt to validate an update message or delete message operation. + * + * @return + * A {@link String} containing the message ID. + */ @XmlElement(name = "MessageId") public String getMessageId() { return messageId; } + /** + * Reserved for internal use. Sets the value of the message ID for the queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the message ID returned by the server. + * + * @param messageId + * A {@link String} containing the message ID. + */ public void setMessageId(String messageId) { this.messageId = messageId; } + /** + * Gets the {@link Date} when this message was added to the queue. + * + * @return + * The {@link Date} when this message was added to the queue. + */ @XmlElement(name = "InsertionTime") @XmlJavaTypeAdapter(RFC1123DateAdapter.class) public Date getInsertionDate() { return insertionDate; } + /** + * Reserved for internal use. Sets the value of the insertion time for the queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the insertion time returned by the server. + * + * @param insertionDate + * The {@link Date} when this message was added to the queue. + */ public void setInsertionDate(Date insertionDate) { this.insertionDate = insertionDate; } + /** + * Gets the {@link Date} when this message will expire and be automatically removed from the queue. + * + * @return + * The {@link Date} when this message will expire. + */ @XmlElement(name = "ExpirationTime") @XmlJavaTypeAdapter(RFC1123DateAdapter.class) public Date getExpirationDate() { return expirationDate; } + /** + * Reserved for internal use. Sets the value of the expiration time for the queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the expiration time returned by the server. + * + * @param expirationDate + * The {@link Date} when this message will expire. + */ public void setExpirationDate(Date expirationDate) { this.expirationDate = expirationDate; } + /** + * Gets the pop receipt value for the queue message. The pop receipt is a value that is opaque to the client + * that must be used along with the message ID to validate an update message or delete message operation. + * + * @return + * A {@link String} containing the pop receipt value for the queue message. + */ @XmlElement(name = "PopReceipt") public String getPopReceipt() { return popReceipt; } + /** + * Reserved for internal use. Sets the value of the pop receipt for the queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the pop receipt returned by the server. + * + * @param popReceipt + * A {@link String} containing the pop receipt value for the queue message. + */ public void setPopReceipt(String popReceipt) { this.popReceipt = popReceipt; } + /** + * Gets the {@link Date} when this message will become visible in the queue. + * + * @return + * The {@link Date} when this message will become visible in the queue. + */ @XmlElement(name = "TimeNextVisible") @XmlJavaTypeAdapter(RFC1123DateAdapter.class) public Date getTimeNextVisible() { return timeNextVisible; } + /** + * Reserved for internal use. Sets the value of the time the message will become visible. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the time next visible returned by the server. + * + * @param timeNextVisible + * The {@link Date} when this message will become visible in the queue. + */ public void setTimeNextVisible(Date timeNextVisible) { this.timeNextVisible = timeNextVisible; } + /** + * Gets the number of times this queue message has been retrieved with a list messages operation. + * + * @return + * The number of times this queue message has been retrieved. + */ @XmlElement(name = "DequeueCount") public int getDequeueCount() { return dequeueCount; } + /** + * Reserved for internal use. Sets the value of the dequeue count of the message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the queue message dequeue count returned by the server. + * + * @param dequeueCount + * The number of times this queue message has been retrieved. + */ public void setDequeueCount(int dequeueCount) { this.dequeueCount = dequeueCount; } + /** + * Gets the {@link String} containing the content of the queue message. + * + * @return + * A {@link String} containing the content of the queue message. + */ @XmlElement(name = "MessageText") public String getMessageText() { return messageText; } + /** + * Reserved for internal use. Sets the {@link String} containing the content of the message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the queue message content returned by the server. + * + * @param messageText + * A {@link String} containing the content of the message. + */ public void setMessageText(String messageText) { this.messageText = messageText; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java index 6b62c7cacd2a7..3384a4367f562 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java @@ -2,61 +2,166 @@ * 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.queue.models; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * Represents the options that may be set on the Queue service for {@link QueueContract#listQueues(ListQueuesOptions)} + * requests. These options include a server response timeout for the request, a prefix to match queue names to return, a + * marker to specify where to resume a list queues query, the maximum number of queues to return in a single response, + * and whether to include queue metadata with the response. + */ public class ListQueuesOptions extends QueueServiceOptions { private String prefix; private String marker; private int maxResults; private boolean includeMetadata; + /** + * Sets the server request timeout value associated with this {@link ListQueuesOptions} instance. + * <p> + * The timeout value only affects calls made on methods where this {@link ListQueuesOptions} instance is passed as a + * parameter. + * + * @param timeout + * The server request timeout value to set in milliseconds. + * @return + * A reference to this {@link ListQueuesOptions} instance. + */ @Override public ListQueuesOptions setTimeout(Integer timeout) { super.setTimeout(timeout); return this; } + /** + * Gets the prefix {@link String} used to match queue names to return in a + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. + * + * @return + * The prefix {@link String} used to match queue names to return in a + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. + */ public String getPrefix() { return prefix; } + /** + * Sets the prefix {@link String} to use to match queue names to return in a + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. + * <p> + * The prefix value only affects calls made on methods where this {@link ListQueuesOptions} instance is passed as a + * parameter. + * + * @param prefix + * The prefix {@link String} to use to match queue names to return in a + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. + * @return + * A reference to this {@link ListQueuesOptions} instance. + */ public ListQueuesOptions setPrefix(String prefix) { this.prefix = prefix; return this; } + /** + * Gets a {@link String} value that identifies the beginning of the list of queues to be returned with a + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. + * <p> + * The {@link QueueContract#listQueues(ListQueuesOptions) listQueues} method returns a <strong>NextMarker</strong> + * element within the response if the list returned was not complete, which can be accessed with the + * {@link ListQueuesResult#getNextMarker()} method. This opaque value may then be set on a {@link ListQueuesOptions} + * instance with a call to {@link ListQueuesOptions#setMarker(String) setMarker} to be used in a subsequent + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} call to request the next portion of the list of + * queues. + * + * @return + * The marker value that identifies the beginning of the list of queues to be returned. + */ public String getMarker() { return marker; } + /** + * Sets a {@link String} marker value that identifies the beginning of the list of queues to be returned with a + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. + * <p> + * The {@link QueueContract#listQueues(ListQueuesOptions) listQueues} method returns a <strong>NextMarker</strong> + * element within the response if the list returned was not complete, which can be accessed with the + * {@link ListQueuesResult#getNextMarker()} method. This opaque value may then be set on a {@link ListQueuesOptions} + * instance with a call to {@link ListQueuesOptions#setMarker(String) setMarker} to be used in a subsequent + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} call to request the next portion of the list of + * queues. + * + * @param marker + * The {@link String} marker value to set. + * @return + * A reference to this {@link ListQueuesOptions} instance. + */ public ListQueuesOptions setMarker(String marker) { this.marker = marker; return this; } + /** + * Gets the maximum number of queues to return with a {@link QueueContract#listQueues(ListQueuesOptions) listQueues} + * request. If the value is not specified, the server will return up to 5,000 items. + * + * @return + * The maximum number of queues to return. + */ public int getMaxResults() { return maxResults; } + /** + * Sets the maximum number of queues to return with a {@link QueueContract#listQueues(ListQueuesOptions) listQueues} + * request. If the value is not specified, by default the server will return up to 5,000 items. + * <p> + * The maxResults value only affects calls made on methods where this {@link ListQueuesOptions} instance is passed + * as a parameter. + * + * @param maxResults + * The maximum number of queues to return. + * @return + * A reference to this {@link ListQueuesOptions} instance. + */ public ListQueuesOptions setMaxResults(int maxResults) { this.maxResults = maxResults; return this; } + /** + * Gets a flag indicating whether to return metadata with a {@link QueueContract#listQueues(ListQueuesOptions) + * listQueues} request. + * + * @return + * {@link true} to return metadata. + */ public boolean isIncludeMetadata() { return includeMetadata; } + /** + * Sets a flag indicating whether to return metadata with a {@link QueueContract#listQueues(ListQueuesOptions) + * listQueues} request. + * + * @param includeMetadata + * {@link true} to return metadata. + * @return + * A reference to this {@link ListQueuesOptions} instance. + */ public ListQueuesOptions setIncludeMetadata(boolean includeMetadata) { this.includeMetadata = includeMetadata; return this; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesResult.java index 6dc3a704a930d..144dddada23c2 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesResult.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesResult.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.queue.models; @@ -25,7 +25,16 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import com.microsoft.windowsazure.services.blob.implementation.MetadataAdapter; +import com.microsoft.windowsazure.services.queue.QueueContract; +/** + * A wrapper class for the results returned in response to Queue service REST API operations to list queues. This + * is returned by calls to implementations of {@link QueueContract#listQueues()} and + * {@link QueueContract#listQueues(ListQueuesOptions)}. + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179466.aspx">List Queues</a> documentation on + * MSDN for details of the underlying Queue service REST API operation. + */ @XmlRootElement(name = "EnumerationResults") public class ListQueuesResult { private List<Queue> queues = new ArrayList<Queue>(); @@ -35,90 +44,248 @@ public class ListQueuesResult { private String nextMarker; private int maxResults; + /** + * Gets the list of queues returned by a {@link QueueContract}<em>.listQueues</em> request. + * + * @return + * A {@link List} of {@link Queue} instances representing the queues returned by the request. + */ @XmlElementWrapper(name = "Queues") @XmlElement(name = "Queue") public List<Queue> getQueues() { return queues; } + /** + * Reserved for internal use. Sets the list of queues returned by a {@link QueueContract}<em>.listQueues</em> + * request. This method is invoked by the API as part of the response generation from the Queue service REST API + * operation to set the value from the queue list returned by the server. + * + * @param value + * A {@link List} of {@link Queue} instances representing the queues returned by the request. + */ public void setQueues(List<Queue> value) { this.queues = value; } + /** + * Gets the base URI for Queue service REST API operations on the storage account. The URI consists of the protocol + * along with the DNS prefix name for the account followed by ".queue.core.windows.net". For example, if the DNS + * prefix name for the storage account is "myaccount" then the value returned by this method is + * "http://myaccount.queue.core.windows.net". + * + * @return + * A {@link String} containing the base URI for Queue service REST API operations on the storage account. + */ @XmlAttribute(name = "AccountName") public String getAccountName() { return accountName; } + /** + * Reserved for internal use. Sets the base URI for Queue service REST API operations on the storage account. This + * method is invoked by the API as part of the response generation from the Queue service REST API operation to set + * the value from the response returned by the server. + * + * @param accountName + * A {@link String} containing the base URI for Queue service REST API operations on the storage account. + */ public void setAccountName(String accountName) { this.accountName = accountName; } + /** + * Gets the prefix {@link String} used to qualify the results returned by the + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. Only queues with names that start with + * the prefix are returned by the request. By default, the prefix is empty and all queues are returned. + * + * @return + * The {@link String} prefix used to qualify the names of the queues returned. + */ @XmlElement(name = "Prefix") public String getPrefix() { return prefix; } + /** + * Reserved for internal use. Sets the prefix {@link String} used to qualify the results returned by the Queue + * service REST API list queues operation invoked with a call to {@link QueueContract#listQueues(ListQueuesOptions) + * listQueues}. This method is invoked by the API as part of the response generation from the Queue service REST API + * operation to set the value from the <strong>Prefix</strong> element returned by the server. + * + * @param prefix + * The {@link String} prefix used to qualify the names of the queues returned. + */ public void setPrefix(String prefix) { this.prefix = prefix; } + /** + * Gets the marker value for the beginning of the queue results returned by the + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. The marker is used by the server to + * specify the place to resume a query for queues. The marker value is a {@link String} opaque to the client. A + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request response may include a + * <strong>NextMarker</strong> value if there are more queue results than can be returned in a single response. Call + * the {@link ListQueuesResult#getNextMarker() getNextMarker} method to get this value. The + * client can request the next set of queue results by setting the marker to this value in the + * {@link ListQueuesOptions} parameter. By default, this value is empty and the server responds with the first + * queues that match the request. + * + * @return + * A {@link String} containing the marker value used for the response. + */ @XmlElement(name = "Marker") public String getMarker() { return marker; } + /** + * Reserved for internal use. Sets the marker value specifying the beginning of the results returned by the Queue + * service REST API list queues operation invoked with a call to {@link QueueContract#listQueues(ListQueuesOptions) + * listQueues}. This method is invoked by the API as part of the response generation from the Queue service REST API + * operation to set the value from the <strong>Marker</strong> element returned by the server. + * + * @param marker + * A {@link String} containing the marker value used for the response. + */ public void setMarker(String marker) { this.marker = marker; } + /** + * Gets the next marker value needed to retrieve additional queues. If more queues are available that satisfy a + * <em>listQueues</em> request than can be returned in the response, the server generates a marker value to specify + * the beginning of the queues to return in a subsequent request. The client can request the next set of queue + * results by setting the marker to this value in the {@link ListQueuesOptions} parameter. This value is + * empty if there are no more queues that satisfy the request than are included in the response. + * + * @return + * A {@link String} containing the marker value to use to resume the list queues request. + */ @XmlElement(name = "NextMarker") public String getNextMarker() { return nextMarker; } + /** + * Reserved for internal use. Sets the next marker value specifying the place to resume a list queues query if more + * results are available than have been returned by the Queue service REST API list queues operation response. This + * method is invoked by the API as part of the response generation from the Queue service REST API operation to set + * the value from the <strong>NextMarker</strong> element returned by the server. + * + * @param nextMarker + * A {@link String} containing the marker value to use to resume the list queues request. + */ public void setNextMarker(String nextMarker) { this.nextMarker = nextMarker; } + /** + * Gets the value specified for the number of queue results to return for the + * {@link QueueContract#listQueues(ListQueuesOptions) listQueues} request. The server will not return more than this + * number of queues in the response. If the value is not specified, the server will return up to 5,000 items. + * <p> + * If there are more queues available that match the request than the number returned, the response will include a + * next marker value to specify the beginning of the queues to return in a subsequent request. Call the + * {@link ListQueuesResult#getNextMarker() getNextMarker} method to get this value. The client can request the next + * set of queue results by setting the marker to this value in the {@link ListQueuesOptions} parameter. + * + * @return + * The maximum number of results to return specified by the request. + */ @XmlElement(name = "MaxResults") public int getMaxResults() { return maxResults; } + /** + * Reserved for internal use. Sets the value returned by the Queue service REST API list queues operation response + * for the maximum number of queues to return. This method is invoked by the API as part of the response generation + * from the Queue service REST API operation to set the value from the <strong>MaxResults</strong> element returned + * by the server. + * + * @param maxResults + * The maximum number of results to return specified by the request. + */ public void setMaxResults(int maxResults) { this.maxResults = maxResults; } + /** + * Represents a queue in the storage account returned by the server. A {@link Queue} instance contains a copy of the + * queue name, URI, and metadata in the storage service as of the time the queue was requested. + */ public static class Queue { private String name; private String url; private HashMap<String, String> metadata = new HashMap<String, String>(); + /** + * Gets the name of this queue. + * + * @return + * A {@link String} containing the name of this queue. + */ @XmlElement(name = "Name") public String getName() { return name; } + /** + * Reserved for internal use. Sets the name of this queue. This method is invoked by the API as part of the + * response generation from the Queue service REST API operation to set the value from the + * <strong>Name</strong> element returned by the server. + * + * @param name + * A {@link String} containing the name of this queue. + */ public void setName(String name) { this.name = name; } + /** + * Gets the URI for Queue service REST API operations on this queue. + * + * @return + * A {@link String} containing the URI for Queue service REST API operations on this queue. + */ @XmlElement(name = "Url") public String getUrl() { return url; } + /** + * Reserved for internal use. Sets the URI of this queue. This method is invoked by the API as part of the + * response generation from the Queue service REST API operation to set the value from the + * <strong>Url</strong> element returned by the server. + * + * @param url + * A {@link String} containing the URI for Queue service REST API operations on this queue. + */ public void setUrl(String url) { this.url = url; } + /** + * Gets the metadata collection of key-value {@link String} pairs associated with this queue. + * + * @return + * A {@link java.util.HashMap} of key-value {@link String} pairs containing the queue metadata. + */ @XmlElement(name = "Metadata") @XmlJavaTypeAdapter(MetadataAdapter.class) public HashMap<String, String> getMetadata() { return metadata; } + /** + * Reserved for internal use. Sets the metadata of this queue. This method is invoked by the API as part of the + * response generation from the Queue service REST API operation to set the value from the + * <strong>Metadata</strong> element returned by the server. + * + * @param metadata + * A {@link java.util.HashMap} of key-value {@link String} pairs containing the queue metadata. + */ public void setMetadata(HashMap<String, String> metadata) { this.metadata = metadata; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesOptions.java index 9339f83eb958b..eff229bf1657c 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesOptions.java @@ -2,31 +2,69 @@ * 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.queue.models; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * Represents the options that may be set on a {@link QueueContract#peekMessages(String, PeekMessagesOptions) + * peekMessages} request. These options include a server response timeout for the request and the number of messages to + * peek from the queue. + */ public class PeekMessagesOptions extends QueueServiceOptions { private Integer numberOfMessages; + /** + * Sets the server request timeout value associated with this {@link PeekMessagesOptions} instance. + * <p> + * The timeout value only affects calls made on methods where this {@link PeekMessagesOptions} instance is passed as + * a parameter. + * + * @param timeout + * The server request timeout value to set in milliseconds. + * @return + * A reference to this {@link PeekMessagesOptions} instance. + */ @Override public PeekMessagesOptions setTimeout(Integer timeout) { super.setTimeout(timeout); return this; } + /** + * Gets the number of messages to return in the response to a + * {@link QueueContract#peekMessages(String, PeekMessagesOptions) peekMessages} request specified in this instance. + * + * @return + * The number of messages to return in the response. + */ public Integer getNumberOfMessages() { return numberOfMessages; } + /** + * Sets the number of messages to return in the response to a + * {@link QueueContract#peekMessages(String, PeekMessagesOptions) peekMessages} request. + * <p> + * The <em>numberOfMessages</em> value only affects calls made on methods where this {@link PeekMessagesOptions} + * instance is passed as a parameter. + * + * + * @param numberOfMessages + * The number of messages to return in the response. This value must be in the range from 0 to 32. + * @return + * A reference to this {@link PeekMessagesOptions} instance. + */ public PeekMessagesOptions setNumberOfMessages(Integer numberOfMessages) { this.numberOfMessages = numberOfMessages; return this; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesResult.java index 79990e4391d69..602f2bee8e8c9 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesResult.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/PeekMessagesResult.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.queue.models; @@ -23,20 +23,49 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; import com.microsoft.windowsazure.services.blob.implementation.RFC1123DateAdapter; +import com.microsoft.windowsazure.services.queue.QueueContract; +/** + * A wrapper class for the results returned in response to Queue Service REST API operations to peek messages. This + * is returned by calls to implementations of {@link QueueContract#peekMessages(String)} and + * {@link QueueContract#peekMessages(String, PeekMessagesOptions)}. + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/dd179472.aspx">Peek Messages</a> documentation + * on MSDN for details of the underlying Queue Service REST API operation. + */ @XmlRootElement(name = "QueueMessagesList") public class PeekMessagesResult { private List<QueueMessage> queueMessages = new ArrayList<QueueMessage>(); + /** + * Gets the list of queue messages returned by a {@link QueueContract}<em>.peekMessages</em> request. The queue + * messages returned do not have a visibility timeout set, and they can be retrieved by other clients for + * processing. + * + * @return + * A {@link List} of {@link QueueMessage} instances representing the messages returned by the request. + */ @XmlElement(name = "QueueMessage") public List<QueueMessage> getQueueMessages() { return queueMessages; } + /** + * Reserved for internal use. Sets the list of queue messages returned by a {@link QueueContract} + * <em>.peekMessages</em> request. This method is invoked by the API as part of the response generation from the + * Queue Service REST API operation to set the value from the queue message list returned by the server. + * + * @param queueMessages + * A {@link List} of {@link QueueMessage} instances representing the messages returned by the request. + */ public void setQueueMessages(List<QueueMessage> queueMessages) { this.queueMessages = queueMessages; } + /** + * Represents a message in the queue returned by the server. A {@link QueueMessage} instance contains a copy of the + * queue message data in the storage service as of the time the message was requested. + */ public static class QueueMessage { private String messageId; private Date insertionDate; @@ -44,49 +73,119 @@ public static class QueueMessage { private int dequeueCount; private String messageText; + /** + * Gets the message ID for the message in the queue. * + * + * @return + * A {@link String} containing the message ID. + */ @XmlElement(name = "MessageId") public String getMessageId() { return messageId; } + /** + * Reserved for internal use. Sets the value of the message ID for the queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the message ID returned by the server. + * + * @param messageId + * A {@link String} containing the message ID. + */ public void setMessageId(String messageId) { this.messageId = messageId; } + /** + * Gets the {@link Date} when this message was added to the queue. + * + * @return + * The {@link Date} when this message was added to the queue. + */ @XmlElement(name = "InsertionTime") @XmlJavaTypeAdapter(RFC1123DateAdapter.class) public Date getInsertionDate() { return insertionDate; } + /** + * Reserved for internal use. Sets the value of the insertion time for the queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the insertion time returned by the server. + * + * @param insertionDate + * The {@link Date} when this message was added to the queue. + */ public void setInsertionDate(Date insertionDate) { this.insertionDate = insertionDate; } + /** + * Gets the {@link Date} when this message will expire and be automatically removed from the queue. + * + * @return + * The {@link Date} when this message will expire. + */ @XmlElement(name = "ExpirationTime") @XmlJavaTypeAdapter(RFC1123DateAdapter.class) public Date getExpirationDate() { return expirationDate; } + /** + * Reserved for internal use. Sets the value of the expiration time for the queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the expiration time returned by the server. + * + * @param expirationDate + * The {@link Date} when this message will expire. + */ public void setExpirationDate(Date expirationDate) { this.expirationDate = expirationDate; } + /** + * Gets the number of times this queue message has been retrieved with a list messages operation. + * + * @return + * The number of times this queue message has been retrieved. + */ @XmlElement(name = "DequeueCount") public int getDequeueCount() { return dequeueCount; } + /** + * Reserved for internal use. Sets the value of the dequeue count of the message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the queue message dequeue count returned by the server. + * + * @param dequeueCount + * The number of times this queue message has been retrieved. + */ public void setDequeueCount(int dequeueCount) { this.dequeueCount = dequeueCount; } + /** + * Gets the {@link String} containing the content of the queue message. + * + * @return + * A {@link String} containing the content of the queue message. + */ @XmlElement(name = "MessageText") public String getMessageText() { return messageText; } + /** + * Reserved for internal use. Sets the {@link String} containing the content of the message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the queue message content returned by the server. + * + * @param messageText + * A {@link String} containing the content of the message. + */ public void setMessageText(String messageText) { this.messageText = messageText; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/QueueServiceOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/QueueServiceOptions.java index ff574f82e1790..9f22a1f5fa1bc 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/QueueServiceOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/QueueServiceOptions.java @@ -2,26 +2,52 @@ * 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.queue.models; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * Represents the base class for options that may be set on Queue Service REST API operations invoked through the + * {@link QueueContract} interface. This class defines a server request timeout, which can be applied to all operations. + */ public class QueueServiceOptions { // Nullable because it is optional private Integer timeout; + /** + * Gets the current server request timeout value associated with this {@link QueueServiceOptions} instance. + * <p> + * The timeout value only affects calls made on methods where this {@link QueueServiceOptions} instance is passed as + * a parameter. + * + * @return + * The server request timeout value in milliseconds. + */ public Integer getTimeout() { return timeout; } + /** + * Sets the server request timeout value associated with this {@link QueueServiceOptions} instance. + * <p> + * The timeout value only affects calls made on methods where this {@link QueueServiceOptions} instance is passed as + * a parameter. + * + * @param timeout + * The server request timeout value to set in milliseconds. + * @return + * A reference to this {@link QueueServiceOptions} instance. + */ public QueueServiceOptions setTimeout(Integer timeout) { this.timeout = timeout; return this; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java index f676d600b63b7..11192fa3bac7a 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java @@ -2,44 +2,106 @@ * 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.queue.models; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlRootElement; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * A wrapper class for the Queue service properties set or retrieved with Queue Service REST API operations. This + * is returned by calls to implementations of {@link QueueContract#getServiceProperties()} and + * {@link QueueContract#getServiceProperties(QueueServiceOptions)} and passed to the server with calls to + * {@link QueueContract#setServiceProperties(ServiceProperties)} and + * {@link QueueContract#setServiceProperties(ServiceProperties, QueueServiceOptions)}. + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/hh452243.aspx">Get Queue Service Properties</a> + * and <a href="http://msdn.microsoft.com/en-us/library/windowsazure/hh452232.aspx">Set Queue Service Properties</a> + * documentation on MSDN for details of the underlying Queue Service REST API operations. See the <a + * href="http://msdn.microsoft.com/en-us/library/windowsazure/hh343268.aspx">Storage Analytics Overview</a> + * documentation on MSDN for more information about logging and metrics. + */ @XmlRootElement(name = "StorageServiceProperties") public class ServiceProperties { private Logging logging = new Logging(); private Metrics metrics = new Metrics(); + /** + * Gets a reference to the {@link Logging} instance in this {@link ServiceProperties} instance. + * <p> + * This {@link ServiceProperties} instance holds a local copy of the Queue service properties when returned by a + * call to {@link QueueContract}<em>.getServiceProperties</em>. + * <p> + * Note that changes to this value are not reflected in the Queue service properties until they have been set on the + * storage account with a call to {@link QueueContract}<em>.setServiceProperties</em>. + * + * @return + * A reference to the {@link Logging} instance in this {@link ServiceProperties} instance. + */ @XmlElement(name = "Logging") public Logging getLogging() { return logging; } + /** + * Sets the {@link Logging} instance in this {@link ServiceProperties} instance. + * <p> + * Note that changes to this value are not reflected in the Queue service properties until they have been set on the + * storage account with a call to {@link QueueContract}<em>.setServiceProperties</em>. + * + * @param logging + * The {@link Logging} instance to set in this {@link ServiceProperties} instance. + */ public void setLogging(Logging logging) { this.logging = logging; } + /** + * Gets a reference to the {@link Metrics} instance in this {@link ServiceProperties} instance. + * <p> + * This {@link ServiceProperties} instance holds a local copy of the Queue service properties when returned by a + * call to {@link QueueContract}<em>.getServiceProperties</em>. + * <p> + * Note that changes to this value are not reflected in the Queue service properties until they have been set on the + * storage account with a call to {@link QueueContract}<em>.setServiceProperties</em>. + * + * @return + * A reference to the {@link Metrics} instance in this {@link ServiceProperties} instance. + */ @XmlElement(name = "Metrics") public Metrics getMetrics() { return metrics; } + /** + * Sets the {@link Metrics} instance in this {@link ServiceProperties} instance. + * <p> + * Note that changes to this value are not reflected in the Queue service properties until they have been set on the + * storage account with a call to {@link QueueContract}<em>.setServiceProperties</em>. + * + * @param metrics + * The {@link Metrics} instance to set in this {@link ServiceProperties} instance. + */ public void setMetrics(Metrics metrics) { this.metrics = metrics; } + /** + * This inner class represents the settings for logging on the Queue service of the storage account. These settings + * include the Storage Analytics version, whether to log delete requests, read requests, or write requests, and a + * {@link RetentionPolicy} instance for retention policy settings. + */ public static class Logging { private String version; private Boolean delete; @@ -47,113 +109,281 @@ public static class Logging { private Boolean write; private RetentionPolicy retentionPolicy; + /** + * Gets a reference to the {@link RetentionPolicy} instance in this {@link Logging} instance. + * + * @return + * A reference to the {@link RetentionPolicy} instance in this {@link Logging} instance. + */ @XmlElement(name = "RetentionPolicy") public RetentionPolicy getRetentionPolicy() { return retentionPolicy; } + /** + * Sets the {@link RetentionPolicy} instance in this {@link Logging} instance. + * + * @param retentionPolicy + * The {@link RetentionPolicy} instance to set in this {@link Logging} instance. + */ public void setRetentionPolicy(RetentionPolicy retentionPolicy) { this.retentionPolicy = retentionPolicy; } + /** + * Gets a flag indicating whether queue write operations are logged. If this value is {@link true} then all + * requests that write to the Queue service will be logged. These requests include adding a message, updating a + * message, setting queue metadata, and creating a queue. + * + * @return + * {@link true} if queue write operations are logged, otherwise {@link false}. + */ @XmlElement(name = "Write") public boolean isWrite() { return write; } + /** + * Sets a flag indicating whether queue write operations are logged. If this value is {@link true} then all + * requests that write to the Queue service will be logged. These requests include adding a message, updating a + * message, setting queue metadata, and creating a queue. + * + * @param write + * {@link true} to enable logging of queue write operations, otherwise {@link false}. + */ public void setWrite(boolean write) { this.write = write; } + /** + * Gets a flag indicating whether queue read operations are logged. If this value is {@link true} then all + * requests that read from the Queue service will be logged. These requests include listing queues, getting + * queue metadata, listing messages, and peeking messages. + * + * @return + * {@link true} if queue read operations are logged, otherwise {@link false}. + */ @XmlElement(name = "Read") public boolean isRead() { return read; } + /** + * Sets a flag indicating whether queue read operations are logged. If this value is {@link true} then all + * requests that read from the Queue service will be logged. These requests include listing queues, getting + * queue metadata, listing messages, and peeking messages. + * + * @param read + * {@link true} to enable logging of queue read operations, otherwise {@link false}. + */ public void setRead(boolean read) { this.read = read; } + /** + * Gets a flag indicating whether queue delete operations are logged. If this value is {@link true} then all + * requests that delete from the Queue service will be logged. These requests include deleting queues, deleting + * messages, and clearing messages. + * + * @return + * {@link true} if queue delete operations are logged, otherwise {@link false}. + */ @XmlElement(name = "Delete") public boolean isDelete() { return delete; } + /** + * Sets a flag indicating whether queue delete operations are logged. If this value is {@link true} then all + * requests that delete from the Queue service will be logged. These requests include deleting queues, deleting + * messages, and clearing messages. + * + * @param delete + * {@link true} to enable logging of queue delete operations, otherwise {@link false}. + */ public void setDelete(boolean delete) { this.delete = delete; } + /** + * Gets the Storage Analytics version number associated with this {@link Logging} instance. + * + * @return + * A {@link String} containing the Storage Analytics version number. + */ @XmlElement(name = "Version") public String getVersion() { return version; } + /** + * Sets the Storage Analytics version number to associate with this {@link Logging} instance. The current + * supported + * version number is "1.0". + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/hh343268.aspx">Storage Analytics + * Overview</a> documentation on MSDN for more information. + * + * @param version + * A {@link String} containing the Storage Analytics version number to set. + */ public void setVersion(String version) { this.version = version; } } + /** + * This inner class represents the settings for metrics on the Queue service of the storage account. These settings + * include the Storage Analytics version, whether metrics are enabled, whether to include API operation summary + * statistics, and a {@link RetentionPolicy} instance for retention policy settings. + */ public static class Metrics { private String version; private boolean enabled; private Boolean includeAPIs; private RetentionPolicy retentionPolicy; + /** + * Gets a reference to the {@link RetentionPolicy} instance in this {@link Metrics} instance. + * + * @return + * A reference to the {@link RetentionPolicy} instance in this {@link Metrics} instance. + */ @XmlElement(name = "RetentionPolicy") public RetentionPolicy getRetentionPolicy() { return retentionPolicy; } + /** + * Sets the {@link RetentionPolicy} instance in this {@link Metrics} instance. + * + * @param retentionPolicy + * The {@link RetentionPolicy} instance to set in this {@link Metrics} instance. + */ public void setRetentionPolicy(RetentionPolicy retentionPolicy) { this.retentionPolicy = retentionPolicy; } + /** + * Gets a flag indicating whether metrics should generate summary statistics for called API operations. If this + * value is {@link true} then all Queue service REST API operations will be included in the metrics. + * + * @return + * {@link true} if Queue service REST API operations are included in metrics, otherwise {@link false}. + */ @XmlElement(name = "IncludeAPIs") public Boolean isIncludeAPIs() { return includeAPIs; } + /** + * Sets a flag indicating whether metrics should generate summary statistics for called API operations. If this + * value is {@link true} then all Queue service REST API operations will be included in the metrics. + * + * @param includeAPIs + * {@link true} to include Queue service REST API operations in metrics, otherwise {@link false}. + */ public void setIncludeAPIs(Boolean includeAPIs) { this.includeAPIs = includeAPIs; } + /** + * Gets a flag indicating whether metrics is enabled for the Queue storage service. + * + * @return + * A flag indicating whether metrics is enabled for the Queue storage service. + */ @XmlElement(name = "Enabled") public boolean isEnabled() { return enabled; } + /** + * Sets a flag indicating whether to enable metrics for the Queue storage service. + * + * @param enabled + * {@link true} to enable metrics for the Queue storage service, otherwise {@link false}. + */ public void setEnabled(boolean enabled) { this.enabled = enabled; } + /** + * Gets the Storage Analytics version number associated with this {@link Metrics} instance. + * + * @return + * A {@link String} containing the Storage Analytics version number. + */ @XmlElement(name = "Version") public String getVersion() { return version; } + /** + * Sets the Storage Analytics version number to associate with this {@link Metrics} instance. The current + * supported + * version number is "1.0". + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/hh343268.aspx">Storage Analytics + * Overview</a> documentation on MSDN for more information. + * + * @param version + * A {@link String} containing the Storage Analytics version number to set. + */ public void setVersion(String version) { this.version = version; } } + /** + * This inner class represents the retention policy settings for logging or metrics on the Queue service of the + * storage account. These settings include whether a retention policy is enabled for the data, and the number of + * days that metrics or logging data should be retained. + */ public static class RetentionPolicy { private boolean enabled; private Integer days; // nullable, because optional if "enabled" is false + /** + * Gets the number of days that metrics or logging data should be retained. All data older than this value will + * be deleted. The value may be null if a retention policy is not enabled. + * + * @return + */ @XmlElement(name = "Days") public Integer getDays() { return days; } + /** + * Sets the number of days that metrics or logging data should be retained. All data older than this value will + * be deleted. The value must be in the range from 1 to 365. This value must be set if a retention policy is + * enabled, but is not required if a retention policy is not enabled. + * + * @param days + * The number of days that metrics or logging data should be retained. + */ public void setDays(Integer days) { this.days = days; } + /** + * Gets a flag indicating whether a retention policy is enabled for the storage service. + * + * @return + * {@link true} if data retention is enabled, otherwise {@link false}. + */ @XmlElement(name = "Enabled") public boolean isEnabled() { return enabled; } + /** + * Sets a flag indicating whether a retention policy is enabled for the storage service. + * + * @param enabled + * Set {@link true} to enable data retention. + */ public void setEnabled(boolean enabled) { this.enabled = enabled; } diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/UpdateMessageResult.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/UpdateMessageResult.java index a57f8be013ffe..7c81179931e43 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/UpdateMessageResult.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/UpdateMessageResult.java @@ -2,36 +2,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 + * 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.queue.models; import java.util.Date; +import com.microsoft.windowsazure.services.queue.QueueContract; + +/** + * A wrapper class for the results returned in response to Queue Service REST API operations to update a message. This + * is returned by calls to implementations of {@link QueueContract#updateMessage(String, String, String, String, int)} + * and {@link QueueContract#updateMessage(String, String, String, String, int, QueueServiceOptions)}. + * <p> + * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/hh452234.aspx">Update Message</a> documentation + * on MSDN for details of the underlying Queue Service REST API operation. + */ public class UpdateMessageResult { private String popReceipt; private Date timeNextVisible; + /** + * Gets the pop receipt value for the updated queue message. The pop receipt is a value that is opaque to the client + * that must be used along with the message ID to validate an update message or delete message operation. + * + * @return + * A {@link String} containing the pop receipt value for the queue message. + */ public String getPopReceipt() { return popReceipt; } + /** + * Reserved for internal use. Sets the value of the pop receipt for the updated queue message. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the pop receipt returned by the server. + * + * @param popReceipt + * A {@link String} containing the pop receipt value for the queue message. + */ public void setPopReceipt(String popReceipt) { this.popReceipt = popReceipt; } + /** + * Gets the {@link Date} when the updated message will become visible in the queue. + * + * @return + * The {@link Date} when the updated message will become visible in the queue. + */ public Date getTimeNextVisible() { return timeNextVisible; } + /** + * Reserved for internal use. Sets the value of the time the updated message will become visible. This method is + * invoked by the API as part of the response generation from the Queue Service REST API operation to set the + * value with the time next visible returned by the server. + * + * @param timeNextVisible + * The {@link Date} when the updated message will become visible in the queue. + */ public void setTimeNextVisible(Date timeNextVisible) { this.timeNextVisible = timeNextVisible; } From 88d99b5f6ed6bc1b389b54dac8226d6e804b7804 Mon Sep 17 00:00:00 2001 From: Colin Robertson <a-colinr@microsoft.com> Date: Mon, 19 Mar 2012 15:09:04 -0700 Subject: [PATCH 42/63] add package.html files for javadocs --- .../windowsazure/services/queue/models/package.html | 5 +++++ .../com/microsoft/windowsazure/services/queue/package.html | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/package.html create mode 100644 microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/package.html diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/package.html b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/package.html new file mode 100644 index 0000000000000..6d2f45a3fae7d --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/package.html @@ -0,0 +1,5 @@ +<html> +<head /> +<body>This package contains the queue data transfer object classes. +</body> +</html> diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/package.html b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/package.html new file mode 100644 index 0000000000000..4847f5461cb62 --- /dev/null +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/package.html @@ -0,0 +1,5 @@ +<html> +<head /> +<body>This package contains the queue service class, interface, and associated configuration and utility classes. +</body> +</html> From 2877e82848b1d486fb40c86d0f1294149ee1d142 Mon Sep 17 00:00:00 2001 From: Colin Robertson <a-colinr@microsoft.com> Date: Thu, 22 Mar 2012 16:12:49 -0700 Subject: [PATCH 43/63] Fix Javadoc build warnings. --- .../services/queue/QueueContract.java | 1 + .../queue/models/CreateMessageOptions.java | 1 + .../queue/models/ListQueuesOptions.java | 4 +- .../queue/models/ServiceProperties.java | 52 ++++++++++--------- 4 files changed, 31 insertions(+), 27 deletions(-) diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.java index 7abab4aebde5d..aa86eaf4ffcdc 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/QueueContract.java @@ -16,6 +16,7 @@ import java.util.HashMap; +import com.microsoft.windowsazure.services.core.Configuration; import com.microsoft.windowsazure.services.core.FilterableService; import com.microsoft.windowsazure.services.core.ServiceException; import com.microsoft.windowsazure.services.queue.models.CreateMessageOptions; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java index f318e7700b26c..1ce3ba2a191b9 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/CreateMessageOptions.java @@ -100,6 +100,7 @@ public Integer getTimeToLiveInSeconds() { * @param timeToLiveInSeconds * The maximum time to allow the message to be in the queue, in seconds. * @return + * A reference to this {@link CreateMessageOptions} instance. */ public CreateMessageOptions setTimeToLiveInSeconds(Integer timeToLiveInSeconds) { this.timeToLiveInSeconds = timeToLiveInSeconds; diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java index 3384a4367f562..7d4d346570b82 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ListQueuesOptions.java @@ -147,7 +147,7 @@ public ListQueuesOptions setMaxResults(int maxResults) { * listQueues} request. * * @return - * {@link true} to return metadata. + * <code>true</code> to return metadata. */ public boolean isIncludeMetadata() { return includeMetadata; @@ -158,7 +158,7 @@ public boolean isIncludeMetadata() { * listQueues} request. * * @param includeMetadata - * {@link true} to return metadata. + * <code>true</code> to return metadata. * @return * A reference to this {@link ListQueuesOptions} instance. */ diff --git a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java index 11192fa3bac7a..9f2715a7fe0d6 100644 --- a/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java +++ b/microsoft-azure-api/src/main/java/com/microsoft/windowsazure/services/queue/models/ServiceProperties.java @@ -131,12 +131,12 @@ public void setRetentionPolicy(RetentionPolicy retentionPolicy) { } /** - * Gets a flag indicating whether queue write operations are logged. If this value is {@link true} then all + * Gets a flag indicating whether queue write operations are logged. If this value is <code>true</code> then all * requests that write to the Queue service will be logged. These requests include adding a message, updating a * message, setting queue metadata, and creating a queue. * * @return - * {@link true} if queue write operations are logged, otherwise {@link false}. + * <code>true</code> if queue write operations are logged, otherwise <code>false</code>. */ @XmlElement(name = "Write") public boolean isWrite() { @@ -144,24 +144,24 @@ public boolean isWrite() { } /** - * Sets a flag indicating whether queue write operations are logged. If this value is {@link true} then all + * Sets a flag indicating whether queue write operations are logged. If this value is <code>true</code> then all * requests that write to the Queue service will be logged. These requests include adding a message, updating a * message, setting queue metadata, and creating a queue. * * @param write - * {@link true} to enable logging of queue write operations, otherwise {@link false}. + * <code>true</code> to enable logging of queue write operations, otherwise <code>false</code>. */ public void setWrite(boolean write) { this.write = write; } /** - * Gets a flag indicating whether queue read operations are logged. If this value is {@link true} then all + * Gets a flag indicating whether queue read operations are logged. If this value is <code>true</code> then all * requests that read from the Queue service will be logged. These requests include listing queues, getting * queue metadata, listing messages, and peeking messages. * * @return - * {@link true} if queue read operations are logged, otherwise {@link false}. + * <code>true</code> if queue read operations are logged, otherwise <code>false</code>. */ @XmlElement(name = "Read") public boolean isRead() { @@ -169,24 +169,24 @@ public boolean isRead() { } /** - * Sets a flag indicating whether queue read operations are logged. If this value is {@link true} then all + * Sets a flag indicating whether queue read operations are logged. If this value is <code>true</code> then all * requests that read from the Queue service will be logged. These requests include listing queues, getting * queue metadata, listing messages, and peeking messages. * * @param read - * {@link true} to enable logging of queue read operations, otherwise {@link false}. + * <code>true</code> to enable logging of queue read operations, otherwise <code>false</code>. */ public void setRead(boolean read) { this.read = read; } /** - * Gets a flag indicating whether queue delete operations are logged. If this value is {@link true} then all - * requests that delete from the Queue service will be logged. These requests include deleting queues, deleting - * messages, and clearing messages. + * Gets a flag indicating whether queue delete operations are logged. If this value is <code>true</code> then + * all requests that delete from the Queue service will be logged. These requests include deleting queues, + * deleting messages, and clearing messages. * * @return - * {@link true} if queue delete operations are logged, otherwise {@link false}. + * <code>true</code> if queue delete operations are logged, otherwise <code>false</code>. */ @XmlElement(name = "Delete") public boolean isDelete() { @@ -194,12 +194,12 @@ public boolean isDelete() { } /** - * Sets a flag indicating whether queue delete operations are logged. If this value is {@link true} then all - * requests that delete from the Queue service will be logged. These requests include deleting queues, deleting - * messages, and clearing messages. + * Sets a flag indicating whether queue delete operations are logged. If this value is <code>true</code> then + * all requests that delete from the Queue service will be logged. These requests include deleting queues, + * deleting messages, and clearing messages. * * @param delete - * {@link true} to enable logging of queue delete operations, otherwise {@link false}. + * <code>true</code> to enable logging of queue delete operations, otherwise <code>false</code>. */ public void setDelete(boolean delete) { this.delete = delete; @@ -266,10 +266,11 @@ public void setRetentionPolicy(RetentionPolicy retentionPolicy) { /** * Gets a flag indicating whether metrics should generate summary statistics for called API operations. If this - * value is {@link true} then all Queue service REST API operations will be included in the metrics. + * value is <code>true</code> then all Queue service REST API operations will be included in the metrics. * * @return - * {@link true} if Queue service REST API operations are included in metrics, otherwise {@link false}. + * <code>true</code> if Queue service REST API operations are included in metrics, otherwise + * <code>false</code>. */ @XmlElement(name = "IncludeAPIs") public Boolean isIncludeAPIs() { @@ -278,10 +279,11 @@ public Boolean isIncludeAPIs() { /** * Sets a flag indicating whether metrics should generate summary statistics for called API operations. If this - * value is {@link true} then all Queue service REST API operations will be included in the metrics. + * value is <code>true</code> then all Queue service REST API operations will be included in the metrics. * * @param includeAPIs - * {@link true} to include Queue service REST API operations in metrics, otherwise {@link false}. + * <code>true</code> to include Queue service REST API operations in metrics, otherwise + * <code>false</code>. */ public void setIncludeAPIs(Boolean includeAPIs) { this.includeAPIs = includeAPIs; @@ -302,7 +304,7 @@ public boolean isEnabled() { * Sets a flag indicating whether to enable metrics for the Queue storage service. * * @param enabled - * {@link true} to enable metrics for the Queue storage service, otherwise {@link false}. + * <code>true</code> to enable metrics for the Queue storage service, otherwise <code>false</code>. */ public void setEnabled(boolean enabled) { this.enabled = enabled; @@ -321,8 +323,7 @@ public String getVersion() { /** * Sets the Storage Analytics version number to associate with this {@link Metrics} instance. The current - * supported - * version number is "1.0". + * supported version number is "1.0". * <p> * See the <a href="http://msdn.microsoft.com/en-us/library/windowsazure/hh343268.aspx">Storage Analytics * Overview</a> documentation on MSDN for more information. @@ -349,6 +350,7 @@ public static class RetentionPolicy { * be deleted. The value may be null if a retention policy is not enabled. * * @return + * The number of days that metrics or logging data should be retained. */ @XmlElement(name = "Days") public Integer getDays() { @@ -371,7 +373,7 @@ public void setDays(Integer days) { * Gets a flag indicating whether a retention policy is enabled for the storage service. * * @return - * {@link true} if data retention is enabled, otherwise {@link false}. + * <code>true</code> if data retention is enabled, otherwise <code>false</code>. */ @XmlElement(name = "Enabled") public boolean isEnabled() { @@ -382,7 +384,7 @@ public boolean isEnabled() { * Sets a flag indicating whether a retention policy is enabled for the storage service. * * @param enabled - * Set {@link true} to enable data retention. + * Set <code>true</code> to enable data retention. */ public void setEnabled(boolean enabled) { this.enabled = enabled; 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 44/63] 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 906c4bdc4a757..525c2adf086ae 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 45/63] 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 4a4146a5055db..5f14d2d82a1f7 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 46/63] 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 c65db1d6bbf06..5b7875c0ab983 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 4a4146a5055db..d5d2ceafc8dec 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 47/63] 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 af860e50936dd..f1ea12dbaa76f 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 9fa415c44c8f1..41bb096c7daa0 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 a777b001c5841..c69d18d8f1ebb 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 48/63] 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 41bb096c7daa0..34444b858a039 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 49/63] 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 e9a5dbc846493..10c343b87bb94 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 4a4146a5055db..7a7de013076c9 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 50/63] 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 f7aae2cd9188f..234581b023fb8 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 bfeddfdb5c807..54b58bd8f20a6 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 51/63] 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 7a7de013076c9..aca02d15bc3b8 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 52/63] 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 54b58bd8f20a6..0e44678565088 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 53/63] 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 daf68d549f11b..20ffb6596727f 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 54/63] 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 a0120eb419846..347538853151a 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 013c109da8edd..d0a4e23f6b6d8 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 55/63] 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 20ffb6596727f..88a4e0c1c130c 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 56/63] Fix minor spacing issues --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6390ff1f65f7b..76a899e45b209 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 57/63] 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 5bba95ef4599c..861cd21f3672f 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 a36de8dc02476..21b95538f6e11 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 58/63] 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 6988596979495..d35293670c1b8 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 59/63] 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 88a4e0c1c130c..55fc4c14fb3a0 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 60/63] 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 604f51149d596..ce6c5e34a1fce 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 36b975d7a6900..f115f9ed52d8f 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 61/63] 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 ce6c5e34a1fce..7d74bbecba2aa 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 2cdb8473a26d3..7666da869e056 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 098b28ae0e397..aa3dd041be7cc 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 31a6daf581b22..86dfafb309f6e 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 12aaedf407d4e..49cb129d86e03 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 cf3997dd7225c..0000000000000 --- 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 31c7d05652bc7..e5d7a7c511513 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 08a62dbf957b5..77341e2477c57 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 f115f9ed52d8f..d72c580a402f4 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 f6da13830a0cb..99aa90409830b 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 20ffb6596727f..cb298162dc1d8 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 62/63] 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 86dfafb309f6e..1e410d2adfb46 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 cb298162dc1d8..9301537ca18f0 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 63/63] 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 c83d5dcc1a333..01567851ee74f 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>