From 18faff54b3e45e075b3c7b83b7efe51e95092a52 Mon Sep 17 00:00:00 2001 From: Adam Lowe Date: Fri, 8 Jun 2012 11:23:41 +0100 Subject: [PATCH] openstack-keystone: adding TypeAdaptorFactory to take care of unwrapping objects containing an array of "values" in place of a normal json array --- .../v2_0/config/KeystoneParserModule.java | 83 +++++++++++++++++++ .../keystone/v2_0/domain/ApiMetadata.java | 28 ++----- .../v2_0/features/TenantClientExpectTest.java | 17 ++++ .../parse/ParseDevstackApiMetadataTest.java | 73 ++++++++++++++++ .../parse/ParseRackspaceApiMetadataTest.java | 12 +++ .../src/test/resources/devstackVersion.json | 1 + .../src/test/resources/tenant_list_att.json | 1 + .../org/jclouds/json/config/GsonModule.java | 17 ++++ 8 files changed, 210 insertions(+), 22 deletions(-) create mode 100644 apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseDevstackApiMetadataTest.java create mode 100644 apis/openstack-keystone/src/test/resources/devstackVersion.json create mode 100644 apis/openstack-keystone/src/test/resources/tenant_list_att.json diff --git a/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/config/KeystoneParserModule.java b/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/config/KeystoneParserModule.java index c808961900..51e2927073 100644 --- a/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/config/KeystoneParserModule.java +++ b/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/config/KeystoneParserModule.java @@ -18,10 +18,28 @@ */ package org.jclouds.openstack.keystone.v2_0.config; +import static com.google.common.base.Preconditions.checkState; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Set; + import org.jclouds.json.config.GsonModule; import org.jclouds.json.config.GsonModule.DateAdapter; +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.gson.Gson; +import com.google.gson.TypeAdapter; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; import com.google.inject.AbstractModule; +import com.google.inject.TypeLiteral; /** * @author Adam Lowe @@ -32,5 +50,70 @@ public class KeystoneParserModule extends AbstractModule { @Override protected void configure() { bind(DateAdapter.class).to(GsonModule.Iso8601DateAdapter.class); + bind(new TypeLiteral>() { + }).toInstance(ImmutableSet.of(new SetTypeAdapterFactory())); + } + + /** + * Handles the goofy structures with "values" holder wrapping an array + * http://docs.openstack.org/api/openstack-identity-service/2.0/content/Versions-d1e472.html + *

+ * Treats [A,B,C] and {"values"=[A,B,C], "someotherstuff"=...} as the same Set + */ + public static class SetTypeAdapterFactory implements TypeAdapterFactory { + public TypeAdapter create(Gson gson, TypeToken typeToken) { + Type type = typeToken.getType(); + if (typeToken.getRawType() != Set.class || !(type instanceof ParameterizedType)) { + return null; + } + + Type elementType = ((ParameterizedType) type).getActualTypeArguments()[0]; + TypeAdapter elementAdapter = gson.getAdapter(TypeToken.get(elementType)); + return (TypeAdapter) newSetAdapter(elementAdapter); + } + + private TypeAdapter> newSetAdapter(final TypeAdapter elementAdapter) { + return new TypeAdapter>() { + public void write(JsonWriter out, Set value) throws IOException { + out.beginArray(); + for (E element : value) { + elementAdapter.write(out, element); + } + out.endArray(); + } + + public Set read(JsonReader in) throws IOException { + Set result = Sets.newLinkedHashSet(); + if (in.peek() == JsonToken.BEGIN_OBJECT) { + boolean foundValues = false; + in.beginObject(); + while (in.hasNext()) { + String name = in.nextName(); + if (Objects.equal("values", name)) { + foundValues = true; + readArray(in, result); + } else { + in.skipValue(); + } + } + checkState(foundValues, "Expected BEGIN_ARRAY or the object to contain an array called 'values'"); + in.endObject(); + } else { + readArray(in, result); + } + + return result; + } + + private void readArray(JsonReader in, Set result) throws IOException { + in.beginArray(); + while (in.hasNext()) { + E element = elementAdapter.read(in); + result.add(element); + } + in.endArray(); + } + }.nullSafe(); + } } } diff --git a/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/domain/ApiMetadata.java b/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/domain/ApiMetadata.java index 14e5552c67..24ce002025 100644 --- a/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/domain/ApiMetadata.java +++ b/apis/openstack-keystone/src/main/java/org/jclouds/openstack/keystone/v2_0/domain/ApiMetadata.java @@ -106,30 +106,14 @@ public class ApiMetadata extends Resource { @Nullable private Date updated; - - // dealing with the goofy structure with "values" holder noted here - // http://docs.openstack.org/api/openstack-identity-service/2.0/content/Versions-d1e472.html - // if they change this to not be a value holder, we'll probably need to write a custom - // deserializer. - private static class MediaTypesHolder { - private Set values = ImmutableSet.of(); - - private MediaTypesHolder() { - } - - private MediaTypesHolder(Set mediaTypes) { - this.values = ImmutableSet.copyOf(checkNotNull(mediaTypes, "mediaTypes")); - } - } - @SerializedName(value="media-types") - private MediaTypesHolder mediaTypes = new MediaTypesHolder(); + private Set mediaTypes = Sets.newLinkedHashSet(); protected ApiMetadata(Builder builder) { super(builder); this.status = checkNotNull(builder.status, "status"); this.updated = checkNotNull(builder.updated, "updated"); - this.mediaTypes = new MediaTypesHolder(builder.mediaTypes); + this.mediaTypes = ImmutableSet.copyOf(builder.mediaTypes); } /** @@ -147,12 +131,12 @@ public class ApiMetadata extends Resource { /** */ public Set getMediaTypes() { - return Collections.unmodifiableSet(this.mediaTypes.values); + return Collections.unmodifiableSet(this.mediaTypes); } @Override public int hashCode() { - return Objects.hashCode(status, updated, mediaTypes.values); + return Objects.hashCode(status, updated, mediaTypes); } @Override @@ -162,14 +146,14 @@ public class ApiMetadata extends Resource { ApiMetadata that = ApiMetadata.class.cast(obj); return super.equals(that) && Objects.equal(this.status, that.status) && Objects.equal(this.updated, that.updated) - && Objects.equal(this.mediaTypes.values, that.mediaTypes.values); + && Objects.equal(this.mediaTypes, that.mediaTypes); } protected ToStringHelper string() { return super.string() .add("status", status) .add("updated", updated) - .add("mediaTypes", mediaTypes.values); + .add("mediaTypes", mediaTypes); } } \ No newline at end of file diff --git a/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/features/TenantClientExpectTest.java b/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/features/TenantClientExpectTest.java index 8260d9cb39..ca6b898067 100644 --- a/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/features/TenantClientExpectTest.java +++ b/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/features/TenantClientExpectTest.java @@ -65,6 +65,23 @@ public class TenantClientExpectTest extends BaseKeystoneRestClientExpectTest tenants = client.list(); + assertNotNull(tenants); + assertFalse(tenants.isEmpty()); + + Set expected = ImmutableSet.of(Tenant.builder().name("this-is-a-test").id("14").description("None").build()); + + assertEquals(tenants, expected); + } + public void testListTenantsFailNotFound() { TenantClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, responseWithKeystoneAccess, standardRequestBuilder(endpoint + "/v2.0/tenants").build(), standardResponseBuilder(404).build()) diff --git a/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseDevstackApiMetadataTest.java b/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseDevstackApiMetadataTest.java new file mode 100644 index 0000000000..0cc5f23c58 --- /dev/null +++ b/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseDevstackApiMetadataTest.java @@ -0,0 +1,73 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.jclouds.openstack.keystone.v2_0.parse; + +import java.net.URI; + +import javax.ws.rs.Consumes; +import javax.ws.rs.core.MediaType; + +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.json.BaseItemParserTest; +import org.jclouds.json.config.GsonModule; +import org.jclouds.openstack.keystone.v2_0.config.KeystoneParserModule; +import org.jclouds.openstack.keystone.v2_0.domain.ApiMetadata; +import org.jclouds.openstack.v2_0.domain.Link; +import org.jclouds.rest.annotations.SelectJson; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Guice; +import com.google.inject.Injector; + +/** + * @author Adam Lowe + */ +@Test(groups = "unit", testName = "ParseDevstackApiMetadataTest") +public class ParseDevstackApiMetadataTest extends BaseItemParserTest { + + @Override + public String resource() { + return "/devstackVersion.json"; + } + + // http://docs.openstack.org/api/openstack-identity-service/2.0/content/Versions-d1e472.html + @Override + @SelectJson("version") + @Consumes(MediaType.APPLICATION_JSON) + public ApiMetadata expected() { + return ApiMetadata.builder().id("v2.0") + .links(ImmutableSet.of(Link.builder().relation(Link.Relation.SELF).href(URI.create("http://172.16.89.167:5000/v2.0/")).build(), + Link.builder().relation(Link.Relation.DESCRIBEDBY).type("text/html").href(URI.create("http://docs.openstack.org/api/openstack-identity-service/2.0/content/")).build(), + Link.builder().relation(Link.Relation.DESCRIBEDBY).type("application/pdf").href(URI.create("http://docs.openstack.org/api/openstack-identity-service/2.0/identity-dev-guide-2.0.pdf")).build() + )) + .status("beta") + .updated(new SimpleDateFormatDateService().iso8601SecondsDateParse("2011-11-19T00:00:00+00:00")) + .mediaTypes(ImmutableSet.of( + org.jclouds.openstack.keystone.v2_0.domain.MediaType.builder().base("application/json").type("application/vnd.openstack.identity-v2.0+json").build(), + org.jclouds.openstack.keystone.v2_0.domain.MediaType.builder().base("application/xml").type("application/vnd.openstack.identity-v2.0+xml").build() + )) + .build(); + } + + @Override + protected Injector injector() { + return Guice.createInjector(new GsonModule(), new KeystoneParserModule()); + } +} diff --git a/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseRackspaceApiMetadataTest.java b/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseRackspaceApiMetadataTest.java index 23647aee2b..0be8e5c1c2 100644 --- a/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseRackspaceApiMetadataTest.java +++ b/apis/openstack-keystone/src/test/java/org/jclouds/openstack/keystone/v2_0/parse/ParseRackspaceApiMetadataTest.java @@ -19,18 +19,25 @@ package org.jclouds.openstack.keystone.v2_0.parse; import java.net.URI; +import java.util.Set; import javax.ws.rs.Consumes; import javax.ws.rs.core.MediaType; import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.json.BaseItemParserTest; +import org.jclouds.json.config.GsonModule; +import org.jclouds.openstack.keystone.v2_0.config.KeystoneParserModule; import org.jclouds.openstack.keystone.v2_0.domain.ApiMetadata; import org.jclouds.openstack.v2_0.domain.Link; import org.jclouds.rest.annotations.SelectJson; import org.testng.annotations.Test; import com.google.common.collect.ImmutableSet; +import com.google.gson.TypeAdapterFactory; +import com.google.inject.Guice; +import com.google.inject.Injector; +import com.google.inject.TypeLiteral; /** * @author Adrian Cole @@ -61,4 +68,9 @@ public class ParseRackspaceApiMetadataTest extends BaseItemParserTest bindings = Maps.newHashMap(); + private final Set factories = Sets.newHashSet(); @com.google.inject.Inject(optional = true) public void setBindings(Map bindings) { this.bindings.putAll(bindings); } + @com.google.inject.Inject(optional = true) + public void setFactories(Set factories) { + this.factories.addAll(factories); + } + public Map getBindings() { return bindings; } + + public Set getFactories() { + return factories; + } } @Override