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;
|
||||
|
||||
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<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
|
||||
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")
|
||||
private MediaTypesHolder mediaTypes = new MediaTypesHolder();
|
||||
private Set<MediaType> 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<MediaType> 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);
|
||||
}
|
||||
|
||||
}
|
|
@ -65,6 +65,23 @@ public class TenantClientExpectTest extends BaseKeystoneRestClientExpectTest<Key
|
|||
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() {
|
||||
TenantClient client = requestsSendResponses(keystoneAuthWithUsernameAndPassword, responseWithKeystoneAccess,
|
||||
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;
|
||||
|
||||
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<ApiMetadat
|
|||
))
|
||||
.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.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
|
||||
import javax.inject.Inject;
|
||||
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.Builder;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.primitives.Bytes;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.TypeAdapter;
|
||||
import com.google.gson.TypeAdapterFactory;
|
||||
import com.google.gson.internal.JsonReaderInternalAccess;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.google.gson.stream.JsonReader;
|
||||
|
@ -85,6 +88,10 @@ public class GsonModule extends AbstractModule {
|
|||
builder.registerTypeAdapter(binding.getKey(), binding.getValue());
|
||||
}
|
||||
|
||||
for (TypeAdapterFactory factory : bindings.getFactories()) {
|
||||
builder.registerTypeAdapterFactory(factory);
|
||||
}
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
|
@ -246,15 +253,25 @@ public class GsonModule extends AbstractModule {
|
|||
@Singleton
|
||||
public static class JsonAdapterBindings {
|
||||
private final Map<Type, Object> bindings = Maps.newHashMap();
|
||||
private final Set<TypeAdapterFactory> factories = Sets.newHashSet();
|
||||
|
||||
@com.google.inject.Inject(optional = true)
|
||||
public void setBindings(Map<Type, Object> 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() {
|
||||
return bindings;
|
||||
}
|
||||
|
||||
public Set<TypeAdapterFactory> getFactories() {
|
||||
return factories;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
Loading…
Reference in New Issue