openstack-keystone: adding TypeAdaptorFactory to take care of unwrapping objects containing an array of "values" in place of a normal json array

This commit is contained in:
Adam Lowe 2012-06-08 11:23:41 +01:00
parent 9718634570
commit 18faff54b3
8 changed files with 210 additions and 22 deletions

View File

@ -18,10 +18,28 @@
*/ */
package org.jclouds.openstack.keystone.v2_0.config; 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;
import org.jclouds.json.config.GsonModule.DateAdapter; 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.AbstractModule;
import com.google.inject.TypeLiteral;
/** /**
* @author Adam Lowe * @author Adam Lowe
@ -32,5 +50,70 @@ public class KeystoneParserModule extends AbstractModule {
@Override @Override
protected void configure() { protected void configure() {
bind(DateAdapter.class).to(GsonModule.Iso8601DateAdapter.class); bind(DateAdapter.class).to(GsonModule.Iso8601DateAdapter.class);
bind(new TypeLiteral<Set<TypeAdapterFactory>>() {
}).toInstance(ImmutableSet.<TypeAdapterFactory>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
* <p/>
* Treats [A,B,C] and {"values"=[A,B,C], "someotherstuff"=...} as the same Set
*/
public static class SetTypeAdapterFactory implements TypeAdapterFactory {
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> 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<T>) newSetAdapter(elementAdapter);
}
private <E> TypeAdapter<Set<E>> newSetAdapter(final TypeAdapter<E> elementAdapter) {
return new TypeAdapter<Set<E>>() {
public void write(JsonWriter out, Set<E> value) throws IOException {
out.beginArray();
for (E element : value) {
elementAdapter.write(out, element);
}
out.endArray();
}
public Set<E> read(JsonReader in) throws IOException {
Set<E> 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<E> result) throws IOException {
in.beginArray();
while (in.hasNext()) {
E element = elementAdapter.read(in);
result.add(element);
}
in.endArray();
}
}.nullSafe();
}
} }
} }

View File

@ -106,30 +106,14 @@ public class ApiMetadata extends Resource {
@Nullable @Nullable
private Date updated; 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<MediaType> values = ImmutableSet.of();
private MediaTypesHolder() {
}
private MediaTypesHolder(Set<MediaType> mediaTypes) {
this.values = ImmutableSet.copyOf(checkNotNull(mediaTypes, "mediaTypes"));
}
}
@SerializedName(value="media-types") @SerializedName(value="media-types")
private MediaTypesHolder mediaTypes = new MediaTypesHolder(); private Set<MediaType> mediaTypes = Sets.newLinkedHashSet();
protected ApiMetadata(Builder<?> builder) { protected ApiMetadata(Builder<?> builder) {
super(builder); super(builder);
this.status = checkNotNull(builder.status, "status"); this.status = checkNotNull(builder.status, "status");
this.updated = checkNotNull(builder.updated, "updated"); 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<MediaType> getMediaTypes() { public Set<MediaType> getMediaTypes() {
return Collections.unmodifiableSet(this.mediaTypes.values); return Collections.unmodifiableSet(this.mediaTypes);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hashCode(status, updated, mediaTypes.values); return Objects.hashCode(status, updated, mediaTypes);
} }
@Override @Override
@ -162,14 +146,14 @@ public class ApiMetadata extends Resource {
ApiMetadata that = ApiMetadata.class.cast(obj); ApiMetadata that = ApiMetadata.class.cast(obj);
return super.equals(that) && Objects.equal(this.status, that.status) return super.equals(that) && Objects.equal(this.status, that.status)
&& Objects.equal(this.updated, that.updated) && Objects.equal(this.updated, that.updated)
&& Objects.equal(this.mediaTypes.values, that.mediaTypes.values); && Objects.equal(this.mediaTypes, that.mediaTypes);
} }
protected ToStringHelper string() { protected ToStringHelper string() {
return super.string() return super.string()
.add("status", status) .add("status", status)
.add("updated", updated) .add("updated", updated)
.add("mediaTypes", mediaTypes.values); .add("mediaTypes", mediaTypes);
} }
} }

View File

@ -65,6 +65,23 @@ public class TenantClientExpectTest extends BaseKeystoneRestClientExpectTest<Key
assertEquals(tenants, expected); assertEquals(tenants, expected);
} }
public void testListTenantsATT() {
TenantClient client = requestsSendResponses(
keystoneAuthWithUsernameAndPassword,
responseWithKeystoneAccess,
standardRequestBuilder(endpoint + "/v2.0/tenants").build(),
standardResponseBuilder(200).payload(
payloadFromResourceWithContentType("/tenant_list_att.json", APPLICATION_JSON)).build())
.getTenantClient().get();
Set<Tenant> tenants = client.list();
assertNotNull(tenants);
assertFalse(tenants.isEmpty());
Set<Tenant> expected = ImmutableSet.of(Tenant.builder().name("this-is-a-test").id("14").description("None").build());
assertEquals(tenants, expected);
}
public void testListTenantsFailNotFound() { public void testListTenantsFailNotFound() {
TenantClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, responseWithKeystoneAccess, TenantClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, responseWithKeystoneAccess,
standardRequestBuilder(endpoint + "/v2.0/tenants").build(), standardResponseBuilder(404).build()) standardRequestBuilder(endpoint + "/v2.0/tenants").build(), standardResponseBuilder(404).build())

View File

@ -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<ApiMetadata> {
@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());
}
}

View File

@ -19,18 +19,25 @@
package org.jclouds.openstack.keystone.v2_0.parse; package org.jclouds.openstack.keystone.v2_0.parse;
import java.net.URI; import java.net.URI;
import java.util.Set;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MediaType;
import org.jclouds.date.internal.SimpleDateFormatDateService; import org.jclouds.date.internal.SimpleDateFormatDateService;
import org.jclouds.json.BaseItemParserTest; 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.keystone.v2_0.domain.ApiMetadata;
import org.jclouds.openstack.v2_0.domain.Link; import org.jclouds.openstack.v2_0.domain.Link;
import org.jclouds.rest.annotations.SelectJson; import org.jclouds.rest.annotations.SelectJson;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import com.google.common.collect.ImmutableSet; 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 * @author Adrian Cole
@ -61,4 +68,9 @@ public class ParseRackspaceApiMetadataTest extends BaseItemParserTest<ApiMetadat
)) ))
.build(); .build();
} }
@Override
protected Injector injector() {
return Guice.createInjector(new GsonModule(), new KeystoneParserModule());
}
} }

View File

@ -0,0 +1 @@
{"version": {"status": "beta", "updated": "2011-11-19T00:00:00Z", "media-types": [{"base": "application/json", "type": "application/vnd.openstack.identity-v2.0+json"}, {"base": "application/xml", "type": "application/vnd.openstack.identity-v2.0+xml"}], "id": "v2.0", "links": [{"href": "http://172.16.89.167:5000/v2.0/", "rel": "self"}, {"href": "http://docs.openstack.org/api/openstack-identity-service/2.0/content/", "type": "text/html", "rel": "describedby"}, {"href": "http://docs.openstack.org/api/openstack-identity-service/2.0/identity-dev-guide-2.0.pdf", "type": "application/pdf", "rel": "describedby"}]}}

View File

@ -0,0 +1 @@
{"tenants": {"values": [{"enabled": true, "description": "None", "name": "this-is-a-test", "id": "14"}], "links": []}}

View File

@ -25,6 +25,7 @@ import java.util.Enumeration;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Provider; import javax.inject.Provider;
@ -42,10 +43,12 @@ import org.jclouds.json.internal.OptionalTypeAdapterFactory;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder; import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.primitives.Bytes; import com.google.common.primitives.Bytes;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.GsonBuilder; import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter; import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.internal.JsonReaderInternalAccess; import com.google.gson.internal.JsonReaderInternalAccess;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
@ -85,6 +88,10 @@ public class GsonModule extends AbstractModule {
builder.registerTypeAdapter(binding.getKey(), binding.getValue()); builder.registerTypeAdapter(binding.getKey(), binding.getValue());
} }
for (TypeAdapterFactory factory : bindings.getFactories()) {
builder.registerTypeAdapterFactory(factory);
}
return builder.create(); return builder.create();
} }
@ -246,15 +253,25 @@ public class GsonModule extends AbstractModule {
@Singleton @Singleton
public static class JsonAdapterBindings { public static class JsonAdapterBindings {
private final Map<Type, Object> bindings = Maps.newHashMap(); private final Map<Type, Object> bindings = Maps.newHashMap();
private final Set<TypeAdapterFactory> factories = Sets.newHashSet();
@com.google.inject.Inject(optional = true) @com.google.inject.Inject(optional = true)
public void setBindings(Map<Type, Object> bindings) { public void setBindings(Map<Type, Object> bindings) {
this.bindings.putAll(bindings); this.bindings.putAll(bindings);
} }
@com.google.inject.Inject(optional = true)
public void setFactories(Set<TypeAdapterFactory> factories) {
this.factories.addAll(factories);
}
public Map<Type, Object> getBindings() { public Map<Type, Object> getBindings() {
return bindings; return bindings;
} }
public Set<TypeAdapterFactory> getFactories() {
return factories;
}
} }
@Override @Override