mirror of https://github.com/apache/jclouds.git
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:
parent
9718634570
commit
18faff54b3
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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())
|
||||||
|
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"}]}}
|
|
@ -0,0 +1 @@
|
||||||
|
{"tenants": {"values": [{"enabled": true, "description": "None", "name": "this-is-a-test", "id": "14"}], "links": []}}
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue