You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by rl...@apache.org on 2017/06/14 21:19:54 UTC

[23/26] ambari git commit: AMBARI-21207. Extend Swagger Maven plugin to handle nested APIs (Balazs Bence Sari via adoroszlai)

AMBARI-21207. Extend Swagger Maven plugin to handle nested APIs (Balazs Bence Sari via adoroszlai)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/03812cba
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/03812cba
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/03812cba

Branch: refs/heads/branch-feature-AMBARI-20859
Commit: 03812cba4fff24093cf10035667d1a99d71e9a80
Parents: 24e2cac
Author: Balazs Bence Sari <bs...@hortonworks.com>
Authored: Wed Jun 14 14:38:10 2017 +0200
Committer: Attila Doroszlai <ad...@hortonworks.com>
Committed: Wed Jun 14 14:38:24 2017 +0200

----------------------------------------------------------------------
 ambari-project/pom.xml                          |  47 +++-
 ambari-server/pom.xml                           |  28 +--
 .../server/api/services/ClusterService.java     |   2 +-
 .../server/api/services/ServiceService.java     |   3 +-
 utility/pom.xml                                 |  20 ++
 .../ambari/swagger/AmbariSwaggerReader.java     | 222 +++++++++++++++++++
 .../ambari/swagger/AmbariSwaggerReaderTest.java | 182 +++++++++++++++
 utility/src/test/resources/log4j.properties     |  19 ++
 8 files changed, 499 insertions(+), 24 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/ambari-project/pom.xml
----------------------------------------------------------------------
diff --git a/ambari-project/pom.xml b/ambari-project/pom.xml
index 9bb2e26..b06bd18 100644
--- a/ambari-project/pom.xml
+++ b/ambari-project/pom.xml
@@ -32,6 +32,8 @@
     <powermock.version>1.6.3</powermock.version>
     <jetty.version>8.1.19.v20160209</jetty.version>
     <checkstyle.version>6.19</checkstyle.version> <!-- last version that does not require Java 8 -->
+    <swagger.version>1.5.10</swagger.version>
+    <slf4j.version>1.7.20</slf4j.version>
     <forkCount>4</forkCount>
     <reuseForks>false</reuseForks>
   </properties>
@@ -210,12 +212,22 @@
       <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-api</artifactId>
-        <version>1.7.20</version>
+        <version>${slf4j.version}</version>
       </dependency>
       <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-log4j12</artifactId>
-        <version>1.7.20</version>
+        <version>${slf4j.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>jul-to-slf4j</artifactId>
+        <version>${slf4j.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
+        <artifactId>jcl-over-slf4j</artifactId>
+        <version>${slf4j.version}</version>
       </dependency>
       <dependency>
         <groupId>org.eclipse.persistence</groupId>
@@ -312,7 +324,6 @@
         <artifactId>ant-launcher</artifactId>
         <version>1.7.1</version>
       </dependency>
-
       <dependency>
         <groupId>commons-logging</groupId>
         <artifactId>commons-logging</artifactId>
@@ -486,6 +497,36 @@
         <artifactId>checkstyle</artifactId>
         <version>${checkstyle.version}</version>
       </dependency>
+      <dependency>
+        <groupId>org.easymock</groupId>
+        <artifactId>easymock</artifactId>
+        <version>3.4</version>
+      </dependency>
+      <dependency>
+        <groupId>io.swagger</groupId>
+        <artifactId>swagger-annotations</artifactId>
+        <version>${swagger.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.swagger</groupId>
+        <artifactId>swagger-core</artifactId>
+        <version>${swagger.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.swagger</groupId>
+        <artifactId>swagger-jaxrs</artifactId>
+        <version>${swagger.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>io.swagger</groupId>
+        <artifactId>swagger-models</artifactId>
+        <version>${swagger.version}</version>
+      </dependency>
+      <dependency>
+        <groupId>com.github.kongchen</groupId>
+        <artifactId>swagger-maven-plugin</artifactId>
+        <version>3.1.4</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
   <build>

http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/ambari-server/pom.xml
----------------------------------------------------------------------
diff --git a/ambari-server/pom.xml b/ambari-server/pom.xml
index a1cd239..ac78595 100644
--- a/ambari-server/pom.xml
+++ b/ambari-server/pom.xml
@@ -48,7 +48,6 @@
     <tarballResourcesFolder>src/main/resources</tarballResourcesFolder>
     <skipPythonTests>false</skipPythonTests>
     <hadoop.version>2.7.2</hadoop.version>
-    <swagger.version>1.5.10</swagger.version>
     <empty.dir>src/main/package</empty.dir> <!-- any directory in project with not very big amount of files (not to waste-load them) -->
     <el.log>ALL</el.log> <!-- log level for EclipseLink eclipselink-staticweave-maven-plugin -->
     <xlint>none</xlint> <!-- passed to Java compiler -Xlint: flag -->
@@ -413,10 +412,10 @@
       <plugin>
         <groupId>com.github.kongchen</groupId>
         <artifactId>swagger-maven-plugin</artifactId>
-        <version>3.1.4</version>
         <configuration>
           <apiSources>
             <apiSource>
+              <swaggerApiReader>org.apache.ambari.swagger.AmbariSwaggerReader</swaggerApiReader>
               <springmvc>false</springmvc>
               <locations>org.apache.ambari.server.api.services</locations>
               <schemes>http,https</schemes>
@@ -1291,12 +1290,18 @@
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-api</artifactId>
-      <version>1.7.20</version>
     </dependency>
     <dependency>
       <groupId>org.slf4j</groupId>
       <artifactId>slf4j-log4j12</artifactId>
-      <version>1.7.20</version>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jul-to-slf4j</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
     </dependency>
     <dependency>
       <groupId>log4j</groupId>
@@ -1426,17 +1431,14 @@
     <dependency>
       <groupId>io.swagger</groupId>
       <artifactId>swagger-annotations</artifactId>
-      <version>${swagger.version}</version>
     </dependency>
     <dependency>
       <groupId>io.swagger</groupId>
       <artifactId>swagger-core</artifactId>
-      <version>${swagger.version}</version>
     </dependency>
     <dependency>
       <groupId>io.swagger</groupId>
       <artifactId>swagger-jaxrs</artifactId>
-      <version>${swagger.version}</version>
       <exclusions>
         <exclusion>
           <!-- Because it is already in the jersey one and causes the shade plugin to be confused -->
@@ -1448,7 +1450,6 @@
     <dependency>
       <groupId>io.swagger</groupId>
       <artifactId>swagger-models</artifactId>
-      <version>${swagger.version}</version>
     </dependency>
     <dependency>
       <groupId>org.codehaus.jackson</groupId>
@@ -1477,7 +1478,6 @@
     <dependency>
       <groupId>org.easymock</groupId>
       <artifactId>easymock</artifactId>
-      <version>3.4</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -1651,16 +1651,6 @@
       <scope>compile</scope>
     </dependency>
     <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>jul-to-slf4j</artifactId>
-      <version>1.7.20</version>
-    </dependency>
-    <dependency>
-      <groupId>org.slf4j</groupId>
-      <artifactId>jcl-over-slf4j</artifactId>
-      <version>1.7.20</version>
-    </dependency>
-    <dependency>
       <groupId>io.dropwizard.metrics</groupId>
       <artifactId>metrics-core</artifactId>
       <version>3.1.0</version>

http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java b/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
index f61fb2a..44d50731 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/api/services/ClusterService.java
@@ -505,7 +505,7 @@ public class ClusterService extends BaseService {
    * @return the services service
    */
   @Path("{clusterName}/services")
-  public ServiceService getServiceHandler(@Context javax.ws.rs.core.Request request, @PathParam("clusterName") String clusterName) {
+  public ServiceService getServiceHandler(@Context javax.ws.rs.core.Request request, @ApiParam @PathParam("clusterName") String clusterName) {
     return new ServiceService(clusterName);
   }
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java b/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
index a28c4aa..6ab2704 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/api/services/ServiceService.java
@@ -51,7 +51,7 @@ import io.swagger.annotations.ApiResponses;
 /**
  * Service responsible for services resource requests.
  */
-@Api(value = "Services", description = "Endpoint for service specific operations")
+@Api(value = "Cluster Services", description = "Endpoint for service specific operations")
 public class ServiceService extends BaseService {
   private static final String SERVICE_REQUEST_TYPE = "org.apache.ambari.server.controller.ServiceRequestSwagger";
   private static final String ARTIFACT_REQUEST_TYPE = "org.apache.ambari.server.controller.ClusterServiceArtifactRequest";
@@ -112,6 +112,7 @@ public class ServiceService extends BaseService {
    * @return service collection resource representation
    */
   @GET
+  @Path("") // This is needed if class level path is not present otherwise no Swagger docs will be generated for this method
   @Produces(MediaType.TEXT_PLAIN)
   @ApiOperation(value = "Get all services",
       nickname = "ServiceService#getServices",

http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/utility/pom.xml
----------------------------------------------------------------------
diff --git a/utility/pom.xml b/utility/pom.xml
index 918080e..ac91474 100644
--- a/utility/pom.xml
+++ b/utility/pom.xml
@@ -33,6 +33,21 @@
 
   <dependencies>
     <dependency>
+      <groupId>com.github.kongchen</groupId>
+      <artifactId>swagger-maven-plugin</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
       <scope>compile</scope>    <!-- has to be compile-time dependency on junit -->
@@ -53,6 +68,11 @@
       <artifactId>guava</artifactId>
       <version>19.0</version> <!-- required for checkstyle -->
     </dependency>
+    <dependency>
+      <groupId>org.easymock</groupId>
+      <artifactId>easymock</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>

http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/utility/src/main/java/org/apache/ambari/swagger/AmbariSwaggerReader.java
----------------------------------------------------------------------
diff --git a/utility/src/main/java/org/apache/ambari/swagger/AmbariSwaggerReader.java b/utility/src/main/java/org/apache/ambari/swagger/AmbariSwaggerReader.java
new file mode 100644
index 0000000..e258fc4
--- /dev/null
+++ b/utility/src/main/java/org/apache/ambari/swagger/AmbariSwaggerReader.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ambari.swagger;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.ws.rs.Path;
+
+import org.apache.maven.plugin.logging.Log;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+
+import com.github.kongchen.swagger.docgen.reader.JaxrsReader;
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
+import io.swagger.annotations.Api;
+import io.swagger.models.Operation;
+import io.swagger.models.Swagger;
+import io.swagger.models.Tag;
+import io.swagger.models.parameters.Parameter;
+import io.swagger.models.parameters.PathParameter;
+
+/**
+ * Customized {@link com.github.kongchen.swagger.docgen.reader.ClassSwaggerReader} implementation to
+ * treat nested API's.
+ */
+public class AmbariSwaggerReader extends JaxrsReader {
+
+  /**
+   * Logger instance.
+   */
+  protected final static Logger logger = LoggerFactory.getLogger(AmbariSwaggerReader.class);
+
+  public AmbariSwaggerReader(Swagger swagger, Log LOG) {
+    super(swagger, LOG);
+  }
+
+  private final Map<Class<?>, NestedApiRecord> nestedAPIs = Maps.newHashMap();
+
+  @Override
+  public Swagger getSwagger() {
+    if (null == this.swagger) {
+      this.swagger = new Swagger();
+    }
+    return this.swagger;
+  }
+
+  /**
+   * Original method is overwritten so that to gather information about top level api - nested api relations
+   */
+  @Override
+  public Swagger read(Set<Class<?>> classes) {
+    // scan for and register nested API classes
+    logger.debug("Looking for nested API's");
+    for (Class<?> cls: classes) {
+      logger.debug("Examining API {}", cls.getSimpleName());
+      for (Method method: cls.getMethods()) {
+        Path methodPath = AnnotationUtils.findAnnotation(method, Path.class);
+        if (null != methodPath) {
+          Class<?> returnType = method.getReturnType();
+          Api nestedApi = AnnotationUtils.findAnnotation(returnType, Api.class);
+          Path nestedApiPath = AnnotationUtils.findAnnotation(returnType, Path.class);
+          logger.debug("Examinig API method {}#{}, path={}, returnType={}", cls.getSimpleName(), method.getName(),
+              nestedApiPath != null ? nestedApiPath.value() : null, returnType.getSimpleName());
+          if (null != nestedApi) {
+            if (null != nestedApiPath) {
+              logger.info("This class exists both as top level and nested API: {}, treating it as top level API",
+                  returnType.getName());
+            }
+            else {
+              Path apiPath = AnnotationUtils.findAnnotation(cls, Path.class);
+              String apiPathValue;
+              if (null == apiPath) {
+                logger.warn("Parent api {} also seems to be a nested API. The current version does not support " +
+                    "multi-level nesting.");
+                apiPathValue = "";
+              }
+              else {
+                apiPathValue = apiPath.value();
+              }
+              NestedApiRecord nar = new NestedApiRecord(returnType, cls, apiPathValue, method, methodPath.value());
+              if (nestedAPIs.containsKey(returnType)) {
+                logger.warn("{} is a nested API of multiple top level API's. Ignoring top level API {}", returnType, cls);
+              }
+              else {
+                logger.info("Registering nested API: {}", returnType);
+                nestedAPIs.put(returnType, nar);
+              }
+            }
+          }
+        }
+      }
+    }
+    logger.info("Found {} nested API's", nestedAPIs.size());
+    // With all information gathered, call superclass implementation
+    return super.read(classes);
+  }
+
+  /**
+   * Original method is overwritten to handle nested api's properly
+   */
+  @Override
+  protected Swagger read(Class<?> cls, String parentPath,
+                         String parentMethod,
+                         boolean readHidden,
+                         String[] parentConsumes,
+                         String[] parentProduces,
+                         Map<String, Tag> parentTags,
+                         List<Parameter> parentParameters) {
+    NestedApiRecord nestedApiRecord = nestedAPIs.get(cls);
+    if (null != nestedApiRecord) {
+      logger.info("Processing nested API: {}", nestedApiRecord);
+      // Get the path parameters of the parent API method. All methods of the nested API class should include these
+      // parameters.
+      Operation operation = parseMethod(nestedApiRecord.parentMethod);
+      List<Parameter> pathParameters = ImmutableList.copyOf(
+          Collections2.filter(operation.getParameters(), Predicates.instanceOf(PathParameter.class)));
+      logger.info("Will copy path params from parent method: {}",
+          Lists.transform(pathParameters, new ParameterToName()));
+      return super.read(cls,
+          joinPaths(nestedApiRecord.parentApiPath, nestedApiRecord.parentMethodPath, parentPath),
+          parentMethod, readHidden,
+          parentConsumes, parentProduces, parentTags, pathParameters);
+    }
+    else {
+      logger.info("Processing top level API: {}", cls.getSimpleName());
+      return super.read(cls, parentPath, parentMethod, readHidden, parentConsumes, parentProduces, parentTags, parentParameters);
+    }
+  }
+
+  /**
+   * Joins path elements properly with slashes avoiding duplicate slashes.
+   *
+   * @param firstPath the first path element
+   * @param paths optionally other path elements
+   * @return the joined path
+   */
+  static String joinPaths(String firstPath, String... paths) {
+    StringBuilder joined = new StringBuilder(firstPath);
+    for(String path: paths) {
+      if (path.isEmpty()) { /* NOP */ }
+      else if (joined.length() == 0) {
+        joined.append(path);
+      }
+      else if (joined.charAt(joined.length() - 1) == '/') {
+        if (path.startsWith("/")) {
+          joined.append(path.substring(1, path.length()));
+        }
+        else {
+          joined.append(path);
+        }
+      }
+      else {
+        if (path.startsWith("/")) {
+          joined.append(path);
+        }
+        else {
+          joined.append('/').append(path);
+        }
+
+      }
+    }
+    return joined.toString();
+  }
+}
+
+class ParameterToName implements Function<Parameter, String> {
+  public String apply(Parameter input) {
+    return input.getName();
+  }
+}
+
+class NestedApiRecord {
+  final Class<?> nestedApi;
+  final Class<?> parentApi;
+  final String parentApiPath;
+  final Method parentMethod;
+  final String parentMethodPath;
+
+  public NestedApiRecord(Class<?> nestedApi, Class<?> parentApi, String parentApiPath, Method parentMethod, String parentMethodPath) {
+    this.nestedApi = nestedApi;
+    this.parentApi = parentApi;
+    this.parentApiPath = parentApiPath;
+    this.parentMethod = parentMethod;
+    this.parentMethodPath = parentMethodPath;
+  }
+
+  @Override
+  public String toString() {
+    return "NestedApiRecord {" +
+        "nestedApi=" + nestedApi +
+        ", parentApi=" + parentApi +
+        ", parentApiPath='" + parentApiPath + '\'' +
+        ", parentMethod=" + parentMethod +
+        ", parentMethodPath='" + parentMethodPath + '\'' +
+        '}';
+  }
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/utility/src/test/java/org/apache/ambari/swagger/AmbariSwaggerReaderTest.java
----------------------------------------------------------------------
diff --git a/utility/src/test/java/org/apache/ambari/swagger/AmbariSwaggerReaderTest.java b/utility/src/test/java/org/apache/ambari/swagger/AmbariSwaggerReaderTest.java
new file mode 100644
index 0000000..a102152
--- /dev/null
+++ b/utility/src/test/java/org/apache/ambari/swagger/AmbariSwaggerReaderTest.java
@@ -0,0 +1,182 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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.apache.ambari.swagger;
+
+import static org.easymock.EasyMock.createMock;
+import static org.junit.Assert.assertEquals ;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+
+import org.apache.commons.collections.set.ListOrderedSet;
+import org.apache.maven.plugin.logging.Log;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import io.swagger.models.Response;
+import io.swagger.models.Swagger;
+import io.swagger.models.parameters.Parameter;
+import io.swagger.models.parameters.PathParameter;
+
+public class AmbariSwaggerReaderTest {
+
+
+  /**
+   * Test the {@link AmbariSwaggerReader#joinPaths(String, String...)} method
+   */
+  @Test
+  public void testJoinPaths() {
+    assertEquals("/toplevel/nested/{param}/list",
+        AmbariSwaggerReader.joinPaths("", "/", "/", "", "toplevel", "/nested/", "/{param}", "list"));
+    assertEquals("/toplevel/nested/{param}/list",
+        AmbariSwaggerReader.joinPaths("/", "toplevel", "", "/nested/", "/", "/{param}", "list", ""));
+  }
+
+  /**
+   * Test the basic case: one top level API and one nested API, each with one operation
+   */
+  @Test
+  public void swaggerBasicCase() {
+    AmbariSwaggerReader asr = new AmbariSwaggerReader(null, createMock(Log.class));
+    Swagger swagger = asr.read(ImmutableSet.of(TopLevelAPI.class, NestedAPI.class));
+    assertEquals(ImmutableSet.of("/toplevel/top", "/toplevel/{param}/nested/list"),
+        swagger.getPaths().keySet());
+    assertPathParamsExist(swagger, "/toplevel/{param}/nested/list", "param");
+  }
+
+  /**
+   * Test conflicting nested API's (the same API's are returned from different top level API's).
+   * In this case the nested API should be associated to the first processed top level API.
+   */
+  @Test
+  public void swaggerConflictingNestedApis() {
+    AmbariSwaggerReader asr = new AmbariSwaggerReader(null, createMock(Log.class));
+    ListOrderedSet classes = ListOrderedSet.decorate(
+        Lists.newArrayList(TopLevelAPI.class, AnotherTopLevelAPI.class, NestedAPI.class));
+    Swagger swagger = asr.read(classes);
+    assertEquals(
+        ImmutableSet.of("/toplevel/top", "/toplevel/{param}/nested/list", "/toplevel2/anotherTop"),
+        swagger.getPaths().keySet());
+    assertPathParamsExist(swagger, "/toplevel/{param}/nested/list", "param");
+  }
+
+  /**
+   * If an API is both top level (the class has a @Path annotation) and nested (class is a return type of an
+   * API operation) then it should be treated as top level.
+   */
+  @Test
+  public void swaggerApiThatIsBothTopLevelAndNestedIsCountedAsTopLevel() {
+    AmbariSwaggerReader asr = new AmbariSwaggerReader(null, createMock(Log.class));
+    Swagger swagger = asr.read(ImmutableSet.of(YetAnotherTopLevelAPI.class, NestedAndTopLevelAPI.class));
+    assertEquals(ImmutableSet.of("/toplevel3/yetAnotherTop", "/canBeReachedFromTopToo/list"),
+        swagger.getPaths().keySet());
+  }
+
+
+  /**
+   * Verify that the top level API's path parameters are transferred to the nested API.
+   */
+  private static void assertPathParamsExist(Swagger swagger, String path, String... expectedPathParams) {
+    List<Parameter> parameters = swagger.getPath(path).getGet().getParameters();
+    assertNotNull("No path parameters for path: " + path, parameters);
+    Set<String> pathParamNames = new HashSet<>();
+    for (Parameter param: parameters) {
+      if (param instanceof PathParameter) {
+        pathParamNames.add(param.getName());
+      }
+    }
+    Set<String> missingPathParams = Sets.difference(ImmutableSet.copyOf(expectedPathParams), pathParamNames);
+    assertTrue("Expected path params for [" + path + "] are missing: " + missingPathParams, missingPathParams.isEmpty());
+  }
+
+}
+
+@Path("/toplevel")
+@Api(value = "Top Level", description = "A top level API")
+abstract class TopLevelAPI {
+
+  @GET
+  @Path("/top")
+  @ApiOperation(value = "list")
+  public abstract Response getList();
+
+  @Path("{param}/nested")
+  public abstract NestedAPI getNested(@ApiParam @PathParam(value = "param") String param);
+}
+
+@Path("/toplevel2")
+@Api(value = "Top Level 2", description = "Another top level API")
+abstract class AnotherTopLevelAPI {
+
+  @GET
+  @Path("/anotherTop")
+  @ApiOperation(value = "list")
+  public abstract Response getList();
+
+  @Path("{param}/anotherNested")
+  public abstract NestedAPI getSecondNested(@ApiParam @PathParam(value = "param") String param);
+
+}
+
+@Path("/toplevel3")
+@Api(value = "Top Level 3", description = "Yet another top level API")
+abstract class YetAnotherTopLevelAPI {
+
+  @GET
+  @Path("/yetAnotherTop")
+  @ApiOperation(value = "list")
+  public abstract Response getList();
+
+  @Path("{param}/nested")
+  public abstract NestedAPI getFirstNested(@ApiParam @PathParam(value = "param") String param);
+
+}
+
+@Api(value = "Nested", description = "A nested API")
+abstract class NestedAPI {
+
+  @GET
+  @Path("/list")
+  @ApiOperation(value = "list")
+  public abstract Response getList();
+
+}
+
+@Path("/canBeReachedFromTopToo")
+@Api(value = "Nested and Top Level", description = "An API that is both nested and top level")
+abstract class NestedAndTopLevelAPI {
+
+  @GET
+  @Path("/list")
+  @ApiOperation(value = "list")
+  public abstract Response getList();
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/03812cba/utility/src/test/resources/log4j.properties
----------------------------------------------------------------------
diff --git a/utility/src/test/resources/log4j.properties b/utility/src/test/resources/log4j.properties
new file mode 100644
index 0000000..c088bb7
--- /dev/null
+++ b/utility/src/test/resources/log4j.properties
@@ -0,0 +1,19 @@
+#   Licensed 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.
+
+# log4j configuration used during build and unit tests
+
+log4j.rootLogger=INFO,stdout
+log4j.threshold=ALL
+log4j.appender.stdout=org.apache.log4j.ConsoleAppender
+log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
+log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %-5p [%t] %c{2} (%F:%M(%L)) - %m%n