nacx commented on this pull request.


> +    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.
+
+-->
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xmlns="http://maven.apache.org/POM/4.0.0";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.jclouds.labs</groupId>
+        <artifactId>jclouds-labs</artifactId>
+        <version>2.2.0-SNAPSHOT</version>
+    </parent>
+    <artifactId>aliyun-ecs</artifactId>
+    <name>Cloudsoft :: jclouds Alibaba Elastic Compute Service API</name>

Cloudsoft?

> +                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <version>3.5.0</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <obrRepository>NONE</obrRepository>
+                    <instructions>
+                        
<!--<Bundle-Activator>${jclouds.osgi.activator}</Bundle-Activator>-->
+                        
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+                        
<!--<Export-Package>${jclouds.osgi.export}</Export-Package>-->
+                        
<Import-Package>${guava.osgi.import},${jclouds.osgi.import}</Import-Package>
+                        <DynamicImport-Package>*</DynamicImport-Package>
+                        
<!--<Fragment-Host>${jclouds.osgi.fragment}</Fragment-Host>-->
+                    </instructions>
+                </configuration>
+            </plugin>

Why do we need to configure the bundle here? The default config is not OK?

> +                <artifactId>maven-bundle-plugin</artifactId>
+                <version>3.5.0</version>
+                <extensions>true</extensions>
+                <configuration>
+                    <obrRepository>NONE</obrRepository>
+                    <instructions>
+                        
<!--<Bundle-Activator>${jclouds.osgi.activator}</Bundle-Activator>-->
+                        
<Bundle-SymbolicName>${project.artifactId}</Bundle-SymbolicName>
+                        
<!--<Export-Package>${jclouds.osgi.export}</Export-Package>-->
+                        
<Import-Package>${guava.osgi.import},${jclouds.osgi.import}</Import-Package>
+                        <DynamicImport-Package>*</DynamicImport-Package>
+                        
<!--<Fragment-Host>${jclouds.osgi.fragment}</Fragment-Host>-->
+                    </instructions>
+                </configuration>
+            </plugin>
+            <!--TODO to be removed-->

Remove it then

> +import static org.jclouds.reflect.Reflection2.typeToken;
+
+public class ECSServiceApiMetadata extends 
BaseHttpApiMetadata<ECSComputeServiceApi> {
+
+   public ECSServiceApiMetadata() {
+      this(new Builder());
+   }
+
+   protected ECSServiceApiMetadata(Builder builder) {
+      super(builder);
+   }
+
+   public static Properties defaultProperties() {
+      Properties properties = BaseHttpApiMetadata.defaultProperties();
+      properties.put(TEMPLATE,
+              
"osFamily=CENTOS,os64Bit=true,osVersionMatches=7.4,hardwareId=ecs.t5-lc2m1.nano,locationId=eu-central-1");

Let's just fix the OS by default. The default location should be configured by 
the jclouds *Implicit Location Supplier*. The default hardware is, by default, 
the smallest one.

> +import org.jclouds.http.HttpErrorHandler;
+import org.jclouds.http.annotation.ClientError;
+import org.jclouds.http.annotation.Redirection;
+import org.jclouds.http.annotation.ServerError;
+import org.jclouds.location.suppliers.ImplicitLocationSupplier;
+import org.jclouds.location.suppliers.implicit.FirstRegion;
+import org.jclouds.rest.ConfiguresHttpApi;
+import org.jclouds.rest.config.HttpApiModule;
+
+@ConfiguresHttpApi
+public class ECSComputeServiceHttpApiModule extends 
HttpApiModule<ECSComputeServiceApi> {
+
+   @Override
+   protected void configure() {
+      super.configure();
+      
bind(ImplicitLocationSupplier.class).to(FirstRegion.class).in(Scopes.SINGLETON);

Doesn't the default implementatio 
(`OnlyLocationOrFirstRegionOptionallyMatchingRegionId`) do pretty much the same?

> +
+   @Override
+   protected void configure() {
+      bind(GsonModule.DateAdapter.class).to(AliyunDateAdapter.class);
+   }
+
+   /**
+    * Data adapter for the date formats used by Aliyun.
+    * <p>
+    * Essentially this is a workaround for the Aliyun getUsage() API call 
returning a corrupted form of ISO-8601
+    * dates, which doesn't have seconds 2018-06-20T13:39Z
+    */
+   public static class AliyunDateAdapter extends GsonModule.Iso8601DateAdapter 
{
+
+      @Inject
+      private AliyunDateAdapter(DateService dateService) {

Remove the `private` modifier so you can properly test it.

> +
+   public abstract String status();
+
+   public abstract String imageName();
+
+   public abstract Boolean isSupportIoOptimizeds();
+
+   public abstract Boolean isSelfShared();
+
+   public abstract Boolean isCopied();
+
+   public abstract Boolean isSubscribed();
+
+   public abstract String platform();
+
+   public abstract String size();

Are all fields non-nullable? Also consider providing a builder, since it has 
too many fields.

> +
+   /**
+    * Returns a region enum corresponding to the given region name.
+    *
+    * @param regionName
+    *            The name of the region. Ex.: eu-west-1
+    * @return Region enum representing the given region name.
+    */
+   public static Regions fromName(String regionName) {
+      for (Regions region : Regions.values()) {
+         if (region.getName().equals(regionName)) {
+            return region;
+         }
+      }
+      throw new IllegalArgumentException("Cannot create enum from " + 
regionName + " value!");
+   }

Is there any API call to retrieve the list of available regions, instead of 
having them hardcoded?

> +   AP_NORTHEAST_1("ap-northeast-1", "Japan (Tokyo)"),
+   AP_SOUTH_1("ap-south-1", "India (Mumbai)"),
+   AP_SOUTHEAST_1("ap-southeast-1", "Singapore"),
+   AP_SOUTHEAST_2("ap-southeast-2", "Australia (Sydney)"),
+   AP_SOUTHEAST_3("ap-southeast-3", "Malaysia (Kuala Lumpur)"),
+   AP_SOUTHEAST_5("ap-southeast-5", "Indonesia (Jakarta)"),
+   CN_NORTH_1("cn-qingdao", "China (Qingdao)"),
+   CN_NORTH_2("cn-beijing", "China (Beijing)"),
+   CN_NORTH_3("cn-zhangjiakou", "China (Zhangjiakou)"),
+   CN_NORTH_5("cn-huhehaote", "China (Huhehaote)"),
+   CN_EAST_1("cn-hangzhou", "China (Zhangjiakou)"),
+   CN_EAST_2("cn-shanghai", "China (Shanghai)"),
+   CN_SOUTH_1("cn-shenzhen", "China (Shenzhen)"),
+   CN_SOUTH_2("cn-hongkong", "China (Hongkong)"),
+   CN_NORTHWEST_1("cn-northwest-1", "India (Mumbai)"),
+   ME_EAST_1("me-east-1", "UAE (Dubai)");

Consider adding the ISO codes here too, to make it easier to reuse the enum.

> +   public ListImagesOptions imageName(String imageName) {
+      queryParameters.put(IMAGE_NAME_PARAM, imageName);
+      return this;
+   }
+
+   public ListImagesOptions imageOwnerAlias(String imageOwnerAlias) {
+      queryParameters.put(IMAGE_OWNER_ALIAS_PARAM, imageOwnerAlias);
+      return this;
+   }
+
+   public ListImagesOptions usage(String usage) {
+      queryParameters.put(USAGE_PARAM, usage);
+      return this;
+   }
+
+   public ListImagesOptions paginationOptions(final PaginationOptions 
paginationOptions) {

Could it be better for this class to extend `PaginationOptions` instead?

> + * limitations under the License.
+ */
+package org.jclouds.aliyun.ecs.domain.options;
+
+import org.jclouds.http.options.BaseHttpRequestOptions;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public class PaginationOptions extends BaseHttpRequestOptions {
+
+   public static final String PAGE_NUMBER = "pageNumber";
+   public static final String PAGE_SIZE = "pageSize";
+
+   public PaginationOptions pageNumber(int pageNumber) {
+      checkState(pageNumber > 0, "pageSize must be > 0");
+      checkState(pageNumber <= 10000, "limit must be <= 50");

Precondition and message don't match.

> + * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.jclouds.aliyun.ecs.domain.options;
+
+import org.jclouds.http.options.BaseHttpRequestOptions;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public class PaginationOptions extends BaseHttpRequestOptions {
+
+   public static final String PAGE_NUMBER = "pageNumber";
+   public static final String PAGE_SIZE = "pageSize";
+
+   public PaginationOptions pageNumber(int pageNumber) {
+      checkState(pageNumber > 0, "pageSize must be > 0");

Wrong message

> +
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+
+/**
+ * 
https://www.alibabacloud.com/help/doc-detail/25534.htm?spm=a2c63.p38356.b99.330.79eb59abhmnMDE
+ */
+@Consumes(MediaType.APPLICATION_JSON)
+@RequestFilters(FormSign.class)
+@QueryParams(keys = {"Version", "Format", "SignatureVersion", "ServiceCode", 
"SignatureMethod"},
+        values = {"2014-05-26", "JSON", "1.0", "ecs", "HMAC-SHA1"})

Is this date the API version that can be set in the `ContextBuilder`? In that 
case instead of hardcoding you can resolve its value. Look at the Chef API for 
an example.

> +
+/**
+ * 
https://www.alibabacloud.com/help/doc-detail/25534.htm?spm=a2c63.p38356.b99.330.79eb59abhmnMDE
+ */
+@Consumes(MediaType.APPLICATION_JSON)
+@RequestFilters(FormSign.class)
+@QueryParams(keys = {"Version", "Format", "SignatureVersion", "ServiceCode", 
"SignatureMethod"},
+        values = {"2014-05-26", "JSON", "1.0", "ecs", "HMAC-SHA1"})
+public interface ImageApi {
+
+   @Named("image:list")
+   @GET
+   @QueryParams(keys = "Action", values = "DescribeImages")
+   @ResponseParser(ParseImages.class)
+   @Fallback(Fallbacks.EmptyIterableWithMarkerOnNotFoundOr404.class)
+   PaginatedCollection<Image> list(@QueryParam("RegionId") String region, 
ListImagesOptions... options);

Don't do this, as it is not OK to allow passing N option objects. Provide a 
method without options, and an overloaded one with just one options parameter.

> +   @ResponseParser(ParseImages.class)
+   @Transform(ParseImages.ToPagedIterable.class)
+   @Fallback(Fallbacks.EmptyPagedIterableOnNotFoundOr404.class)
+   PagedIterable<Image> list(@QueryParam("RegionId") String region);
+
+   @Singleton
+   final class ParseImages extends ParseJson<Images> {
+
+      @Inject
+      ParseImages(final Json json) {
+         super(json, TypeLiteral.get(Images.class));
+      }
+
+      static class ToPagedIterable extends Arg0ToPagedIterable<Image, 
ToPagedIterable> {
+
+         private ECSComputeServiceApi api;

Make it final

> + * 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.aliyun.ecs.filters;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+public class AcsURLEncoder {

Can't you just use 
[Strings2#urlEncode](https://github.com/jclouds/jclouds/blob/master/core/src/main/java/org/jclouds/util/Strings2.java#L56)?
 Or do you need to specifically handle the `~` characterd?

> +      decodedParams.put("Timestamp", timestamp);
+      decodedParams.put("SignatureNonce", signatureNonce);
+
+      String prefix;
+      try {
+         prefix = request.getMethod() + SEPARATOR + 
AcsURLEncoder.percentEncode("/") + SEPARATOR;
+      } catch (UnsupportedEncodingException e) {
+         throw Throwables.propagate(e);
+      }
+
+      // encode each parameter value first,
+      String stringToSign = prefix;
+      ImmutableSortedSet.Builder<String> builder = 
ImmutableSortedSet.naturalOrder();
+      for (Map.Entry<String, String> entry : decodedParams.entries())
+         builder.add(Strings2.urlEncode(entry.getKey()) + "=" + 
Strings2.urlEncode(entry.getValue()));
+      stringToSign += Strings2.urlEncode(Joiner.on("&").join(builder.build()));

If you use the jclouds `Uris` and `UriBuilder` classes I think you don't have 
to worry about manually encoding the parameters.

> +
+   @Inject
+   FormSign(@Provider Supplier<Credentials> creds) {
+      this.creds = creds;
+   }
+
+   public HttpRequest filter(HttpRequest request) throws HttpException {
+      Credentials currentCreds = checkNotNull(creds.get(), "credential 
supplier returned null");
+
+      Signer signer = Signer.getSigner();
+
+      String signature;
+
+      Multimap<String, String> decodedParams = 
queryParser().apply(request.getEndpoint().getRawQuery());
+
+      SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

This could be declared as a constant.

> +import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.bind.DatatypeConverter;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+public class HmacSHA1Signer extends Signer {
+
+   private static final String ALGORITHM_NAME = "HmacSHA1";
+
+   @Override
+   public String signString(String stringToSign, String accessKeySecret) {
+      try {
+         Mac mac = Mac.getInstance(ALGORITHM_NAME);
+         mac.init(new SecretKeySpec(accessKeySecret.getBytes(Charsets.UTF_8), 
ALGORITHM_NAME));
+         byte[] signData = mac.doFinal(stringToSign.getBytes(Charsets.UTF_8));

Instead of manually encrypting, get injected a 
[Crypto](https://github.com/jclouds/jclouds/blob/master/core/src/main/java/org/jclouds/crypto/Crypto.java#L46)
 object and use it. This way users will be able to configure they preferred 
security provider, such as Bouncycastle.

> +   }
+
+   protected void assertNodeRunning(String instanceId) {
+      assertTrue(instanceRunning.apply(instanceId),
+            String.format("Instance %s did not start in the configured 
timeout", instanceId));
+   }
+
+   protected void assertNodeSuspended(String instanceId) {
+      assertTrue(instanceSuspended.apply(instanceId),
+            String.format("Instance %s was not suspended in the configured 
timeout", instanceId));
+   }
+
+   protected void assertNodeTerminated(String instanceId) {
+      assertTrue(instanceTerminated.apply(instanceId),
+            String.format("Instance %s was not terminated in the configured 
timeout", instanceId));
+   }

Remove these methods until needed.

> +      assertEquals(request.getMethod(), method);
+      Map<String, String> queryParameters = 
Splitter.on('&').trimResults().withKeyValueSeparator("=").split(request.getPath());
+      assertEquals(queryParameters.get("Action"), action);
+      assertEquals(Integer.valueOf(queryParameters.get("pageNumber")), page);
+      assertEquals(request.getHeader("Accept"), "application/json");
+      return request;
+   }
+
+   protected RecordedRequest assertSent(MockWebServer server, String method, 
String path, String json)
+         throws InterruptedException {
+      RecordedRequest request = assertSent(server, method, path);
+      assertEquals(request.getHeader("Content-Type"), "application/json");
+      assertEquals(parser.parse(new String(request.getBody(), 
Charsets.UTF_8)), parser.parse(json));
+      return request;
+   }
+}

Remove all methods that are not used.

> +import static org.testng.Assert.assertTrue;
+
+@Test(groups = "unit", testName = "ImageApiMockTest", singleThreaded = true)
+public class ImageApiMockTest extends BaseECSComputeServiceApiMockTest {
+
+   public void testListImages() throws InterruptedException {
+      server.enqueue(jsonResponse("/images-first.json"));
+      server.enqueue(jsonResponse("/images-second.json"));
+      server.enqueue(jsonResponse("/images-last.json"));
+
+      Iterable<Image> images = 
api.imageApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertEquals(size(images), 28); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 3);
+      assertSent(server, "GET", "DescribeImages");
+      assertSent(server, "GET", "DescribeImages", 2);
+      assertSent(server, "GET", "DescribeImages", 3);

We should also verify that we're sending the page size parameter.

> +import static org.testng.Assert.assertTrue;
+
+@Test(groups = "unit", testName = "ImageApiMockTest", singleThreaded = true)
+public class ImageApiMockTest extends BaseECSComputeServiceApiMockTest {
+
+   public void testListImages() throws InterruptedException {
+      server.enqueue(jsonResponse("/images-first.json"));
+      server.enqueue(jsonResponse("/images-second.json"));
+      server.enqueue(jsonResponse("/images-last.json"));
+
+      Iterable<Image> images = 
api.imageApi().list(Regions.EU_CENTRAL_1.getName()).concat();
+      assertEquals(size(images), 28); // Force the PagedIterable to advance
+      assertEquals(server.getRequestCount(), 3);
+      assertSent(server, "GET", "DescribeImages");
+      assertSent(server, "GET", "DescribeImages", 2);
+      assertSent(server, "GET", "DescribeImages", 3);

And to leave the pagination stuff properly tested, add a test for a single page 
response, and for an empty list response.

-- 
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/jclouds/jclouds-labs/pull/438#pullrequestreview-133975031

Reply via email to