From 2dc9533e0b358b0d7c111e1cf7d72793d4145535 Mon Sep 17 00:00:00 2001 From: Adrian Cole Date: Sun, 11 Nov 2012 11:07:03 -0800 Subject: [PATCH] added ability to test if an api is available based on apiVersion --- .../rest/annotations/SinceApiVersion.java | 58 ++++++ .../functions/ImplicitOptionalConverter.java | 9 +- ...cographicallyAtOrAfterSinceApiVersion.java | 83 +++++++++ ...aphicallyAtOrAfterSinceApiVersionTest.java | 173 ++++++++++++++++++ 4 files changed, 320 insertions(+), 3 deletions(-) create mode 100644 core/src/main/java/org/jclouds/rest/annotations/SinceApiVersion.java create mode 100644 core/src/main/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion.java create mode 100644 core/src/test/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersionTest.java diff --git a/core/src/main/java/org/jclouds/rest/annotations/SinceApiVersion.java b/core/src/main/java/org/jclouds/rest/annotations/SinceApiVersion.java new file mode 100644 index 0000000000..3451574025 --- /dev/null +++ b/core/src/main/java/org/jclouds/rest/annotations/SinceApiVersion.java @@ -0,0 +1,58 @@ +/** + * 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 + * + * 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 org.jclouds.rest.annotations; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.inject.Qualifier; + +import org.jclouds.Constants; +import org.jclouds.ContextBuilder; + +/** + * Designates that this resource only exists since a particular + * {@link ApiVersion}. + * + * For example, in EC2, the tag api only exists at or after version + * {@code 2010-08-31} + * + * @author Adrian Cole + * @see ApiVersion + */ +@Target({ TYPE, METHOD, FIELD, PARAMETER }) +@Retention(RUNTIME) +@Qualifier +public @interface SinceApiVersion { + + /** + * less than or equal to the String bound to {@link ApiVersion}, typically + * bound as either {@link Constants#PROPERTY_API_VERSION} property or + * {@link ContextBuilder#apiVersion} + * + */ + String value(); + +} diff --git a/core/src/main/java/org/jclouds/rest/functions/ImplicitOptionalConverter.java b/core/src/main/java/org/jclouds/rest/functions/ImplicitOptionalConverter.java index c2b0a39bba..548ccb7c35 100644 --- a/core/src/main/java/org/jclouds/rest/functions/ImplicitOptionalConverter.java +++ b/core/src/main/java/org/jclouds/rest/functions/ImplicitOptionalConverter.java @@ -19,6 +19,7 @@ package org.jclouds.rest.functions; import org.jclouds.internal.ClassMethodArgsAndReturnVal; +import org.jclouds.rest.config.RestClientModule; import com.google.common.annotations.Beta; import com.google.common.base.Function; @@ -65,8 +66,10 @@ import com.google.inject.ImplementedBy; *
  • call another api which can validate the feature can be presented
  • * * - * The {@link AlwaysPresentImplicitOptionalConverter default implementation} - * always returns present. To override this, add the following in your subclass + * The {@link PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion + * default implementation} returns present if no {@link SinceApiVersion} + * annotation is assigned, or the value is less than or equal to the current + * {@link ApiVersion}. To override this, add the following in your subclass * override of {@link RestClientModule#configure} method: * *
    @@ -76,7 +79,7 @@ import com.google.inject.ImplementedBy;
      * @author Adrian Cole
      */
     @Beta
    -@ImplementedBy(AlwaysPresentImplicitOptionalConverter.class)
    +@ImplementedBy(PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion.class)
     public interface ImplicitOptionalConverter extends Function> {
     
     }
    diff --git a/core/src/main/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion.java b/core/src/main/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion.java
    new file mode 100644
    index 0000000000..ed0f4230ad
    --- /dev/null
    +++ b/core/src/main/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion.java
    @@ -0,0 +1,83 @@
    +/**
    + * 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
    + *
    + *   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 org.jclouds.rest.functions;
    +
    +import static com.google.common.base.Preconditions.checkNotNull;
    +
    +import javax.inject.Inject;
    +import javax.inject.Singleton;
    +
    +import org.jclouds.internal.ClassMethodArgsAndReturnVal;
    +import org.jclouds.rest.annotations.ApiVersion;
    +import org.jclouds.rest.annotations.SinceApiVersion;
    +
    +import com.google.common.annotations.Beta;
    +import com.google.common.annotations.VisibleForTesting;
    +import com.google.common.base.Optional;
    +import com.google.common.cache.CacheBuilder;
    +import com.google.common.cache.CacheLoader;
    +import com.google.common.cache.LoadingCache;
    +
    +/**
    + * 
    + * @author Adrian Cole
    + */
    +@Beta
    +@Singleton
    +public class PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion implements ImplicitOptionalConverter {
    +
    +   @VisibleForTesting
    +   static final class Loader extends CacheLoader> {
    +      private final String apiVersion;
    +
    +      @Inject
    +      Loader(@ApiVersion String apiVersion) {
    +         this.apiVersion = checkNotNull(apiVersion, "apiVersion");
    +      }
    +
    +      @Override
    +      public Optional load(ClassMethodArgsAndReturnVal input) {
    +         Optional sinceApiVersion = Optional.fromNullable(input.getClazz().getAnnotation(
    +               SinceApiVersion.class));
    +         if (sinceApiVersion.isPresent()) {
    +            String since = sinceApiVersion.get().value();
    +            if (since.compareTo(apiVersion) <= 0)
    +               return Optional.of(input.getReturnVal());
    +            return Optional.absent();
    +         } else {
    +            // No SinceApiVersion annotation, so return present
    +            return Optional.of(input.getReturnVal());
    +         }
    +      }
    +   }
    +
    +   private final LoadingCache> lookupCache;
    +
    +   @Inject
    +   protected PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion(@ApiVersion String apiVersion) {
    +      // no need to read class annotations for every request
    +      this.lookupCache = CacheBuilder.newBuilder().build(new Loader(apiVersion));
    +   }
    +
    +   @Override
    +   public Optional apply(ClassMethodArgsAndReturnVal input) {
    +      return lookupCache.getUnchecked(input);
    +   }
    +
    +}
    diff --git a/core/src/test/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersionTest.java b/core/src/test/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersionTest.java
    new file mode 100644
    index 0000000000..8bd5a8d965
    --- /dev/null
    +++ b/core/src/test/java/org/jclouds/rest/functions/PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersionTest.java
    @@ -0,0 +1,173 @@
    +/**
    + * 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
    + *
    + *   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 org.jclouds.rest.functions;
    +
    +import static com.google.common.base.Throwables.propagate;
    +import static org.testng.Assert.assertEquals;
    +import static org.testng.Assert.assertTrue;
    +
    +import java.util.concurrent.TimeUnit;
    +import java.util.logging.Logger;
    +
    +import org.jclouds.internal.ClassMethodArgsAndReturnVal;
    +import org.jclouds.rest.annotations.Delegate;
    +import org.jclouds.rest.annotations.SinceApiVersion;
    +import org.jclouds.rest.functions.PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion.Loader;
    +import org.testng.annotations.Test;
    +
    +import com.google.common.base.Optional;
    +import com.google.common.base.Stopwatch;
    +
    +/**
    + * Allows you to use simple api version comparison to determine if a feature is
    + * available.
    + * 
    + * @author Adrian Cole
    + */
    +@Test(groups = "unit")
    +public class PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersionTest {
    +
    +   // feature present in base api
    +   static interface KeyPairAsyncApi {
    +
    +   }
    +
    +   @SinceApiVersion("2010-08-31")
    +   static interface TagAsyncApi {
    +
    +   }
    +
    +   @SinceApiVersion("2011-01-01")
    +   static interface VpcAsyncApi {
    +
    +   }
    +
    +   static interface EC2AsyncApi {
    +
    +      @Delegate
    +      Optional getTagApiForRegion(String region);
    +
    +      @Delegate
    +      Optional getKeyPairApiForRegion(String region);
    +
    +      @Delegate
    +      Optional getVpcApiForRegion(String region);
    +
    +   }
    +
    +   public void testPresentWhenSinceApiVersionUnset() {
    +      ImplicitOptionalConverter fn = forApiVersion("2011-07-15");
    +      assertEquals(fn.apply(getKeyPairApi()), Optional.of("present"));
    +      assertEquals(fn.apply(getFloatingIPApi()), Optional.of("present"));
    +      assertEquals(fn.apply(getVpcApi()), Optional.of("present"));
    +   }
    +
    +   public void testPresentWhenSinceApiVersionUnsetOrEqualToApiVersion() {
    +      ImplicitOptionalConverter fn = forApiVersion("2011-01-01");
    +      assertEquals(fn.apply(getKeyPairApi()), Optional.of("present"));
    +      assertEquals(fn.apply(getFloatingIPApi()), Optional.of("present"));
    +      assertEquals(fn.apply(getVpcApi()), Optional.of("present"));
    +   }
    +
    +   public void testNotPresentWhenSinceApiVersionSetAndGreaterThanApiVersion() throws SecurityException,
    +         NoSuchMethodException {
    +      ImplicitOptionalConverter fn = forApiVersion("2006-06-26");
    +      assertEquals(fn.apply(getKeyPairApi()), Optional.of("present"));
    +      assertEquals(fn.apply(getFloatingIPApi()), Optional.absent());
    +      assertEquals(fn.apply(getVpcApi()), Optional.absent());
    +   }
    +
    +   private ImplicitOptionalConverter forApiVersion(String apiVersion) {
    +      return new PresentWhenApiVersionLexicographicallyAtOrAfterSinceApiVersion(apiVersion);
    +   }
    +
    +   public void testLoaderPresentWhenSinceApiVersionUnset() {
    +      Loader fn = new Loader("2011-07-15");
    +      assertEquals(fn.load(getKeyPairApi()), Optional.of("present"));
    +      assertEquals(fn.load(getFloatingIPApi()), Optional.of("present"));
    +      assertEquals(fn.load(getVpcApi()), Optional.of("present"));
    +   }
    +
    +   public void testLoaderPresentWhenSinceApiVersionUnsetOrEqualToApiVersion() {
    +      Loader fn = new Loader("2011-01-01");
    +      assertEquals(fn.load(getKeyPairApi()), Optional.of("present"));
    +      assertEquals(fn.load(getFloatingIPApi()), Optional.of("present"));
    +      assertEquals(fn.load(getVpcApi()), Optional.of("present"));
    +   }
    +
    +   public void testLoaderNotPresentWhenSinceApiVersionSetAndGreaterThanApiVersion() throws SecurityException,
    +         NoSuchMethodException {
    +      Loader fn = new Loader("2006-06-26");
    +      assertEquals(fn.load(getKeyPairApi()), Optional.of("present"));
    +      assertEquals(fn.load(getFloatingIPApi()), Optional.absent());
    +      assertEquals(fn.load(getVpcApi()), Optional.absent());
    +   }
    +
    +   public void testCacheIsFasterWhenNoAnnotationPresent() {
    +      ClassMethodArgsAndReturnVal keyPairApi = getKeyPairApi();
    +      ImplicitOptionalConverter fn = forApiVersion("2011-07-15");
    +      Stopwatch watch = new Stopwatch().start();
    +      fn.apply(keyPairApi);
    +      long first = watch.stop().elapsedTime(TimeUnit.MICROSECONDS);
    +      watch.reset().start();
    +      fn.apply(keyPairApi);
    +      long cached = watch.stop().elapsedTime(TimeUnit.MICROSECONDS);
    +      assertTrue(cached < first, String.format("cached [%s] should be less than initial [%s]", cached, first));
    +      Logger.getAnonymousLogger().info(
    +            "lookup cache saved " + (first - cached) + " microseconds when no annotation present");
    +   }
    +
    +   public void testCacheIsFasterWhenAnnotationPresent() {
    +      ClassMethodArgsAndReturnVal floatingIpApi = getKeyPairApi();
    +      ImplicitOptionalConverter fn = forApiVersion("2011-07-15");
    +      Stopwatch watch = new Stopwatch().start();
    +      fn.apply(floatingIpApi);
    +      long first = watch.stop().elapsedTime(TimeUnit.MICROSECONDS);
    +      watch.reset().start();
    +      fn.apply(floatingIpApi);
    +      long cached = watch.stop().elapsedTime(TimeUnit.MICROSECONDS);
    +      assertTrue(cached < first, String.format("cached [%s] should be less than initial [%s]", cached, first));
    +      Logger.getAnonymousLogger().info(
    +            "lookup cache saved " + (first - cached) + " microseconds when annotation present");
    +
    +   }
    +
    +   ClassMethodArgsAndReturnVal getFloatingIPApi() {
    +      return getApi("Tag", TagAsyncApi.class);
    +   }
    +
    +   ClassMethodArgsAndReturnVal getKeyPairApi() {
    +      return getApi("KeyPair", KeyPairAsyncApi.class);
    +   }
    +
    +   ClassMethodArgsAndReturnVal getVpcApi() {
    +      return getApi("Vpc", VpcAsyncApi.class);
    +   }
    +
    +   ClassMethodArgsAndReturnVal getApi(String name, Class type) {
    +      try {
    +         return ClassMethodArgsAndReturnVal.builder().clazz(type)
    +               .method(EC2AsyncApi.class.getDeclaredMethod("get" + name + "ApiForRegion", String.class))
    +               .args(new Object[] { "region" }).returnVal("present").build();
    +      } catch (Exception e) {
    +         throw propagate(e);
    +      }
    +   }
    +
    +}