From 97e7e0b583b47440a6d27c02f893b5a90e8ffb6c Mon Sep 17 00:00:00 2001 From: rpidikiti Date: Mon, 8 Aug 2011 00:43:22 -0700 Subject: [PATCH] Fixed codege issues and complete testing framework implementation --- bin/generate-java-lib.sh | 16 + bin/test-java-lib.sh | 0 build.xml | 36 +- conf/java/sample/java_code_gen_conf.json | 27 + conf/java/sample/lib-test-data.json | 42 ++ conf/java/sample/lib-test-script.json | 265 +++++++++ .../wordnik/swagger/common/APIInvoker.java | 6 +- conf/java/templates/ModelObject.st | 3 +- conf/java/templates/ResourceObject.st | 16 +- ivy.xml | 2 +- .../swagger/codegen/LibraryCodeGenerator.java | 25 +- .../codegen/api/SwaggerResourceDocReader.java | 2 +- .../codegen/config/ApiConfiguration.java | 7 +- .../codegen/config/LanguageConfiguration.java | 2 +- .../common/CamelCaseNamingPolicyProvider.java | 7 +- .../java/JavaDataTypeMappingProvider.java | 9 +- .../codegen/config/java/JavaLibCodeGen.java | 41 +- .../exception/CodeGenerationException.java | 2 +- .../codegen/resource/EndpointOperation.java | 6 +- .../swagger/codegen/util/FileUtil.java | 18 +- .../runtime/annotations/AllowableValues.java | 35 ++ .../annotations/MethodArgumentNames.java | 29 + .../swagger/runtime/annotations/Required.java | 33 ++ .../swagger/runtime/common/APIInvoker.java | 273 +++++++++ .../ApiKeyAuthTokenBasedSecurityHandler.java | 67 +++ .../runtime/common/SecurityHandler.java | 47 ++ .../runtime/exception/APIException.java | 100 ++++ .../runtime/exception/APIExceptionCodes.java | 54 ++ .../swagger/testframework/APITestRunner.java | 545 ++++++++++++++++++ .../swagger/testframework/Assertion.java | 44 ++ .../testframework/JavaTestCaseExecutor.java | 192 ++++++ .../swagger/testframework/TestCase.java | 75 +++ .../swagger/testframework/TestOutput.java | 18 + .../swagger/testframework/TestPackage.java | 49 ++ .../swagger/testframework/TestResource.java | 55 ++ .../swagger/testframework/TestStatus.java | 165 ++++++ .../swagger/testframework/TestSuite.java | 45 ++ 37 files changed, 2300 insertions(+), 58 deletions(-) create mode 100755 bin/generate-java-lib.sh create mode 100755 bin/test-java-lib.sh create mode 100644 conf/java/sample/java_code_gen_conf.json create mode 100644 conf/java/sample/lib-test-data.json create mode 100644 conf/java/sample/lib-test-script.json rename src/main/java/com/wordnik/swagger/{ => codegen}/exception/CodeGenerationException.java (95%) create mode 100644 src/main/java/com/wordnik/swagger/runtime/annotations/AllowableValues.java create mode 100644 src/main/java/com/wordnik/swagger/runtime/annotations/MethodArgumentNames.java create mode 100644 src/main/java/com/wordnik/swagger/runtime/annotations/Required.java create mode 100644 src/main/java/com/wordnik/swagger/runtime/common/APIInvoker.java create mode 100644 src/main/java/com/wordnik/swagger/runtime/common/ApiKeyAuthTokenBasedSecurityHandler.java create mode 100644 src/main/java/com/wordnik/swagger/runtime/common/SecurityHandler.java create mode 100644 src/main/java/com/wordnik/swagger/runtime/exception/APIException.java create mode 100644 src/main/java/com/wordnik/swagger/runtime/exception/APIExceptionCodes.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/APITestRunner.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/Assertion.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/JavaTestCaseExecutor.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/TestCase.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/TestOutput.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/TestPackage.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/TestResource.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/TestStatus.java create mode 100644 src/main/java/com/wordnik/swagger/testframework/TestSuite.java diff --git a/bin/generate-java-lib.sh b/bin/generate-java-lib.sh new file mode 100755 index 00000000000..613480ff5d9 --- /dev/null +++ b/bin/generate-java-lib.sh @@ -0,0 +1,16 @@ +#!/bin/bash +echo "" > classpath.txt +for file in `ls lib`; + do echo -n 'lib/' >> classpath.txt; + echo -n $file >> classpath.txt; + echo -n ':' >> classpath.txt; +done +for file in `ls build`; + do echo -n 'build/' >> classpath.txt; + echo -n $file >> classpath.txt; + echo -n ':' >> classpath.txt; +done + +export CLASSPATH=$(cat classpath.txt):conf/java/templates +export JAVA_OPTS="${JAVA_OPTS} -DrulePath=data -Dproperty=Xmx2g -DloggerPath=$BUILD_COMMON/test-config/log4j.properties" +java $WORDNIK_OPTS $JAVA_CONFIG_OPTIONS $JAVA_OPTS -cp $CLASSPATH com.wordnik.swagger.codegen.config.java.JavaLibCodeGen "$@" \ No newline at end of file diff --git a/bin/test-java-lib.sh b/bin/test-java-lib.sh new file mode 100755 index 00000000000..e69de29bb2d diff --git a/build.xml b/build.xml index a9cb1c70230..2bc3490544f 100644 --- a/build.xml +++ b/build.xml @@ -31,15 +31,15 @@ - - - - - - - - - + + + + + + + + + @@ -80,7 +80,7 @@ - + Must specify the parameter for apiConfiguration eg. -DapiConfiguration==../api-server-lib/java/config/apiConfiguration.json @@ -99,5 +99,21 @@ + + + + + + + + + + + + + + + + diff --git a/conf/java/sample/java_code_gen_conf.json b/conf/java/sample/java_code_gen_conf.json new file mode 100644 index 00000000000..74db34b345c --- /dev/null +++ b/conf/java/sample/java_code_gen_conf.json @@ -0,0 +1,27 @@ +{ + "apiUrl":"http://localhost:8002/api/", + + "apiKey":"special-key", + + "defaultServiceBaseClass":"Object", + + "defaultModelBaseClass":"Object", + + "serviceBaseClasses":{}, + + "defaultModelImports":[], + + "defaultServiceImports":[], + + "modelPackageName":"com.wordnik.swagger.sample.sdk.java.model", + + "apiPackageName":"com.wordnik.swagger.sample.sdk.java.api", + + "ignoreMethods":[], + + "ignoreModels":[], + + "outputDirectory":"../swagger-sample-app/sdk-libs/src/main/java/com/wordnik/swagger/sample/sdk/java", + + "libraryHome":"../swagger-sample-app/sdk-libs" +} diff --git a/conf/java/sample/lib-test-data.json b/conf/java/sample/lib-test-data.json new file mode 100644 index 00000000000..68a5c2cebd5 --- /dev/null +++ b/conf/java/sample/lib-test-data.json @@ -0,0 +1,42 @@ +{ + "userList":[ + { + "userName":"testuser1", + "password":"password1", + "email":"test1@dummy.com" + }, + { + "userName":"testuser2", + "password":"password2", + "email":"test2@dummy.com" + } + ], + "petList":[ + { + "id":101, + "name":"pet1", + "photoUrls":["url1","url2"], + "tags":[ + { + "id":1, + "name":"tag1" + }, + { + "id":2, + "name":"tag2" + } + ], + "status":"available", + "category":{"id":1,"name":"cat1"} + } + ], + "orderList":[ + { + "id":101, + "petId":1, + "quantity":1, + "status":"placed", + "shipDate":13456789 + } + ] +} diff --git a/conf/java/sample/lib-test-script.json b/conf/java/sample/lib-test-script.json new file mode 100644 index 00000000000..c61bd06c0da --- /dev/null +++ b/conf/java/sample/lib-test-script.json @@ -0,0 +1,265 @@ +{ + "resources" : [ + { + "id" : 1, + "name" : "Find Per by Id", + "httpMethod" : "GET", + "path" : "/pet.{format}/{petId}", + "suggestedMethodName" : "getPetById" + }, + { + "id" : 2, + "name" : "Find pets by status", + "httpMethod" : "GET", + "path" : "/pet.{format}/findByStatus", + "suggestedMethodName" : "findPetsByStatus" + }, + { + "id" : 3, + "name" : "Find pets by tags", + "httpMethod" : "GET", + "path" : "/pet.{format}/findByTags", + "suggestedMethodName" : "findPetsByTags" + }, + { + "id" : 4, + "name" : "Add a pet", + "httpMethod" : "POST", + "path" : "/pet.{format}", + "suggestedMethodName" : "addPet" + }, + { + "id" : 5, + "name" : "Update a pet", + "httpMethod" : "PUT", + "path" : "/pet.{format}", + "suggestedMethodName" : "updatePet" + }, + { + "id" : 6, + "name" : "Create user", + "httpMethod" : "POST", + "path" : "/user.{format}", + "suggestedMethodName" : "createUser" + }, + { + "id" : 7, + "name" : "Update user", + "httpMethod" : "PUT", + "path" : "/user.{format}/{username}", + "suggestedMethodName" : "updateUser" + }, + { + "id" : 8, + "name" : "Delete user", + "httpMethod" : "DELETE", + "path" : "/user.{format}/{username}", + "suggestedMethodName" : "deleteUser" + }, + { + "id" : 9, + "name" : "Get user by user name", + "httpMethod" : "GET", + "path" : "/user.{format}/{username}", + "suggestedMethodName" : "getUserByName" + }, + { + "id" : 10, + "name" : "Login", + "httpMethod" : "GET", + "path" : "/user.{format}/login", + "suggestedMethodName" : "loginUser" + }, + { + "id" : 11, + "name" : "Logout", + "httpMethod" : "GET", + "path" : "/user.{format}/logout", + "suggestedMethodName" : "logoutUser" + }, + { + "id" : 12, + "name" : "Find order by id", + "httpMethod" : "GET", + "path" : "/store.{format}/order/{orderId}", + "suggestedMethodName" : "getOrderById" + }, + { + "id" : 13, + "name" : "Delete order by id", + "httpMethod" : "DELETE", + "path" : "/store.{format}/order/{orderId}", + "suggestedMethodName" : "deleteOrder" + }, + { + "id" : 14, + "name" : "Create order", + "httpMethod" : "POST", + "path" : "/store.{format}/order", + "suggestedMethodName" : "placeOrder" + } + ], + "testSuites" : [ + { + "id" : 1, + "name" : "Test User service related APIs", + "testCases" : [ + { + "name" : "Create User", + "id" : 1, + "resourceId" : 6, + "input" : { + "postData":"${input.userList[0]}" + }, + "assertions" : [ + { + "actualOutput" : "${output(1.1)}", + "condition" : "!=", + "expectedOutput" : "EXCEPTION" + } + ] + }, + { + "name" : "Login User", + "id" : 2, + "resourceId" : 10, + "input" : { + "username":"${input.userList[0].username}", + "password":"${input.userList[0].password}" + }, + "assertions" : [ + { + "actualOutput" : "${output(1.2)}", + "condition" : "!=", + "expectedOutput" : "EXCEPTION" + } + ] + }, + { + "name" : "Find user by name", + "id" : 3, + "resourceId" : 9, + "input" : { + "username":"${input.userList[0].username}" + }, + "assertions" : [ + { + "actualOutput" : "${output(1.3).username}", + "condition" : "!=", + "expectedOutput" : "${input.userList[0].username}" + } + ] + }, + { + "name" : "Delete user by name", + "id" : 4, + "resourceId" : 9, + "input" : { + "username":"${input.userList[0].username}" + }, + "assertions" : [ + { + "actualOutput" : "${output(1.4)}", + "condition" : "!=", + "expectedOutput" : "EXCEPTION" + } + ] + } + + + ] + }, + { + "id" : 2, + "name" : "Test Pet service related APIs", + "testCases" : [ + { + "name" : "Add pet", + "id" : 1, + "resourceId" : 4, + "input" : { + "postData":"${input.petList[0]}" + }, + "assertions" : [ + { + "actualOutput" : "${output(2.1)}", + "condition" : "!=", + "expectedOutput" : "EXCEPTION" + } + ] + }, + { + "name" : "Find pet by id", + "id" : 2, + "resourceId" : 1, + "input" : { + "petId":"1" + }, + "assertions" : [ + { + "actualOutput" : "${output(2.2)}", + "condition" : "!=", + "expectedOutput" : "NULL" + } + ] + }, + { + "name" : "Find pet by status", + "id" : 3, + "resourceId" : 2, + "input" : { + "status":"available,sold,pending" + }, + "assertions" : [ + { + "actualOutput" : "${output(2.3).size}", + "condition" : ">", + "expectedOutput" : "0" + } + ] + } + ] + }, + { + "id" : 3, + "name" : "Test Store service related APIs", + "testCases" : [ + { + "name" : "Find order by id", + "id" : 1, + "resourceId" : 12, + "input" : { + "orderId":"1" + }, + "assertions" : [ + { + "actualOutput" : "${output(3.1)}", + "condition" : "!=", + "expectedOutput" : "NULL" + } + ] + }, + { + "name" : "PLace order", + "id" : 2, + "resourceId" : 14, + "input" : { + "postData":"${input.orderList[0]}" + }, + "assertions" : [ + { + "actualOutput" : "${output(1.2)}", + "condition" : "!=", + "expectedOutput" : "EXCEPTION" + } + ] + } + ] + } + ] +} + + + + + \ No newline at end of file diff --git a/conf/java/structure/src/main/java/com/wordnik/swagger/common/APIInvoker.java b/conf/java/structure/src/main/java/com/wordnik/swagger/common/APIInvoker.java index a0b76b52f7d..6f2d5c4b2db 100644 --- a/conf/java/structure/src/main/java/com/wordnik/swagger/common/APIInvoker.java +++ b/conf/java/structure/src/main/java/com/wordnik/swagger/common/APIInvoker.java @@ -136,7 +136,7 @@ public class APIInvoker { * @throws com.wordnik.swagger.exception.APIException if the call to API server fails. */ public static String invokeAPI(String resourceURL, String method, Map queryParams, Object postObject) throws APIException { + String> queryParams, Object postData) throws APIException { Client apiClient = Client.create(); @@ -186,9 +186,9 @@ public class APIInvoker { if(method.equals(GET)) { clientResponse = builder.get(ClientResponse.class); }else if (method.equals(POST)) { - clientResponse = builder.post(ClientResponse.class, serialize(postObject)); + clientResponse = builder.post(ClientResponse.class, serialize(postData)); }else if (method.equals(PUT)) { - clientResponse = builder.put(ClientResponse.class, serialize(postObject)); + clientResponse = builder.put(ClientResponse.class, serialize(postData)); }else if (method.equals(DELETE)) { clientResponse = builder.delete(ClientResponse.class); } diff --git a/conf/java/templates/ModelObject.st b/conf/java/templates/ModelObject.st index 3b44e4eddbf..2ae7d2767f9 100644 --- a/conf/java/templates/ModelObject.st +++ b/conf/java/templates/ModelObject.st @@ -16,8 +16,7 @@ package $packageName$; -import $annotationPackageName$.AllowableValues; -import $annotationPackageName$.Required; +import com.wordnik.swagger.runtime.annotations.*; $imports:{ import | import $import$; diff --git a/conf/java/templates/ResourceObject.st b/conf/java/templates/ResourceObject.st index 70fa182c878..1a29d9cd1d0 100644 --- a/conf/java/templates/ResourceObject.st +++ b/conf/java/templates/ResourceObject.st @@ -17,17 +17,19 @@ package $packageName$; -import $annotationPackageName$.MethodArgumentNames; -import $exceptionPackageName$.APIExceptionCodes; -import $exceptionPackageName$.APIException; +//import $annotationPackageName$.MethodArgumentNames; +//import $exceptionPackageName$.APIExceptionCodes; +//import $exceptionPackageName$.APIException; + + import $modelPackageName$.*; import org.codehaus.jackson.map.DeserializationConfig.Feature; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.type.TypeReference; -import com.wordnik.swagger.annotations.*; -import com.wordnik.swagger.common.*; -import com.wordnik.swagger.exception.*; +import com.wordnik.swagger.runtime.annotations.*; +import com.wordnik.swagger.runtime.common.*; +import com.wordnik.swagger.runtime.exception.*; import java.util.*; import java.io.IOException; @@ -95,7 +97,7 @@ $method.pathParameters:{ argument | $endif$ //make the API Call $if(method.postObject)$ - String response = APIInvoker.invokeAPI(resourcePath, method, queryParams, postObject); + String response = APIInvoker.invokeAPI(resourcePath, method, queryParams, postData); $endif$ $if(!method.postObject)$ diff --git a/ivy.xml b/ivy.xml index 382fe8fc2ba..ea132b8cbcd 100644 --- a/ivy.xml +++ b/ivy.xml @@ -10,7 +10,7 @@ - + diff --git a/src/main/java/com/wordnik/swagger/codegen/LibraryCodeGenerator.java b/src/main/java/com/wordnik/swagger/codegen/LibraryCodeGenerator.java index 812cc6e1818..2be5e8bd32e 100644 --- a/src/main/java/com/wordnik/swagger/codegen/LibraryCodeGenerator.java +++ b/src/main/java/com/wordnik/swagger/codegen/LibraryCodeGenerator.java @@ -20,9 +20,9 @@ import com.wordnik.swagger.codegen.config.*; import com.wordnik.swagger.codegen.config.ApiConfiguration; import com.wordnik.swagger.codegen.config.common.CamelCaseNamingPolicyProvider; import com.wordnik.swagger.codegen.config.java.JavaDataTypeMappingProvider; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; import com.wordnik.swagger.codegen.resource.*; import com.wordnik.swagger.codegen.util.FileUtil; -import com.wordnik.swagger.exception.CodeGenerationException; import org.antlr.stringtemplate.StringTemplate; import org.antlr.stringtemplate.StringTemplateGroup; import org.codehaus.jackson.map.DeserializationConfig; @@ -70,6 +70,28 @@ public class LibraryCodeGenerator { this.setNameGenerator(new CamelCaseNamingPolicyProvider()); } + public LibraryCodeGenerator(String apiServerURL, String apiKey, String modelPackageName, String apiPackageName, String classOutputDir, String libraryHome){ + + final ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); + ApiConfiguration aApiConfiguration = new ApiConfiguration(); + aApiConfiguration.setApiKey(apiKey); + aApiConfiguration.setApiPackageName(apiPackageName); + aApiConfiguration.setModelPackageName(modelPackageName); + aApiConfiguration.setApiUrl(apiServerURL); + this.setApiConfig(aApiConfiguration); + CodeGenRulesProvider codeGenRules = new CodeGenRulesProvider(); + this.setCodeGenRulesProvider(codeGenRules); + LanguageConfiguration aLanguageConfiguration = new LanguageConfiguration(); + aLanguageConfiguration.setOutputDirectory(classOutputDir); + aLanguageConfiguration.setLibraryHome(libraryHome); + initializeLangConfig(aLanguageConfiguration); + this.setLanguageConfig(aLanguageConfiguration); + + this.setDataTypeMappingProvider(new JavaDataTypeMappingProvider()); + this.setNameGenerator(new CamelCaseNamingPolicyProvider()); + } + /** * Generate classes needed for the model and API invocation */ @@ -324,6 +346,7 @@ public class LibraryCodeGenerator { private void writeFile(File aFile, String content, String classType){ try{ + System.out.println("Writing to the file " + aFile.getAbsolutePath()); FileWriter aWriter = new FileWriter(aFile); BufferedWriter bufWriter = new BufferedWriter(aWriter); bufWriter.write(content); diff --git a/src/main/java/com/wordnik/swagger/codegen/api/SwaggerResourceDocReader.java b/src/main/java/com/wordnik/swagger/codegen/api/SwaggerResourceDocReader.java index 46c0e78e794..1f14861b7cf 100644 --- a/src/main/java/com/wordnik/swagger/codegen/api/SwaggerResourceDocReader.java +++ b/src/main/java/com/wordnik/swagger/codegen/api/SwaggerResourceDocReader.java @@ -20,11 +20,11 @@ import com.sun.jersey.api.client.Client; import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.WebResource; import com.wordnik.swagger.codegen.config.DataTypeMappingProvider; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; import com.wordnik.swagger.codegen.resource.Endpoint; import com.wordnik.swagger.codegen.resource.Resource; import com.wordnik.swagger.codegen.config.ApiConfiguration; import com.wordnik.swagger.codegen.config.NamingPolicyProvider; -import com.wordnik.swagger.exception.CodeGenerationException; import org.codehaus.jackson.map.DeserializationConfig; import org.codehaus.jackson.map.ObjectMapper; diff --git a/src/main/java/com/wordnik/swagger/codegen/config/ApiConfiguration.java b/src/main/java/com/wordnik/swagger/codegen/config/ApiConfiguration.java index 1e7b76ba72c..2809c7b95c2 100644 --- a/src/main/java/com/wordnik/swagger/codegen/config/ApiConfiguration.java +++ b/src/main/java/com/wordnik/swagger/codegen/config/ApiConfiguration.java @@ -16,8 +16,9 @@ package com.wordnik.swagger.codegen.config; -import com.wordnik.swagger.exception.CodeGenerationException; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -39,13 +40,13 @@ public class ApiConfiguration { * we may need to write custom classes and those classes will not be known to code generation. To import those * classes in service classes we use this property */ - private List defaultModelImports; + private List defaultModelImports = new ArrayList(); /** * Default service imports that we need to include in all service classes. This is needed because some times, * we may need to write custom classes ans those classes will not be known to code generation. To import those * classes in service classes we use this property */ - private List defaultServiceImports; + private List defaultServiceImports = new ArrayList(); private String modelPackageName; private String apiPackageName; diff --git a/src/main/java/com/wordnik/swagger/codegen/config/LanguageConfiguration.java b/src/main/java/com/wordnik/swagger/codegen/config/LanguageConfiguration.java index 6b6d535d045..57bdf721088 100644 --- a/src/main/java/com/wordnik/swagger/codegen/config/LanguageConfiguration.java +++ b/src/main/java/com/wordnik/swagger/codegen/config/LanguageConfiguration.java @@ -16,7 +16,7 @@ package com.wordnik.swagger.codegen.config; -import com.wordnik.swagger.exception.CodeGenerationException; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; /** * User: deepakmichael diff --git a/src/main/java/com/wordnik/swagger/codegen/config/common/CamelCaseNamingPolicyProvider.java b/src/main/java/com/wordnik/swagger/codegen/config/common/CamelCaseNamingPolicyProvider.java index b5b71f49d6f..42204c545a3 100644 --- a/src/main/java/com/wordnik/swagger/codegen/config/common/CamelCaseNamingPolicyProvider.java +++ b/src/main/java/com/wordnik/swagger/codegen/config/common/CamelCaseNamingPolicyProvider.java @@ -16,9 +16,8 @@ package com.wordnik.swagger.codegen.config.common; -import com.wordnik.swagger.codegen.resource.Model; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; import com.wordnik.swagger.codegen.config.NamingPolicyProvider; -import com.wordnik.swagger.exception.CodeGenerationException; /** * User: ramesh @@ -111,7 +110,7 @@ public class CamelCaseNamingPolicyProvider implements NamingPolicyProvider { /** * For input UserAPI and resource path /findUserById the suggested input object name will be: UserFindUserByIdInput * - * If the input path is /{userId}/delete the sugegsted name will be UserDeleteInput. The path parameters are ignored + * If the input path is /{userId}/delete the suggested name will be UserDeleteInput. The path parameters are ignored * in generating the input object name * * Note: Input objects are only created when the number of input arguments in a method exceeds certain number so
that the method signatures are clean @@ -123,7 +122,7 @@ public class CamelCaseNamingPolicyProvider implements NamingPolicyProvider { */ public String getInputObjectName(String serviceName, String resourcePath) { - //Since service name has API at the end remove that fromt he name + //Since service name has API at the end remove that format he name String inputobjectName = serviceName.substring(0, serviceName.length() - 3); String[] pathElements = resourcePath.split("/"); diff --git a/src/main/java/com/wordnik/swagger/codegen/config/java/JavaDataTypeMappingProvider.java b/src/main/java/com/wordnik/swagger/codegen/config/java/JavaDataTypeMappingProvider.java index 9dfa93a57dd..f34ae08d5d0 100644 --- a/src/main/java/com/wordnik/swagger/codegen/config/java/JavaDataTypeMappingProvider.java +++ b/src/main/java/com/wordnik/swagger/codegen/config/java/JavaDataTypeMappingProvider.java @@ -53,24 +53,29 @@ public class JavaDataTypeMappingProvider implements DataTypeMappingProvider { static{ primitiveObjectMap.put("string", "String"); primitiveObjectMap.put("String", "String"); + primitiveObjectMap.put("java.lang.String", "String"); primitiveObjectMap.put("int", "Integer"); primitiveObjectMap.put("integer", "Integer"); primitiveObjectMap.put("Integer", "Integer"); + primitiveObjectMap.put("java.lang.Integer", "Integer"); primitiveObjectMap.put("boolean", "Boolean"); primitiveObjectMap.put("Boolean", "Boolean"); + primitiveObjectMap.put("java.lang.Boolean", "Boolean"); primitiveObjectMap.put("long", "Long"); primitiveObjectMap.put("Long", "Long"); + primitiveObjectMap.put("java.lang.Long", "Long"); primitiveObjectMap.put("float", "Float"); primitiveObjectMap.put("Float", "Float"); + primitiveObjectMap.put("java.lang.Float", "Float"); primitiveObjectMap.put("Date", "Date"); primitiveObjectMap.put("date", "Date"); + primitiveObjectMap.put("java.util.Date", "Date"); } private NamingPolicyProvider nameGenerator = new CamelCaseNamingPolicyProvider(); public boolean isPrimitiveType(String type) { - if(type.equalsIgnoreCase("String") || type.equalsIgnoreCase("int") || type.equalsIgnoreCase("integer") || - type.equalsIgnoreCase("boolean") || type.equalsIgnoreCase("float")|| type.equalsIgnoreCase("long") ){ + if(primitiveObjectMap.containsKey(type)){ return true; } return false; diff --git a/src/main/java/com/wordnik/swagger/codegen/config/java/JavaLibCodeGen.java b/src/main/java/com/wordnik/swagger/codegen/config/java/JavaLibCodeGen.java index a88edfc43fc..2e634afd68b 100644 --- a/src/main/java/com/wordnik/swagger/codegen/config/java/JavaLibCodeGen.java +++ b/src/main/java/com/wordnik/swagger/codegen/config/java/JavaLibCodeGen.java @@ -17,17 +17,12 @@ package com.wordnik.swagger.codegen.config.java; import com.wordnik.swagger.codegen.LibraryCodeGenerator; -import com.wordnik.swagger.codegen.config.ApiConfiguration; -import com.wordnik.swagger.codegen.config.CodeGenRulesProvider; import com.wordnik.swagger.codegen.config.LanguageConfiguration; import com.wordnik.swagger.codegen.config.common.CamelCaseNamingPolicyProvider; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; import com.wordnik.swagger.codegen.util.FileUtil; -import com.wordnik.swagger.exception.CodeGenerationException; -import org.codehaus.jackson.map.DeserializationConfig; -import org.codehaus.jackson.map.ObjectMapper; import java.io.File; -import java.io.IOException; /** * User: ramesh @@ -40,9 +35,30 @@ public class JavaLibCodeGen extends LibraryCodeGenerator { if(args.length < 1){ throw new CodeGenerationException("Invalid number of arguments passed: No command line argument was passed to the program for config json"); } - String configPath = args[0]; - JavaLibCodeGen codeGenerator = new JavaLibCodeGen(configPath); - codeGenerator.generateCode(); + if(args.length == 1) { + String configPath = args[0]; + JavaLibCodeGen codeGenerator = new JavaLibCodeGen(configPath); + codeGenerator.generateCode(); + } + if(args.length == 6) { + String apiServerURL = args[0]; + String apiKey = args[1]; + String modelPackageName = args[2]; + String apiPackageName = args[3]; + String classOutputDir = args[4]; + String libraryHome = args[5]; + JavaLibCodeGen codeGenerator = new JavaLibCodeGen(apiServerURL, apiKey, modelPackageName, + apiPackageName, classOutputDir, libraryHome); + codeGenerator.generateCode(); + } + + } + + public JavaLibCodeGen(String apiServerURL, String apiKey, String modelPackageName, String apiPackageName, + String classOutputDir, String libraryHome){ + super(apiServerURL, apiKey, modelPackageName, apiPackageName, classOutputDir, libraryHome); + this.setDataTypeMappingProvider(new JavaDataTypeMappingProvider()); + this.setNameGenerator(new CamelCaseNamingPolicyProvider()); } public JavaLibCodeGen(String configPath){ @@ -62,10 +78,9 @@ public class JavaLibCodeGen extends LibraryCodeGenerator { //create ouput directories FileUtil.createOutputDirectories(javaConfiguration.getModelClassLocation(), javaConfiguration.getClassFileExtension()); FileUtil.createOutputDirectories(javaConfiguration.getResourceClassLocation(), javaConfiguration.getClassFileExtension()); - FileUtil.clearFolder(javaConfiguration.getLibraryHome() + "/src/main/java/com/wordnik/swagger/common"); - FileUtil.clearFolder(javaConfiguration.getLibraryHome() + "/src/main/java/com/wordnik/swagger/exception"); - FileUtil.clearFolder(javaConfiguration.getLibraryHome() + "/src/main/java/com/wordnik/swagger/annotations"); - FileUtil.copyDirectory(new File(javaConfiguration.getStructureLocation()), new File(javaConfiguration.getLibraryHome())); + FileUtil.clearFolder(javaConfiguration.getLibraryHome() + "/src/main/java/com/wordnik/swagger/runtime"); + FileUtil.createOutputDirectories(javaConfiguration.getLibraryHome() + "/src/main/java/com/wordnik/swagger/runtime", "java"); + FileUtil.copyDirectory(new File("src/main/java/com/wordnik/swagger/runtime"), new File(javaConfiguration.getLibraryHome()+"/src/main/java/com/wordnik/swagger/runtime")); return javaConfiguration; } diff --git a/src/main/java/com/wordnik/swagger/exception/CodeGenerationException.java b/src/main/java/com/wordnik/swagger/codegen/exception/CodeGenerationException.java similarity index 95% rename from src/main/java/com/wordnik/swagger/exception/CodeGenerationException.java rename to src/main/java/com/wordnik/swagger/codegen/exception/CodeGenerationException.java index 27cdb8e2241..1586ae4d500 100644 --- a/src/main/java/com/wordnik/swagger/exception/CodeGenerationException.java +++ b/src/main/java/com/wordnik/swagger/codegen/exception/CodeGenerationException.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.wordnik.swagger.exception; +package com.wordnik.swagger.codegen.exception; /** * Exception raised while generating code for java driver. diff --git a/src/main/java/com/wordnik/swagger/codegen/resource/EndpointOperation.java b/src/main/java/com/wordnik/swagger/codegen/resource/EndpointOperation.java index a3eb6613db8..761fd112faa 100644 --- a/src/main/java/com/wordnik/swagger/codegen/resource/EndpointOperation.java +++ b/src/main/java/com/wordnik/swagger/codegen/resource/EndpointOperation.java @@ -35,6 +35,8 @@ public class EndpointOperation { public static String PARAM_TYPE_PATH = "path"; public static String PARAM_TYPE_BODY = "body"; public static String PARAM_TYPE_HEADER = "header"; + public static String POST_PARAM_NAME = "postData"; + private static String AUTH_TOKEN_PARAM_NAME = "auth_token"; private static String API_KEY_PARAM_NAME = "api_key"; private static String FORMAT_PARAM_NAME = "format"; @@ -238,7 +240,7 @@ public class EndpointOperation { arguments.add(anArgument); }else if (modelField.getParamType().equalsIgnoreCase(PARAM_TYPE_BODY)) { if(modelField.getName() == null) { - modelField.setName("postObject"); + modelField.setName(POST_PARAM_NAME); } anArgument.setName(modelField.getName()); anArgument.setDataType(dataTypeMapper.getClassType(modelField.getDataType(), false)); @@ -263,7 +265,7 @@ public class EndpointOperation { modelforMethodInput.setName(inputobjectName); List fields = new ArrayList(); for(MethodArgument argument: method.getArguments()){ - if(!argument.getName().equals("postObject") && !argument.getName().equals("authToken")){ + if(!argument.getName().equals(POST_PARAM_NAME) && !argument.getName().equals("authToken")){ ModelField aModelField = new ModelField(); aModelField.setAllowedValues(argument.getAllowedValues()); aModelField.setDescription(argument.getDescription()); diff --git a/src/main/java/com/wordnik/swagger/codegen/util/FileUtil.java b/src/main/java/com/wordnik/swagger/codegen/util/FileUtil.java index 210323ea2d2..9726024af5d 100644 --- a/src/main/java/com/wordnik/swagger/codegen/util/FileUtil.java +++ b/src/main/java/com/wordnik/swagger/codegen/util/FileUtil.java @@ -16,7 +16,7 @@ package com.wordnik.swagger.codegen.util; -import com.wordnik.swagger.exception.CodeGenerationException; +import com.wordnik.swagger.codegen.exception.CodeGenerationException; import java.io.*; @@ -63,9 +63,11 @@ public class FileUtil { public static void clearFolder(String directoryLocation) { File fDir = new File(directoryLocation); File[] files = fDir.listFiles(); - for(File aFile : files) { - aFile.delete(); - } + if(files != null) { + for(File aFile : files) { + aFile.delete(); + } + } } // Clears the folder of the files with extension @@ -76,9 +78,10 @@ public class FileUtil { return (strName.endsWith(strExt)); } }); - - for (int i = 0; i < fLogs.length; i++) { - deleteFile(fLogs[i].getAbsolutePath()); + if(fLogs != null){ + for (int i = 0; i < fLogs.length; i++) { + deleteFile(fLogs[i].getAbsolutePath()); + } } } @@ -110,6 +113,7 @@ public class FileUtil { in.close(); out.close(); } catch (IOException e) { + e.printStackTrace(); throw new CodeGenerationException("Copy directory operation failed"); } } diff --git a/src/main/java/com/wordnik/swagger/runtime/annotations/AllowableValues.java b/src/main/java/com/wordnik/swagger/runtime/annotations/AllowableValues.java new file mode 100644 index 00000000000..e8dde8001e7 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/annotations/AllowableValues.java @@ -0,0 +1,35 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/** + * Annotation used to provide list of possible values + * @author ramesh + * + */ +@Target({ElementType.FIELD,ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface AllowableValues { + + String value() default ""; +} diff --git a/src/main/java/com/wordnik/swagger/runtime/annotations/MethodArgumentNames.java b/src/main/java/com/wordnik/swagger/runtime/annotations/MethodArgumentNames.java new file mode 100644 index 00000000000..29af03d089b --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/annotations/MethodArgumentNames.java @@ -0,0 +1,29 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Target({ElementType.FIELD,ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MethodArgumentNames { + String value() default ""; +} diff --git a/src/main/java/com/wordnik/swagger/runtime/annotations/Required.java b/src/main/java/com/wordnik/swagger/runtime/annotations/Required.java new file mode 100644 index 00000000000..bfdac9f7c0b --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/annotations/Required.java @@ -0,0 +1,33 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to indicate given property or field is required or not + * @author ramesh + * + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface Required { + +} diff --git a/src/main/java/com/wordnik/swagger/runtime/common/APIInvoker.java b/src/main/java/com/wordnik/swagger/runtime/common/APIInvoker.java new file mode 100644 index 00000000000..8669f3d6729 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/common/APIInvoker.java @@ -0,0 +1,273 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.common; + +import java.io.IOException; +import java.lang.String; +import java.util.Map; +import java.util.List; +import java.util.HashMap; +import java.util.logging.Logger; + +import javax.ws.rs.core.MultivaluedMap; + +import com.wordnik.swagger.runtime.exception.APIException; +import com.wordnik.swagger.runtime.exception.APIExceptionCodes; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.DeserializationConfig.Feature; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.type.TypeReference; + +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.WebResource.Builder; +import com.sun.jersey.api.client.filter.LoggingFilter; + + +/** + * Provides method to initialize the api server settings and also handles the logic related to invoking the API server + * along with serealizing and deserializing input and output responses. + * + * This is also a Base class for all API classes + * + * @author ramesh + * + */ +public class APIInvoker { + + private static String apiServer = "http://api.wordnik.com/v4"; + private static SecurityHandler securityHandler = null; + private static boolean loggingEnabled; + private static Logger logger = null; + + protected static String POST = "POST"; + protected static String GET = "GET"; + protected static String PUT = "PUT"; + protected static String DELETE = "DELETE"; + public static ObjectMapper mapper = new ObjectMapper(); + static{ + mapper.getDeserializationConfig().set(Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.getSerializationConfig().set(SerializationConfig.Feature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(SerializationConfig.Feature.WRITE_NULL_PROPERTIES, false); + mapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false); + } + + /** + * Initializes the API communication with required inputs. + * @param securityHandler security handler responsible for populating necessary security invocation while making API server calls + * @param apiServer Sets the URL for the API server. It is defaulted to the server + * used while building the driver. This value should be provided while testing the APIs against + * test servers or if there is any changes in production server URLs. + * @param enableLogging This will enable the logging using Jersey logging filter. Refer the following documentation + * for more details. {@link com.sun.jersey.api.client.filter.LoggingFilter}. Default output is sent to system.out. + * Create a logger ({@link java.util.logging.Logger} class and set using setLogger method. + */ + public static void initialize(SecurityHandler securityHandler, String apiServer, boolean enableLogging) { + setSecurityHandler(securityHandler); + if(apiServer != null && apiServer.length() > 0) { + if(apiServer.substring(apiServer.length()-1).equals("/")){ + apiServer = apiServer.substring(0, apiServer.length()-1); + } + setApiServer(apiServer); + } + loggingEnabled = enableLogging; + } + + /** + * Set the logger instance used for Jersey logging. + * @param aLogger + */ + public static void setLogger(Logger aLogger) { + logger = aLogger; + } + + /** + * Gets the API key used for server communication. + * This value is set using initialize method. + * @return + */ + public static SecurityHandler setSecurityHandler() { + return securityHandler; + } + + private static void setSecurityHandler(SecurityHandler aSecurityHandler) { + securityHandler = aSecurityHandler; + } + + /** + * Sets the URL for the API server. It is defaulted to the server used while building the driver. + * @return + */ + private static String getApiServer() { + return apiServer; + } + + public static void setApiServer(String server) { + apiServer = server; + } + + + + /** + * Invokes the API and returns the response as json string. + * + * This is an internal method called by individual APIs for communication. It sets the required security information + * based ons ecuroty handler + * + * @param resourceURL - URL for the rest resource + * @param method - Method we should use for communicating to the back end. + * @param postData - if the method is POST, provide the object that should be sent as part of post request. + * @return JSON response of the API call. + * @throws com.wordnik.swagger.runtime.exception.APIException if the call to API server fails. + */ + public static String invokeAPI(String resourceURL, String method, Map queryParams, Object postData) throws APIException { + + + Client apiClient = Client.create(); + + //check for app server values + if(getApiServer() == null || getApiServer().length() == 0) { + String[] args = {getApiServer()}; + throw new APIException(APIExceptionCodes.API_SERVER_NOT_VALID, args); + } + + //initialize the logger if needed + if(loggingEnabled) { + if(logger == null) { + apiClient.addFilter(new LoggingFilter()); + }else{ + apiClient.addFilter(new LoggingFilter(logger)); + } + } + + //make the communication + resourceURL = getApiServer() + resourceURL; + if(queryParams.keySet().size() > 0){ + int i=0; + for(String paramName : queryParams.keySet()){ + String symbol = "&"; + if(i==0){ + symbol = "?"; + } + resourceURL = resourceURL + symbol + paramName + "=" + queryParams.get(paramName); + i++; + } + } + Map headerMap = new HashMap(); + if(securityHandler != null){ + securityHandler.populateSecurityInfo(resourceURL, headerMap); + } + WebResource aResource = apiClient.resource(resourceURL); + + + //set the required HTTP headers + Builder builder = aResource.type("application/json"); + for(String key : headerMap.keySet()){ + builder.header(key, headerMap.get(key)); + } + + ClientResponse clientResponse = null; + if(method.equals(GET)) { + clientResponse = builder.get(ClientResponse.class); + }else if (method.equals(POST)) { + clientResponse = builder.post(ClientResponse.class, serialize(postData)); + }else if (method.equals(PUT)) { + clientResponse = builder.put(ClientResponse.class, serialize(postData)); + }else if (method.equals(DELETE)) { + clientResponse = builder.delete(ClientResponse.class); + } + + //process the response + if(clientResponse.getClientResponseStatus() == ClientResponse.Status.OK) { + String response = clientResponse.getEntity(String.class); + return response; + }else{ + int responseCode = clientResponse.getClientResponseStatus().getStatusCode() ; + throw new APIException(responseCode, clientResponse.getEntity(String.class)); + } + } + + /** + * De-serialize the object from String to object of type input class name. + * @param response + * @param inputClassName + * @return + */ + public static Object deserialize(String response, Class inputClassName) throws APIException { + try { + System.out.println("response: " + response + " , class name:" + inputClassName); + Object responseObject = mapper.readValue(response, inputClassName); + return responseObject; + } catch (IOException ioe) { + String[] args = new String[]{response, inputClassName.toString()}; + throw new APIException(APIExceptionCodes.ERROR_CONVERTING_JSON_TO_JAVA, args, "Error in coversting response json value to java object : " + ioe.getMessage(), ioe); + } + } + + + /** + * serialize the object from String to input object. + * @param input + * @return + */ + public static String serialize(Object input) throws APIException { + try { + if(input != null) { + return mapper.writeValueAsString(input); + }else{ + return ""; + } + } catch (IOException ioe) { + throw new APIException(APIExceptionCodes.ERROR_CONVERTING_JAVA_TO_JSON, "Error in coverting input java to json : " + ioe.getMessage(), ioe); + } + } + + + /** + * Overloaded method for returning the path value + * For a string value an empty value is returned if the value is null + * @param value + * @return + */ + public static String toPathValue(String value) { + return value == null ? "" : value; + } + + /** + * Overloaded method for returning a path value + * For a list of objects a comma separated string is returned + * @param objects + * @return + */ + public static String toPathValue(List objects) { + StringBuilder out = new StringBuilder(); + for(Object o: objects){ + out.append(o.toString()); + out.append(","); + } + if(out.indexOf(",") != -1) { + return out.substring(0, out.lastIndexOf(",") ); + } + return out.toString(); + } + + public static boolean isLoggingEnable() { + return loggingEnabled; + } +} diff --git a/src/main/java/com/wordnik/swagger/runtime/common/ApiKeyAuthTokenBasedSecurityHandler.java b/src/main/java/com/wordnik/swagger/runtime/common/ApiKeyAuthTokenBasedSecurityHandler.java new file mode 100644 index 00000000000..456f461715c --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/common/ApiKeyAuthTokenBasedSecurityHandler.java @@ -0,0 +1,67 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.common; + +import java.util.Map; + +/** + * User: ramesh + * Date: 8/4/11 + * Time: 6:39 PM + */ +public class ApiKeyAuthTokenBasedSecurityHandler implements SecurityHandler { + + private String apiKey = ""; + private String authToken = ""; + + public ApiKeyAuthTokenBasedSecurityHandler(String apiKey, String authToken) { + this.apiKey = apiKey; + this.authToken = authToken; + } + + public String getAuthToken() { + return authToken; + } + + public void setAuthToken(String authToken) { + this.authToken = authToken; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + /** + * Populate the security infomration in http headers map and/or reqsource URL. + * + * Value spopulated in the http headers map will be set as http headers while making the server communication. + * + * Depending on the usecase requried information can be added to either of them or both. + * + * @param resourceURL + * @param headers + */ + public void populateSecurityInfo(String resourceURL, Map httpHeaders) { + resourceURL = resourceURL + "api_key="+apiKey; + httpHeaders.put("api_key", apiKey); + httpHeaders.put("auth_token", authToken); + } +} diff --git a/src/main/java/com/wordnik/swagger/runtime/common/SecurityHandler.java b/src/main/java/com/wordnik/swagger/runtime/common/SecurityHandler.java new file mode 100644 index 00000000000..2ac10401806 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/common/SecurityHandler.java @@ -0,0 +1,47 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.common; + +import java.util.Map; + +/** + * Provide methods that are responsible for handling security aspect while communicating with the backend server. + * + * Example: For some cases API key may need to be passed in the headers for all server communication and some times + * user authentication token may need to be passed along with api key. + * + * Implementers of this class are responsible for handling storing information related to secutiry and sending it + * along with all API calls + * + * User: ramesh + * Date: 4/12/11 + * Time: 5:46 PM + */ +public interface SecurityHandler { + + /** + * Populate the security infomration in http headers map and/or reqsource URL. + * + * Value spopulated in the http headers map will be set as http headers while making the server communication. + * + * Depending on the usecase requried information can be added to either of them or both. + * + * @param resourceURL + * @param headers + */ + public void populateSecurityInfo(String resourceURL, Map httpHeaders); +} \ No newline at end of file diff --git a/src/main/java/com/wordnik/swagger/runtime/exception/APIException.java b/src/main/java/com/wordnik/swagger/runtime/exception/APIException.java new file mode 100644 index 00000000000..c7508d869f2 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/exception/APIException.java @@ -0,0 +1,100 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.exception; + +import com.sun.jersey.api.client.ClientResponse; +import org.codehaus.jackson.annotate.JsonAutoDetect; +import org.codehaus.jackson.annotate.JsonCreator; +import org.codehaus.jackson.annotate.JsonMethod; +import org.codehaus.jackson.annotate.JsonProperty; + +/** + * Exception that is thrown if there are any issues in invoking Wordnik API. + * + * Each exception carries a code and message. Code can be either HTTP error response code {@link com.sun.jersey.api.client.ClientResponse.Status} + * or The list of possible Wordnik exception code that are listed in the interface {@link APIExceptionCodes}. + * User: ramesh + * Date: 3/31/11 + * Time: 9:27 AM + */ +public class APIException extends Exception { + + private String message; + + private int code; + + private String[] args; + + @JsonCreator + public APIException() { + } + + public APIException(String message) { + super(message); + } + + public APIException(int code) { + this.code = code; + } + + public APIException(int code, String message, Throwable t) { + super(message, t); + this.message = message; + this.code = code; + } + + public APIException(int code, String[] args, String message, Throwable t) { + super(message, t); + this.message = message; + this.code = code; + this.args = args; + } + + public APIException(int code, String message) { + super(message); + this.message = message; + this.code = code; + } + + public APIException(int code, String[] args, String message) { + super(message); + this.message = message; + this.code = code; + this.args = args; + } + + public APIException(int code, String[] args) { + this.code = code; + this.args = args; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } +} diff --git a/src/main/java/com/wordnik/swagger/runtime/exception/APIExceptionCodes.java b/src/main/java/com/wordnik/swagger/runtime/exception/APIExceptionCodes.java new file mode 100644 index 00000000000..5c53af5aef0 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/runtime/exception/APIExceptionCodes.java @@ -0,0 +1,54 @@ +/** + * Copyright 2011 Wordnik, Inc. + * + * 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. + */ + +package com.wordnik.swagger.runtime.exception; + +/** + * Lists all the possible exception codes + * @author ramesh + * + */ +public interface APIExceptionCodes { + + /** + * System exception. + */ + public static final int SYSTEM_EXCEPTION = 0; + + /** + * With Arguments as current key. + */ + public static final int API_KEY_NOT_VALID = 1000; + /** + * With arguments as current token value + */ + public static final int AUTH_TOKEN_NOT_VALID = 1001; + /** + * With arguments as input JSON and output class anme + */ + public static final int ERROR_CONVERTING_JSON_TO_JAVA = 1002; + /** + * With arguments as JAVA class name + */ + public static final int ERROR_CONVERTING_JAVA_TO_JSON = 1003; + + public static final int ERROR_FROM_WEBSERVICE_CALL = 1004; + /** + * With arguments as current API server name + */ + public static final int API_SERVER_NOT_VALID = 1005; + +} diff --git a/src/main/java/com/wordnik/swagger/testframework/APITestRunner.java b/src/main/java/com/wordnik/swagger/testframework/APITestRunner.java new file mode 100644 index 00000000000..6e906365014 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/APITestRunner.java @@ -0,0 +1,545 @@ +package com.wordnik.swagger.testframework; + +import com.wordnik.swagger.codegen.config.common.CamelCaseNamingPolicyProvider; +import com.wordnik.swagger.runtime.common.APIInvoker; +import com.wordnik.swagger.runtime.common.ApiKeyAuthTokenBasedSecurityHandler; +import com.wordnik.swagger.runtime.common.SecurityHandler; +import com.wordnik.swagger.runtime.exception.APIException; +import org.apache.commons.beanutils.MethodUtils; +import org.apache.commons.beanutils.PropertyUtils; +import org.codehaus.jackson.map.DeserializationConfig.Feature; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; +import org.codehaus.jackson.map.type.TypeFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * Instance of this class is used to run the tests and assert the results based on + * JSON based test script. + * Created by IntelliJ IDEA. + * User: ramesh + * Date: 3/30/11 + * Time: 6:27 PM + */ +public class APITestRunner { + + private static String INPUT_DATA_EXPRESSION_PREFIX = "${input."; + private static String OUTPUT_DATA_EXPRESSION_PREFIX = "${output"; + public static String POST_PARAM_NAME = "postData"; + + private static String CONDITION_EQUAL = "=="; + private static String CONDITION_NOT_EQUAL = "!="; + private static String CONDITION_GREATER = ">"; + private static String CONDITION_LESSER = "<"; + private static String CONDITION_GREATER_EQUAL = ">="; + private static String CONDITION_LESSER_EQUAL = "<="; + + private TestOutput testCaseOutput = new TestOutput(); + private TestStatus testStatus = new TestStatus(); + private Object testData = null; + private TestPackage aPackage = null; + + private static String JAVA = "JAVA"; + private static String SCALA = "SCALA"; + private static String PYTHON = "PYTHON"; + private static String RUBY = "RUBY"; + private static String ANDROID = "ANDROID"; + private static String OBJECTIVE_C = "OBJECTIVE_C"; + private static String AS3 = "AS3"; + private static String NET = "NET"; + private static String PHP = "PHP"; + private static String HASKEL = "HASKEL"; + private static String CLOJURE = "CLOJURE"; + + private static ObjectMapper mapper = new ObjectMapper(); + static{ + mapper.getDeserializationConfig().set(Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationConfig.Feature.WRITE_NULL_PROPERTIES, false); + mapper.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false); + } + + private CamelCaseNamingPolicyProvider namingPolicyProvider = new CamelCaseNamingPolicyProvider(); + + /** + * Follow the following argument pattern + * + * Arg[0] --> api server URL + * Arg[1] --> api key + * Arg[2] --> test script file path + * Arg[3] --> test data file path + * Arg[4] --> test data class name (class to which test data file will be deserialized) + * Arg[5] --> package where API classes are available + * Arg[6] --> Language to execute test cases + * Arg[7] --> Optional test cases id. provide this if you need to execute only one test case + * + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception { + + String apiServer = args[0]; + String apiKey = args[1]; + String testScriptLocation = args[2]; + String testDataLocation = args[3]; + String testDataClass = args[4]; + String apiPackageName = args[5]; + String language = args[6]; + String suiteId = "0"; + if(args.length > 7){ + suiteId = args[7]; + } + + ApiKeyAuthTokenBasedSecurityHandler securityHandler = new ApiKeyAuthTokenBasedSecurityHandler(apiKey, ""); + APIInvoker.initialize(securityHandler, apiServer, true); + APITestRunner runner = new APITestRunner(); + runner.initialize(testScriptLocation, testDataLocation, testDataClass); + runner.runTests(apiServer, apiPackageName, runner.getTestPackage(), language, new Integer(suiteId), apiPackageName, securityHandler); + } + + public void initialize(String testScriptLocation, String testDataLocation, String testDataClass) throws Exception { + //load test script + File aFile = new File(testScriptLocation); + BufferedReader reader = new BufferedReader(new FileReader(aFile)); + StringBuilder builder = new StringBuilder(); + while(true){ + String line = reader.readLine(); + if(line == null){ + break; + }else{ + builder.append(line); + } + } + mapper.getDeserializationConfig().set(Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); + aPackage = (TestPackage) mapper.readValue(builder.toString(), TestPackage.class); + + //load test data + aFile = new File(testDataLocation); + reader = new BufferedReader(new FileReader(aFile)); + builder = new StringBuilder(); + while(true){ + String line = reader.readLine(); + if(line == null){ + break; + }else{ + builder.append(line); + } + } + testData = mapper.readValue(builder.toString(), Class.forName(testDataClass)); + reader.close(); + } + + + /** + * Gets the package that is initialized for testing + * @return + */ + public TestPackage getTestPackage() { + return aPackage; + } + + /*** + * Runs individual test cases in all test suites and stored the result in output object + * @param testPackage + */ + private void runTests(String apiServer, String apiPackageName, TestPackage testPackage, String language, int suiteId, + String libraryPackageName, ApiKeyAuthTokenBasedSecurityHandler securityHandler) throws Exception { + /** + * Logic: + * + * 1. Get each test case + * + * 2. based on the patch arrive at the method that needs to be executed + * + * 3. From the method get list of input parameters + * + * 4. Read input parameters based on input structure + * + * 5 Execute method by calling system command + * + * 6. Get the output + * + * 7. Store output in output object + * + * 8. execute assertions + * + * 9. Continue with next test case + */ + + Map resourceMap = new HashMap(); + for(TestResource resource: testPackage.getResources()){ + resourceMap.put(resource.getId(), resource); + } + + for(TestSuite suite : testPackage.getTestSuites()) { + if(suiteId != 0 && suiteId != suite.getId()){ + continue; + } + int testSuiteId = suite.getId(); + //1 + for(TestCase testCase : suite.getTestCases()){ + String testCasePath = testCasePath(testSuiteId , testCase.getId()); + + //2 + TestResource resource = resourceMap.get(testCase.getResourceId()); + String path = resource.getPath(); + String className = namingPolicyProvider.getServiceName(path); + String methodName = resource.getSuggestedMethodName(); + + //3 + Class apiClass = Class.forName(libraryPackageName +"." + className); + Method[] methods = apiClass.getMethods(); + Method methodToExecute = null; + for(Method method : methods){ + if(method.getName().equals(methodName)){ + methodToExecute = method; + break; + } + } + try { + if(methodToExecute != null) { + //4 + Map arguments = getArgumentsForMethodCall(methodToExecute, testCase); + + String authToken = "\"\""; + String postData = "\"\""; + StringBuilder queryPathParameters = new StringBuilder(); + for(String argName : arguments.keySet()){ + Object value = arguments.get(argName); + if(argName.equals("authToken")){ + authToken = value.toString(); + }else if (argName.equals(POST_PARAM_NAME)){ + postData = convertObjectToJSONString(value); + }else{ + if(queryPathParameters.toString().length()> 0){ + queryPathParameters.append("~"); + } + queryPathParameters.append(argName+"="+value.toString()); + } + } + + //get eternal command + String[] externalCommand = constructExternalCommand(apiServer, apiPackageName, + securityHandler.getApiKey(), authToken, + resource.getPath(), resource.getHttpMethod(), resource.getSuggestedMethodName(), + queryPathParameters.toString(), postData, language); + //print the command + System.out.println("Test Case :" + testCasePath); + for(String arg : externalCommand){ + System.out.print(arg + " "); + } + System.out.println(""); + //execute and get data + String outputString = executeExternalTestCaseAndGetResult(externalCommand); + Object output = null; + if(outputString != null && outputString.length() > 0) { + output = convertJSONStringToObject(outputString, methodToExecute.getReturnType()); + } + //6 + Class returnType = methodToExecute.getReturnType(); + if(!returnType.getName().equalsIgnoreCase("void")){ + //7 + testCaseOutput.getData().put(testCasePath, output); + } + //8 + //log it as passed, if there is any failures in assertions, assertions will update the status + //to failed + testStatus.logStatus(testCase, testCasePath, true); + executeAssertions(testCasePath, testCase); + } + }catch(Exception e){ + boolean asserted = false; + if(testCase.getAssertions() != null) { + for(Assertion assertion : testCase.getAssertions()){ + if(assertion.getExpectedOutput().equalsIgnoreCase("Exception")){ + testStatus.logStatus(testCase, testCasePath, true); + asserted = true; + } + } + } + if(!asserted){ + testStatus.logStatus(testCase, testCasePath, false, e.getMessage(), e); + } + } + } + } + System.out.println(testStatus.printTestStatus()); + } + + + /** + * Populate necessayr argument values tat needs ot be populated before calling the method + * @return + */ + protected Map getArgumentsForMethodCall(Method methodToExecute, TestCase testCase) throws Exception { + + Map queryPathParameters = new HashMap(); + if(testCase.getInput() != null) { + for(String inputParamName: testCase.getInput().keySet()){ + Object value = getParamValue(testCase.getInput().get(inputParamName)); + queryPathParameters.put(inputParamName, value); + } + } + return queryPathParameters; + } + + /** + * Execute all assertions in the test case. If there are nay failures test case will be amrked as failed + * and logged into test status object. + * @param testCase + */ + private void executeAssertions(String testCasePath, TestCase testCase) { + List assertions = testCase.getAssertions(); + if(assertions != null) { + for(Assertion assertion: assertions){ + try{ + Object actualOutPut = getParamValue(assertion.getActualOutput()); + Object expectedValue = getParamValue(assertion.getExpectedOutput()); + boolean failed = false; + if(assertion.getCondition().equals(CONDITION_EQUAL)){ + if(expectedValue.toString().equalsIgnoreCase("NULL") && actualOutPut == null){ + failed = false; + }else{ + if(expectedValue.getClass().isAssignableFrom(String.class)){ + actualOutPut = actualOutPut.toString(); + } + if(!actualOutPut.equals(expectedValue)){ + failed = true; + } + } + }else if(assertion.getCondition().equals(CONDITION_NOT_EQUAL)){ + if(expectedValue.toString().equalsIgnoreCase("EXCEPTION")){ + //this means user is not expecting any exception, output can be null, if we have reached + // here means there are no exceptions hence we can call the assertion is passed. + failed = false; + } + else if(actualOutPut == null || actualOutPut.equals(expectedValue)){ + failed = true; + } + }else{ + float actual = new Float(actualOutPut.toString()); + float expected = new Float(expectedValue.toString()); + if(assertion.getCondition().equals(CONDITION_GREATER)){ + if(!(actual > expected)){ + failed = true; + } + }else if(assertion.getCondition().equals(CONDITION_LESSER)){ + if(!(actual < expected)){ + failed = true; + } + }else if(assertion.getCondition().equals(CONDITION_LESSER_EQUAL)){ + if(!(actual <= expected)){ + failed = true; + } + }else if(assertion.getCondition().equals(CONDITION_GREATER_EQUAL)){ + if(!(actual >= expected)){ + failed = true; + } + } + } + if(failed) { + if(actualOutPut == null) { + testStatus.logAssertionStatus(testCasePath, false, expectedValue.toString(), null, assertion.getCondition()); + }else{ + testStatus.logAssertionStatus(testCasePath, false, expectedValue.toString(), actualOutPut.toString(), assertion.getCondition()); + } + } else{ + if(actualOutPut == null) { + testStatus.logAssertionStatus(testCasePath, true, expectedValue.toString(), "null", assertion.getCondition()); + }else{ + testStatus.logAssertionStatus(testCasePath, true, expectedValue.toString(), actualOutPut.toString(), assertion.getCondition()); + } + } + }catch(Exception e){ + e.printStackTrace(); + testStatus.logAssertionStatus(testCasePath, false, assertion.getExpectedOutput(), assertion.getActualOutput(), assertion.getCondition(), e); + } + } + } + } + + /** + * creates the test case unique path + * @param suiteId + * @param caseId + * @return + */ + private String testCasePath(int suiteId, int caseId) { + return suiteId + "." + caseId; + } + + /** + * Read the values based on expression. + * The expression can refer the value in input data structure or outputs generated from executing the current or previous + * test cases or direct input. The Test script follow the syntax of ${output if it is referring output data, + * ${input if it is referring input data. + * @param name + * @return + * @throws Exception + */ + private Object getParamValue(String name) throws Exception { + //this means we should use the input form previous steps or data file. + if(name.startsWith("$")){ + //sample:"${input.userList[0].username}" + if(name.startsWith(INPUT_DATA_EXPRESSION_PREFIX)){ + String expression = name.substring(INPUT_DATA_EXPRESSION_PREFIX.length(), name.length()-1); + boolean hasSize = false; + if(expression.endsWith("size")){ + expression = expression.substring(0, expression.length()-5); + hasSize = true; + } + Object value = PropertyUtils.getProperty(testData, expression); + if(hasSize){ + return MethodUtils.invokeMethod(value, "size", null); + } + + return value; + }else if(name.startsWith(OUTPUT_DATA_EXPRESSION_PREFIX)) { + //sample: ${output(1.1.1).token} + String expression = name.substring(OUTPUT_DATA_EXPRESSION_PREFIX.length(), name.length()-1); + expression = "data"+expression; + boolean hasSize = false; + if(expression.endsWith("size")){ + expression = expression.substring(0, expression.length()-5); + hasSize = true; + } + Object value = PropertyUtils.getProperty(testCaseOutput, expression); + if(hasSize){ + return MethodUtils.invokeMethod(value, "size", null); + } + return value; + }else{ + throw new RuntimeException("Input expression for parameter " + name + "is not as per valid syntax "); + } + }else{ + return name; + } + } + + /** + * Converts JSON string to object. + */ + public static Object convertJSONStringToObject(String inputJSON, Class objectType) throws Exception { + boolean isArray = false; + boolean isList = false; + Class className = objectType; + String ObjectTypeName = objectType.getName(); + + //identify if the input is a array + if(ObjectTypeName.startsWith("[")){ + isArray = true; + className = objectType.getComponentType(); + } + + //identify if the input is a list + if(List.class.isAssignableFrom(objectType)){ + isList = true; + } + + if(isArray || isList){ + Object responseObject = mapper.readValue(inputJSON, TypeFactory.type(objectType)); + return responseObject; + }else{ + return APIInvoker.deserialize(inputJSON, className); + } + } + + /** + * Converts JSON string to object. + */ + public static String convertObjectToJSONString(Object input) throws Exception { + return APIInvoker.serialize(input); + } + + /** + * Reads the test case results from standard out, converts that into java object and stores the value + * in test case output data so that the same can be used in subsequent test case execution + * + * First line fo response should be a status line with possible values as SUCCESS, ERROR + */ + private String executeExternalTestCaseAndGetResult(String[] command) throws Exception { + + Process p = Runtime.getRuntime().exec(command); + + BufferedReader stdInput = new BufferedReader(new + InputStreamReader(p.getInputStream())); + StringBuilder output = new StringBuilder(); + String s = null; + boolean isStatusLine = true; + String status = ""; + while ((s = stdInput.readLine()) != null) { + System.out.println(s); + if(isStatusLine){ + status = s; + if(status.equalsIgnoreCase("SUCCESS")||status.equalsIgnoreCase("OK") ) { + isStatusLine = false; + } + }else{ + output.append(s); + } + } + if(status.equalsIgnoreCase("SUCCESS")||status.equalsIgnoreCase("OK") ) { + return output.toString(); + }else{ + APIException exception = (APIException)convertJSONStringToObject(output.toString(), + APIException.class); + throw exception; + } + } + + + /** + * Get the java command line that needs to be executed for runnign a test case + */ + private String[] constructExternalCommand(String apiServer, String apiPackageName, String apiKey, String authToken, + String resource, String httpMethod, String suggestedMethodName, String queryAndPathParams, + String postData, String language) { + List command = new ArrayList(); + if(language.equals(JAVA)){ + command.add("./bin/runjava.sh"); + command.add("com.wordnik.swagger.testframework.JavaTestCaseExecutor"); + }else if (language.equals(PYTHON)){ + command.add("../python/runtest.py "); + }else if (language.equals(ANDROID)){ + command.add("../android/driver-test/bin/runandroid.sh"); + command.add("com.wordnik.swagger.testframework.JavaTestCaseExecutor"); + } + + command.addAll(getCommandInputs(apiServer, apiPackageName, apiKey, authToken, resource, httpMethod, + suggestedMethodName, queryAndPathParams, postData, + language)); + String[] commandArray = new String[command.size()]; + command.toArray(commandArray); + return commandArray; + } + + private List getCommandInputs(String apiServer, String apiPackageName, String apiKey, String authToken, String resource, String httpMethod, + String suggestedMethodName, String queryAndPathParams, String postData, + String language) { + List inputs = new ArrayList(); + inputs.add(apiServer); + inputs.add(apiPackageName); + inputs.add(apiKey); + inputs.add(authToken); + inputs.add(resource); + inputs.add(httpMethod); + inputs.add(suggestedMethodName); + inputs.add(queryAndPathParams); + if(postData.equals("\"\"")){ + inputs.add(postData); + }else{ + inputs.add(postData); + } + return inputs; + } + +} diff --git a/src/main/java/com/wordnik/swagger/testframework/Assertion.java b/src/main/java/com/wordnik/swagger/testframework/Assertion.java new file mode 100644 index 00000000000..3187e76c5fb --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/Assertion.java @@ -0,0 +1,44 @@ +package com.wordnik.swagger.testframework; + +public class Assertion { + + private String expectedOutput; + + private String actualOutputWithOutputWrapper; + + private String condition; + + private String actualOutput; + + public String getExpectedOutput() { + return expectedOutput; + } + + public void setExpectedOutput(String expression) { + this.expectedOutput = expression; + } + + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + public String getActualOutput() { + return actualOutput; + } + + public void setActualOutput(String value) { + this.actualOutput = value; + } + + public String getActualOutputWithOutputWrapper() { + return actualOutputWithOutputWrapper; + } + + public void setActualOutputWithOutputWrapper(String actualOutputWithOutputWrapper) { + this.actualOutputWithOutputWrapper = actualOutputWithOutputWrapper; + } +} diff --git a/src/main/java/com/wordnik/swagger/testframework/JavaTestCaseExecutor.java b/src/main/java/com/wordnik/swagger/testframework/JavaTestCaseExecutor.java new file mode 100644 index 00000000000..61021004799 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/JavaTestCaseExecutor.java @@ -0,0 +1,192 @@ +package com.wordnik.swagger.testframework; + +import com.wordnik.swagger.codegen.config.java.JavaDataTypeMappingProvider; +import com.wordnik.swagger.runtime.annotations.MethodArgumentNames; +import com.wordnik.swagger.codegen.config.common.CamelCaseNamingPolicyProvider; +import com.wordnik.swagger.runtime.common.APIInvoker; +import com.wordnik.swagger.runtime.common.ApiKeyAuthTokenBasedSecurityHandler; +import com.wordnik.swagger.runtime.exception.APIException; +import com.wordnik.swagger.runtime.exception.APIExceptionCodes; +import org.apache.commons.beanutils.BeanUtils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +/** + * Instance of this class runs single test case + * User: ramesh + * Date: 4/22/11 + * Time: 7:32 AM + */ +public class JavaTestCaseExecutor { + + private CamelCaseNamingPolicyProvider namingPolicyProvider = new CamelCaseNamingPolicyProvider(); + private JavaDataTypeMappingProvider datatypeMppingProvider = new JavaDataTypeMappingProvider(); + + /** + * Follow the following argument pattern + * Arguments in calling this method: + * ApiServerURL + * + * @param args + * @throws Exception + */ + public static void main(String[] args) throws Exception { + + + JavaTestCaseExecutor runner = new JavaTestCaseExecutor(); + String apiServer = args[0]; + String servicePackageName = args[1]; + String apiKey = args[2]; + String authToken = args[3]; + String resource = args[4]; + String httpMethod = args[5]; + String suggestedMethodName = args[6]; + Map queryAndPathParameters = new HashMap(); + String postData = null; + if(args.length > 7 && args[7].length() > 0){ + String[] qpTuple = args[7].split("~"); + for(String tuple: qpTuple){ + String[] nameValue = tuple.split("="); + queryAndPathParameters.put(nameValue[0], nameValue[1]); + } + } + if(args.length > 8 ){ + postData = args[8]; + } + queryAndPathParameters.put("authToken", authToken); + + ApiKeyAuthTokenBasedSecurityHandler securityHandler = new ApiKeyAuthTokenBasedSecurityHandler(apiKey, authToken); + APIInvoker.initialize(securityHandler, apiServer, true); + + runner.executeTestCase(resource, servicePackageName, suggestedMethodName, queryAndPathParameters, postData); + + } + + private void executeTestCase(String resourceName, String servicePackageName, String suggestedName, + Map queryAndPathParameters, String postData) { + + String className = namingPolicyProvider.getServiceName(resourceName); + String methodName = suggestedName; + //3 + try { + Class apiClass = Class.forName(servicePackageName + "." + className); + Method[] methods = apiClass.getMethods(); + Method methodToExecute = null; + for(Method method : methods){ + if(method.getName().equals(methodName)){ + methodToExecute = method; + break; + } + } + + if(methodToExecute != null) { + //4 + Object[] arguments = populateArgumentsForTestCaseExecution(methodToExecute, queryAndPathParameters, + postData, className, resourceName); + Object output = null; + if(arguments != null && arguments.length > 0){ + //5 + output = methodToExecute.invoke(null, arguments); + }else{ + //5 + output = methodToExecute.invoke(null); + } + //6 + System.out.println("SUCCESS"); + System.out.println(APITestRunner.convertObjectToJSONString(output)); + + } + }catch(APIException e){ + StringWriter sWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(sWriter); + e.printStackTrace(writer); + System.out.println(sWriter.getBuffer().toString()); + System.out.println(e.getMessage()); + System.out.println("ERROR"); + try{ + System.out.println(APITestRunner.convertObjectToJSONString(e)); + }catch(Exception ex){ + ex.printStackTrace(); + } + } catch(Exception e){ + StringWriter sWriter = new StringWriter(); + PrintWriter writer = new PrintWriter(sWriter); + e.printStackTrace(writer); + System.out.println(sWriter.getBuffer().toString()); + e.printStackTrace(); + System.out.println("ERROR"); + try{ + APIException apiException = new APIException(APIExceptionCodes.SYSTEM_EXCEPTION, + e.getMessage()); + System.out.println(APITestRunner.convertObjectToJSONString(apiException)); + }catch(Exception ex){ + ex.printStackTrace(); + } + } + } + + /** + * Gets the list of input query and path parameters and post data vlues and covenrt them to arguments that + * can be used for calling the method. This logic will be different in each driver language depends on how method + * input arguments are created. + */ + private Object[] populateArgumentsForTestCaseExecution(Method methodToExecute, Map queryAndPathParameters, + String postData, String serviceName, String resourcePath) throws Exception { + MethodArgumentNames argNames = methodToExecute.getAnnotation(MethodArgumentNames.class); + String[] argNamesArray = null; + if(argNames != null && argNames.value().length() > 0) { + argNamesArray = argNames.value().split(","); + } + Class[] argTypesArray = methodToExecute.getParameterTypes(); + Object output = null; + String inputClassName = namingPolicyProvider.getInputObjectName(serviceName, resourcePath); + + if(argNamesArray != null && argNamesArray.length > 0){ + Object[] arguments = new Object[argNamesArray.length]; + + for(int i=0; i < argNamesArray.length; i++){ + Object argument = null; + //if the method takes input model instead of individual arguments, convert individual arguments into input model object + if(argTypesArray[i].getName().equalsIgnoreCase(inputClassName)){ + argument = populateInputModelObject(argTypesArray[i], queryAndPathParameters); + }else if(datatypeMppingProvider.isPrimitiveType(argTypesArray[i].getName())){ + argument = queryAndPathParameters.get(argNamesArray[i].trim()); + }else if (argNamesArray[i].trim().equals(APITestRunner.POST_PARAM_NAME)){ + argument = APITestRunner.convertJSONStringToObject(postData, argTypesArray[i]); + }else{ + argument = APITestRunner.convertJSONStringToObject(queryAndPathParameters.get(argNamesArray[i].trim()), argTypesArray[i]); + } + arguments[i] = argument; + } + return arguments; + } + return null; + } + + /** + * Populates the swagger input model object. + * + * Input model is created when number of inputs to a method exceed certain limit. + * @param inputDefinitions + * @return + */ + private Object populateInputModelObject(Class swaggerInputClass, Map inputDefinitions) throws Exception { + Object object = swaggerInputClass.getConstructor().newInstance(); + Method[] methods = swaggerInputClass.getMethods(); + for(Method method : methods){ + if(method.getName().startsWith("get")){ + String methodName = method.getName(); + String fieldName = methodName.substring(3); + fieldName = namingPolicyProvider.applyMethodNamingPolicy(fieldName); + Object value = inputDefinitions.get(fieldName); + BeanUtils.setProperty(object, fieldName, value); + } + } + return object; + } + +} diff --git a/src/main/java/com/wordnik/swagger/testframework/TestCase.java b/src/main/java/com/wordnik/swagger/testframework/TestCase.java new file mode 100644 index 00000000000..02ea7d9ef5d --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/TestCase.java @@ -0,0 +1,75 @@ +package com.wordnik.swagger.testframework; + +import java.util.List; +import java.util.Map; + + +/** + * Test case executes individual test case. A given test suite has one or many test cases. + * @author ramesh + * + */ +public class TestCase { + + private String name; + + private int id; + + private int resourceId; + + private Map input; + + private String referenceInput; + + private List assertions; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getResourceId() { + return resourceId; + } + + public void setResourceId(int resourceId) { + this.resourceId = resourceId; + } + + public Map getInput() { + return input; + } + + public void setInput(Map input) { + this.input = input; + } + + public String getReferenceInput() { + return referenceInput; + } + + public void setReferenceInput(String referenceInput) { + this.referenceInput = referenceInput; + } + + public List getAssertions() { + return assertions; + } + + public void setAssertions(List assertions) { + this.assertions = assertions; + } + + +} diff --git a/src/main/java/com/wordnik/swagger/testframework/TestOutput.java b/src/main/java/com/wordnik/swagger/testframework/TestOutput.java new file mode 100644 index 00000000000..0c4973dca47 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/TestOutput.java @@ -0,0 +1,18 @@ +package com.wordnik.swagger.testframework; + +import java.util.HashMap; +import java.util.Map; + +public class TestOutput { + + Map data = new HashMap(); + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } + +} diff --git a/src/main/java/com/wordnik/swagger/testframework/TestPackage.java b/src/main/java/com/wordnik/swagger/testframework/TestPackage.java new file mode 100644 index 00000000000..8d34e6a1c13 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/TestPackage.java @@ -0,0 +1,49 @@ +package com.wordnik.swagger.testframework; + +import java.util.List; + +public class TestPackage { + + private List resources; + + private List testSuites; + + + public List getResources() { + return resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + + public List getTestSuites() { + return testSuites; + } + + public void setTestSuites(List testSuites) { + this.testSuites = testSuites; + } + + public TestCase getTestCaseById(int suiteId, int caseId) { + TestSuite aSuite = null; + TestCase aTestCase = null; + if(this.getTestSuites() != null){ + for(TestSuite suite : getTestSuites()){ + if(suite.getId() == suiteId){ + aSuite = suite; + break; + } + } + } + if(aSuite != null){ + for(TestCase testCase : aSuite.getTestCases()){ + if(testCase.getId() == caseId){ + aTestCase = testCase; + break; + } + } + } + return aTestCase; + } +} diff --git a/src/main/java/com/wordnik/swagger/testframework/TestResource.java b/src/main/java/com/wordnik/swagger/testframework/TestResource.java new file mode 100644 index 00000000000..303e6bde1a6 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/TestResource.java @@ -0,0 +1,55 @@ +package com.wordnik.swagger.testframework; + +public class TestResource { + + private int id; + + private String name; + + private String httpMethod; + + private String path; + + private String suggestedMethodName; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getHttpMethod() { + return httpMethod; + } + + public void setHttpMethod(String method) { + this.httpMethod = method; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getSuggestedMethodName() { + return suggestedMethodName; + } + + public void setSuggestedMethodName(String suggestedMethodName) { + this.suggestedMethodName = suggestedMethodName; + } + +} diff --git a/src/main/java/com/wordnik/swagger/testframework/TestStatus.java b/src/main/java/com/wordnik/swagger/testframework/TestStatus.java new file mode 100644 index 00000000000..5708f013034 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/TestStatus.java @@ -0,0 +1,165 @@ +package com.wordnik.swagger.testframework; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Stores the status of the tests executed. + * @author ramesh + * + */ +public class TestStatus { + + private int totalTestCaseCount; + + private int failedTestCaseCount; + + private List testCaseStatuses = new ArrayList(); + private Map testCaseStatusMap = new HashMap(); + + public void logStatus(TestCase testCase, String testCasePath, boolean passed){ + logStatus(testCase, testCasePath, passed, null, null); + } + + public void logStatus(TestCase testCase, String testCasePath, boolean passed, String failureDescription){ + logStatus(testCase, testCasePath, passed, failureDescription, null); + } + + public void logStatus(TestCase testCase, String testCasePath, boolean passed, String failureDescription, Throwable exception){ + TestCaseStatus status = new TestCaseStatus(testCase, testCasePath, passed, failureDescription, exception); + testCaseStatuses.add(status); + testCaseStatusMap.put(testCasePath, status); + totalTestCaseCount++; + if(!passed){ + failedTestCaseCount++; + } + + } + + public void logAssertionStatus(String path, boolean passed, String expectedValue, String actualValue, String condition) { + logAssertionStatus(path, passed, expectedValue, actualValue, condition, null); + } + + public void logAssertionStatus(String path, boolean passed, String expectedValue, String actualValue, String condition, Throwable stack) { + TestCaseStatus status = testCaseStatusMap.get(path); + if(!passed && status.passed) { + status.passed = false; + failedTestCaseCount++; + } + status.addAssertionStatus(passed, expectedValue, actualValue, condition, stack); + } + + public String printTestStatus() { + StringBuilder output = new StringBuilder(); + output.append("Summary --> Total Test Cases: " + totalTestCaseCount + " Failed Test Cases: " + failedTestCaseCount); + output.append("\nDetails: \n"); + for(TestCaseStatus status : testCaseStatuses) { + output.append(status.getStatusDescription() + "\n"); + } + return output.toString(); + } + + /** + * Indicates if there are any failured in executing the test cases. + * @return + */ + public boolean hasFailures() { + return (failedTestCaseCount > 0); + } + + /** + * Inner class Stores the details on individual test cases. + * @author ramesh + * + */ + private class TestCaseStatus { + + String testCasePath; + + boolean passed; + + String failureDescription; + + Throwable exceptionStack; + + TestCase testCase; + + List assertionStatuses = new ArrayList(); + + public TestCaseStatus(TestCase testCase, String testCasePath, boolean passed, String failureDescription, + Throwable exceptionStack) { + this.exceptionStack = exceptionStack; + this.testCase = testCase; + this.passed = passed; + this.failureDescription = failureDescription; + this.testCasePath = testCasePath; + } + + public void addAssertionStatus(boolean passed, String expectedValue, String actualValue, String condition, Throwable stack) { + String assertionDesc = expectedValue + " " + condition + " " + actualValue; + AssertionStatus status = new AssertionStatus(assertionDesc, passed, stack); + assertionStatuses.add(status); + if(!passed){ + this.passed = false; + } + } + + public String getStatusDescription(){ + StringBuilder output = new StringBuilder(); + if(passed){ + output.append(testCasePath); + output.append(" : "); + output.append(testCase.getName()); + output.append(" : "); + output.append(" passed "); + output.append(" \n "); + }else{ + output.append(testCasePath); + output.append(" : "); + output.append(testCase.getName()); + output.append(" : "); + output.append(" failed "); + output.append(" \n "); + if ( failureDescription != null) { + output.append(" failure message : "); + output.append(failureDescription + "\n"); + } + if(exceptionStack != null){ + StringWriter out = new StringWriter(); + PrintWriter writer = new PrintWriter(out); + exceptionStack.printStackTrace(writer); + output.append(out.toString()); + } + for(AssertionStatus status:assertionStatuses){ + if(!status.passed) { + output.append(status.getAssertionDescription()+"\n"); + } + } + } + return output.toString(); + } + } + + private class AssertionStatus { + + String assertionDescription; + + boolean passed; + + Throwable exceptionStack; + + public AssertionStatus(String description, boolean passed, Throwable stack){ + this.assertionDescription = description; + this.passed = passed; + this.exceptionStack = stack; + } + + public String getAssertionDescription() { + return assertionDescription + " : " + (passed ? "Passed " : " Failed ") + ((exceptionStack !=null) ? exceptionStack.getMessage() : "" ); + } + } +} diff --git a/src/main/java/com/wordnik/swagger/testframework/TestSuite.java b/src/main/java/com/wordnik/swagger/testframework/TestSuite.java new file mode 100644 index 00000000000..96357cb1e24 --- /dev/null +++ b/src/main/java/com/wordnik/swagger/testframework/TestSuite.java @@ -0,0 +1,45 @@ +package com.wordnik.swagger.testframework; + +import java.util.List; + +/** + * Test suite contains collection of test cases. + * + * As a best practice write one test suite for each resource + * @author ramesh + * + */ +public class TestSuite { + + private String name ; + + private int id; + + private List testCases; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public List getTestCases() { + return testCases; + } + + public void setTestCases(List testCases) { + this.testCases = testCases; + } + + +}