diff --git a/codegen-cli/src/main/resources/config/service.yml b/codegen-cli/src/main/resources/config/service.yml index c7a93c163..b70bc2fd1 100644 --- a/codegen-cli/src/main/resources/config/service.yml +++ b/codegen-cli/src/main/resources/config/service.yml @@ -1,55 +1,5 @@ # Singleton service factory configuration/IoC injection singletons: -# singleton service factory configuration -singletons: -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.CallbackValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.ContactValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.EncodingPropertyValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.ExampleValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.ExternalDocsValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.HeaderValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.LicenseValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.InfoValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.LinkValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.SchemaValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.MediaTypeValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.OAuthFlowValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.OpenApi3Validator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.RequestBodyValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.OperationValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.ParameterValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.PathValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.ResponseValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.SecurityRequirementValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.SecuritySchemeValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.ServerValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.ServerVariableValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.TagValidator -- com.networknt.oas.validator.Validator: - - com.networknt.oas.validator.impl.XmlValidator # Generator interface implementations - com.networknt.codegen.Generator: - com.networknt.codegen.rest.OpenApiGenerator @@ -58,3 +8,4 @@ singletons: - com.networknt.codegen.hybrid.HybridServiceGenerator - com.networknt.codegen.graphql.GraphqlGenerator - com.networknt.codegen.eventuate.EventuateOpenApiGenerator + - com.networknt.codegen.rest.OpenApiKotlinGenerator diff --git a/codegen-core/src/main/java/com/networknt/codegen/Generator.java b/codegen-core/src/main/java/com/networknt/codegen/Generator.java index 74ba59a6b..7d2733f2e 100644 --- a/codegen-core/src/main/java/com/networknt/codegen/Generator.java +++ b/codegen-core/src/main/java/com/networknt/codegen/Generator.java @@ -92,6 +92,7 @@ default void transfer(String folder, String path, String filename, DefaultRocker * @param path Current file path in the output folder * @param filename Current filename in the output folder * @throws IOException throws IOException + * @return boolean if the path exists or not */ default boolean checkExist(String folder, String path, String filename) throws IOException { String absPath = folder + (path.isEmpty()? "" : separator + path) + separator + filename; diff --git a/codegen-fwk/src/test/resources/config/service.yml b/codegen-fwk/src/test/resources/config/service.yml index eecf790e8..b70bc2fd1 100644 --- a/codegen-fwk/src/test/resources/config/service.yml +++ b/codegen-fwk/src/test/resources/config/service.yml @@ -8,4 +8,4 @@ singletons: - com.networknt.codegen.hybrid.HybridServiceGenerator - com.networknt.codegen.graphql.GraphqlGenerator - com.networknt.codegen.eventuate.EventuateOpenApiGenerator - + - com.networknt.codegen.rest.OpenApiKotlinGenerator diff --git a/codegen-web/src/main/resources/config/service.yml b/codegen-web/src/main/resources/config/service.yml index 5ac7c863e..3f983b9ba 100644 --- a/codegen-web/src/main/resources/config/service.yml +++ b/codegen-web/src/main/resources/config/service.yml @@ -32,6 +32,6 @@ singletons: - com.networknt.codegen.hybrid.HybridServerGenerator - com.networknt.codegen.hybrid.HybridServiceGenerator - com.networknt.codegen.graphql.GraphqlGenerator - + - com.networknt.codegen.rest.OpenApiKotlinGenerator - com.networknt.resources.PathResourceProvider: - com.networknt.codegen.handler.CodegenResourceProvider diff --git a/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiKotlinGenerator.java b/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiKotlinGenerator.java new file mode 100644 index 000000000..47a3a5a17 --- /dev/null +++ b/light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiKotlinGenerator.java @@ -0,0 +1,598 @@ +package com.networknt.codegen.rest; + +import com.jsoniter.JsonIterator; +import com.jsoniter.ValueType; +import com.jsoniter.any.Any; +import com.jsoniter.output.JsonStream; +import com.networknt.codegen.Generator; +import com.networknt.codegen.Utils; +import com.networknt.jsonoverlay.Overlay; +import com.networknt.oas.OpenApiParser; +import com.networknt.oas.model.*; +import com.networknt.oas.model.impl.OpenApi3Impl; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.*; +import java.util.stream.Collectors; + +import static java.io.File.separator; + +/** + * The input for OpenAPI 3.0 generator include config with json format + * and OpenAPI specification in yaml format. + * + * The model is OpenAPI spec in yaml format. And config file is config.json + * in JSON format. + * + * @author Steve Hu + */ +public class OpenApiKotlinGenerator implements Generator { + private Map typeMapping = new HashMap<>(); + + // optional generation parameters. if not set, they use default values as + boolean prometheusMetrics =false; + boolean skipHealthCheck = false; + boolean skipServerInfo = false; + boolean regenerateCodeOnly = false; + boolean enableParamDescription = true; + boolean generateModelOnly = false; + boolean generateValuesYml = false; + + public OpenApiKotlinGenerator() { + typeMapping.put("array", "java.util.List"); + typeMapping.put("map", "java.util.Map"); + typeMapping.put("List", "java.util.List"); + typeMapping.put("boolean", "Boolean"); + typeMapping.put("string", "String"); + typeMapping.put("int", "Integer"); + typeMapping.put("float", "Float"); + typeMapping.put("number", "java.math.BigDecimal"); + typeMapping.put("DateTime", "Date"); + typeMapping.put("long", "Long"); + typeMapping.put("short", "Short"); + typeMapping.put("char", "String"); + typeMapping.put("double", "Double"); + typeMapping.put("object", "Object"); + typeMapping.put("integer", "Integer"); + typeMapping.put("ByteArray", "byte[]"); + typeMapping.put("binary", "byte[]"); + } + + @Override + public String getFramework() { + return "openapikotlin"; + } + + /** + * + * @param targetPath The output directory of the generated project + * @param model The optional model data that trigger the generation, i.e. swagger specification, graphql IDL etc. + * @param config A json object that controls how the generator behaves. + * @throws IOException IO Exception occurs during code generation + */ + @Override + public void generate(String targetPath, Object model, Any config) throws IOException { + // whoever is calling this needs to make sure that model is converted to Map + String rootPackage = config.toString("rootPackage").trim(); + String modelPackage = config.toString("modelPackage").trim(); + String handlerPackage = config.toString("handlerPackage").trim(); + + boolean overwriteHandler = config.toBoolean("overwriteHandler"); + boolean overwriteHandlerTest = config.toBoolean("overwriteHandlerTest"); + boolean overwriteModel = config.toBoolean("overwriteModel"); + generateModelOnly = config.toBoolean("generateModelOnly"); + + boolean enableHttp = config.toBoolean("enableHttp"); + String httpPort = config.toString("httpPort").trim(); + boolean enableHttps = config.toBoolean("enableHttps"); + String httpsPort = config.toString("httpsPort").trim(); + + boolean enableRegistry = config.toBoolean("enableRegistry"); + boolean supportClient = config.toBoolean("supportClient"); + String dockerOrganization = config.toString("dockerOrganization").trim(); + + prometheusMetrics = config.toBoolean("prometheusMetrics"); + skipHealthCheck = config.toBoolean("skipHealthCheck"); + skipServerInfo = config.toBoolean("skipServerInfo"); + regenerateCodeOnly = config.toBoolean("specChangeCodeReGenOnly"); + enableParamDescription = config.toBoolean("enableParamDescription"); + + generateValuesYml = config.toBoolean("generateValuesYml"); + + String version = config.toString("version").trim(); + String serviceId = config.get("groupId").toString().trim() + "." + config.get("artifactId").toString().trim() + "-" + config.get("version").toString().trim(); + + if(dockerOrganization == null || dockerOrganization.length() == 0) dockerOrganization = "networknt"; + + // get the list of operations for this model + List> operationList = getOperationList(model); + + // bypass project generation if the mode is the only one requested to be built + if(!generateModelOnly) { + // if set to true, regenerate the code only (handlers, model and the handler.yml, potentially affected by operation changes + if (!regenerateCodeOnly) { + // generate configurations, project, masks, certs, etc + // There is only one port that should be exposed in Dockerfile, otherwise, the service + // discovery will be so confused. If https is enabled, expose the https port. Otherwise http port. + String expose = ""; + if(enableHttps) { + expose = httpsPort; + } else { + expose = httpPort; + } + + transfer(targetPath, "docker", "Dockerfile", templates.restkotlin.dockerfile.template(config, expose)); + transfer(targetPath, "docker", "Dockerfile-Redhat", templates.restkotlin.dockerfileredhat.template(config, expose)); + transfer(targetPath, "", "build.sh", templates.restkotlin.buildSh.template(dockerOrganization, serviceId)); + transfer(targetPath, "", ".gitignore", templates.restkotlin.gitignore.template()); + transfer(targetPath, "", "README.md", templates.restkotlin.README.template()); + transfer(targetPath, "", "LICENSE", templates.restkotlin.LICENSE.template()); + transfer(targetPath, "", "build.gradle.kts", templates.restkotlin.buildGradleKts.template(config)); + transfer(targetPath, "", "gradle.properties", templates.restkotlin.gradleProperties.template(config)); + transfer(targetPath, "", "gradlew", templates.restkotlin.gradlew.template()); + transfer(targetPath, "", "gradlew.bat", templates.restkotlin.gradlewBat.template()); + transfer(targetPath, "", "settings.gradle.kts", templates.restkotlin.settingsGradleKts.template(config)); + + transfer(targetPath, "gradle" + separator + "wrapper", "gradle-wrapper.properties", templates.restkotlin.gradleWrapperProperties.template()); + try (InputStream is = OpenApiGenerator.class.getResourceAsStream("/binaries/gradle-wrapper.jar")) { + Files.copy(is, Paths.get(targetPath, ("gradle.wrapper").replace(".", separator), "gradle-wrapper.jar"), StandardCopyOption.REPLACE_EXISTING); + } + + // config + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "service.yml", templates.restkotlin.openapi.service.template(config)); + + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "server.yml", templates.restkotlin.server.template(serviceId, enableHttp, httpPort, enableHttps, httpsPort, enableRegistry, version)); + transfer(targetPath, ("src.test.resources.config").replace(".", separator), "server.yml", templates.restkotlin.server.template(serviceId, enableHttp, "49587", enableHttps, "49588", enableRegistry, version)); + + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "secret.yml", templates.restkotlin.secret.template()); + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "openapi-security.yml", templates.restkotlin.openapiSecurity.template()); + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "openapi-validator.yml", templates.restkotlin.openapiValidator.template()); + if(supportClient) { + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "client.yml", templates.restkotlin.clientYml.template()); + } else { + transfer(targetPath, ("src.test.resources.config").replace(".", separator), "client.yml", templates.restkotlin.clientYml.template()); + } + + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "primary.crt", templates.restkotlin.primaryCrt.template()); + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "secondary.crt", templates.restkotlin.secondaryCrt.template()); + + // mask + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "mask.yml", templates.restkotlin.maskYml.template()); + // logging + transfer(targetPath, ("src.main.resources").replace(".", separator), "logback.xml", templates.restkotlin.logback.template()); + transfer(targetPath, ("src.test.resources").replace(".", separator), "logback-test.xml", templates.restkotlin.logback.template()); + transfer(targetPath, ("src.test.resources").replace(".", separator), "junit-platform.properties", templates.restkotlin.junitPlatformProperties.template()); + + // routing handler + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "handler.yml", templates.restkotlin.openapi.handlerYml.template(serviceId, handlerPackage, operationList, prometheusMetrics, !skipHealthCheck, !skipServerInfo)); + + // exclusion list for Config module + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "config.yml", templates.restkotlin.openapi.config.template()); + + // values.yml file, transfer only if explicitly set in the config.json + if(generateValuesYml) + transfer(targetPath, ("src.main.resources.config").replace(".", separator), "values.yml", templates.restkotlin.openapi.values.template()); + } + } + + // model + Any anyComponents; + if(model instanceof Any) { + anyComponents = ((Any)model).get("components"); + } else if(model instanceof String){ + // this must be yaml format and we need to convert to json for JsonIterator. + OpenApi3 openApi3 = null; + try { + openApi3 = (OpenApi3) new OpenApiParser().parse((String)model, new URL("https://oas.lightapi.net/")); + } catch (MalformedURLException e) { + throw new RuntimeException("Failed to parse the model", e); + } + anyComponents = JsonIterator.deserialize(Overlay.toJson((OpenApi3Impl)openApi3).toString()).get("components"); + } else { + throw new RuntimeException("Invalid Model Class: " + model.getClass()); + } + if(anyComponents.valueType() != ValueType.INVALID) { + Any schemas = anyComponents.asMap().get("schemas"); + if(schemas != null && schemas.valueType() != ValueType.INVALID) { + for(Map.Entry entry : schemas.asMap().entrySet()) { + List> props = new ArrayList<>(); + String key = entry.getKey(); + Map value = entry.getValue().asMap(); + String type = null; + String enums = null; + boolean isEnum = false; + boolean isEnumClass = false; + // Map properties = null; + List required = null; + + // iterate through each schema in the components + for(Map.Entry entrySchema: value.entrySet()) { + if("type".equals(entrySchema.getKey())) { + type = entrySchema.getValue().toString(); + if("enum".equals(type)) isEnum = true; + } + if("enum".equals(entrySchema.getKey())) { + isEnumClass = true; + enums = entrySchema.getValue().asList().toString(); + enums = enums.substring(enums.indexOf("[") + 1, enums.indexOf("]")); + } + if("properties".equals(entrySchema.getKey())) { + handleProperties(props, entrySchema.getValue().asMap()); + } + if("required".equals(entrySchema.getKey())) { + required = entrySchema.getValue().asList(); + } + if("allOf".equals(entrySchema.getKey())) { + type = "object"; + + // could be referred to as "$ref" references or listed in "properties" + for(Any listItem : entrySchema.getValue().asList()) { + //Map allOfItem = (Map)listItem.asMap().entrySet(); + + for(Map.Entry allOfItem : ((Map)listItem.asMap()).entrySet()) { + if("$ref".equals(allOfItem.getKey())) { + String s = allOfItem.getValue().toString(); + s = s.substring(s.lastIndexOf('/') + 1); + handleProperties(props, schemas.get(s).get("properties").asMap()); + } + if("properties".equals(allOfItem.getKey())) { + handleProperties(props, allOfItem.getValue().asMap()); + } + } + } + } + } + String classVarName = key; + String modelFileName = key.substring(0, 1).toUpperCase() + key.substring(1); + //System.out.println("props = " + Any.wrap(props)); + + // Check the type of current schema. Generation will be executed only if the type of the schema equals to object. + // Since generate a model for primitive types and arrays do not make sense, and an error class would be generated + // due to lack of properties if force to generate. + if (!"object".equals(type)) { + continue; + } + if(!overwriteModel && checkExist(targetPath, ("src.main.kotlin." + modelPackage).replace(".", separator), modelFileName + ".kt")) { + continue; + } + if (isEnumClass) { + transfer(targetPath, ("src.main.kotlin." + modelPackage).replace(".", separator), modelFileName + ".kt", templates.restkotlin.enumClass.template(modelPackage, modelFileName, enums)); + continue; + } + transfer(targetPath, ("src.main.kotlin." + modelPackage).replace(".", separator), modelFileName + ".kt", templates.restkotlin.pojo.template(modelPackage, modelFileName, classVarName, props)); + } + } + } + + // exit after generating the model if the consumer needs only the model classes + if(generateModelOnly) + return; + + // handler + for(Map op : operationList){ + String className = op.get("handlerName").toString(); + String example = null; + List parameters = (List) op.get("parameters"); + if(op.get("example") != null) { + //example = mapper.writeValueAsString(op.get("example")); + example = JsonStream.serialize(op.get("example")); + } + if(checkExist(targetPath, ("src.main.kotlin." + handlerPackage).replace(".", separator), className + ".kt") && !overwriteHandler) { + continue; + } + transfer(targetPath, ("src.main.kotlin." + handlerPackage).replace(".", separator), className + ".kt", templates.restkotlin.handler.template(handlerPackage, className, example, parameters)); + } + + // handler test cases + transfer(targetPath, ("src.test.kotlin." + handlerPackage + ".").replace(".", separator), "LightTestServer.kt", templates.restkotlin.lightTestServerKt.template(handlerPackage)); + for(Map op : operationList){ + if(checkExist(targetPath, ("src.test.kotlin." + handlerPackage).replace(".", separator), op.get("handlerName") + "Test.kt") && !overwriteHandlerTest) { + continue; + } + transfer(targetPath, ("src.test.kotlin." + handlerPackage).replace(".", separator), op.get("handlerName") + "Test.kt", templates.restkotlin.handlerTest.template(handlerPackage, op)); + } + + // transfer binary files without touching them. + try (InputStream is = OpenApiGenerator.class.getResourceAsStream("/binaries/server.keystore")) { + Files.copy(is, Paths.get(targetPath, ("src.main.resources.config").replace(".", separator), "server.keystore"), StandardCopyOption.REPLACE_EXISTING); + } + try (InputStream is = OpenApiGenerator.class.getResourceAsStream("/binaries/server.truststore")) { + Files.copy(is, Paths.get(targetPath, ("src.main.resources.config").replace(".", separator), "server.truststore"), StandardCopyOption.REPLACE_EXISTING); + } + if(supportClient) { + try (InputStream is = OpenApiGenerator.class.getResourceAsStream("/binaries/client.keystore")) { + Files.copy(is, Paths.get(targetPath, ("src.main.resources.config").replace(".", separator), "client.keystore"), StandardCopyOption.REPLACE_EXISTING); + } + try (InputStream is = OpenApiGenerator.class.getResourceAsStream("/binaries/client.truststore")) { + Files.copy(is, Paths.get(targetPath, ("src.main.resources.config").replace(".", separator), "client.truststore"), StandardCopyOption.REPLACE_EXISTING); + } + } else { + try (InputStream is = OpenApiGenerator.class.getResourceAsStream("/binaries/client.keystore")) { + Files.copy(is, Paths.get(targetPath, ("src.test.resources.config").replace(".", separator), "client.keystore"), StandardCopyOption.REPLACE_EXISTING); + } + try (InputStream is = OpenApiGenerator.class.getResourceAsStream("/binaries/client.truststore")) { + Files.copy(is, Paths.get(targetPath, ("src.test.resources.config").replace(".", separator), "client.truststore"), StandardCopyOption.REPLACE_EXISTING); + } + } + + if(model instanceof Any) { + try (InputStream is = new ByteArrayInputStream(model.toString().getBytes(StandardCharsets.UTF_8))) { + Files.copy(is, Paths.get(targetPath, ("src.main.resources.config").replace(".", separator), "openapi.json"), StandardCopyOption.REPLACE_EXISTING); + } + } else if(model instanceof String){ + try (InputStream is = new ByteArrayInputStream(((String)model).getBytes(StandardCharsets.UTF_8))) { + Files.copy(is, Paths.get(targetPath, ("src.main.resources.config").replace(".", separator), "openapi.yaml"), StandardCopyOption.REPLACE_EXISTING); + } + } + } + + /** + * Initialize the property map with base elements as name, getter, setters, etc + * @param entry The entry for which to generate + * @param propMap The property map to add to, created in the caller + */ + private void initializePropertyMap(Map.Entry entry, Map propMap) { + String name = entry.getKey(); + propMap.put("jsonProperty", Any.wrap(name)); + if(name.startsWith("@")) { + name = name.substring(1); + + } + propMap.put("name", Any.wrap(name)); + propMap.put("getter", Any.wrap("get" + name.substring(0, 1).toUpperCase() + name.substring(1))); + propMap.put("setter", Any.wrap("set" + name.substring(0, 1).toUpperCase() + name.substring(1))); + // assume it is not enum unless it is overwritten + propMap.put("isEnum", Any.wrap(false)); + } + + /** + * Handle elements listed as "properties" + * + * @param props The properties map to add to + */ + //private void handleProperties(List> props, Map.Entry entrySchema) { + private void handleProperties(List> props, Map properties) { + // transform properties + for(Map.Entry entryProp: properties.entrySet()) { + //System.out.println("key = " + entryProp.getKey() + " value = " + entryProp.getValue()); + Map propMap = new HashMap<>(); + + // initialize property map + initializePropertyMap(entryProp, propMap); + + String name = entryProp.getKey(); + boolean isArray = false; + for(Map.Entry entryElement: entryProp.getValue().asMap().entrySet()) { + //System.out.println("key = " + entryElement.getKey() + " value = " + entryElement.getValue()); + + if("type".equals(entryElement.getKey())) { + String t = typeMapping.get(entryElement.getValue().toString()); + if("java.util.List".equals(t)) { + isArray = true; + } else { + propMap.putIfAbsent("type", Any.wrap(t)); + } + } + if("items".equals(entryElement.getKey())) { + Any a = entryElement.getValue(); + if(a.get("$ref").valueType() != ValueType.INVALID && isArray) { + String s = a.get("$ref").toString(); + s = s.substring(s.lastIndexOf('/') + 1); + propMap.put("type", Any.wrap("java.util.List<" + s + ">")); + } + if(a.get("type").valueType() != ValueType.INVALID && isArray) { + propMap.put("type", Any.wrap("java.util.List<" + typeMapping.get(a.get("type").toString()) + ">")); + } + } + if("$ref".equals(entryElement.getKey())) { + String s = entryElement.getValue().toString(); + s = s.substring(s.lastIndexOf('/') + 1); + propMap.put("type", Any.wrap(s)); + } + if("default".equals(entryElement.getKey())) { + Any a = entryElement.getValue(); + propMap.put("default", a); + } + if("enum".equals(entryElement.getKey())) { + propMap.put("isEnum", Any.wrap(true)); + propMap.put("nameWithEnum", Any.wrap(name.substring(0, 1).toUpperCase() + name.substring(1) + "Enum")); + this.addUnderscores(entryElement); + propMap.put("value", Any.wrap(entryElement.getValue())); + } + if("format".equals(entryElement.getKey())) { + String s = entryElement.getValue().toString(); + if("date-time".equals(s)) { + propMap.put("type", Any.wrap("java.time.LocalDateTime")); + } + if("date".equals(s)) { + propMap.put("type", Any.wrap("java.time.LocalDate")); + } + if("double".equals(s)) { + propMap.put("type", Any.wrap("Double")); + } + if("float".equals(s)) { + propMap.put("type", Any.wrap("Float")); + } + if("int64".equals(s)){ + propMap.put("type", Any.wrap("Long")); + } + if("int32".equals(s)) { + propMap.put("type", Any.wrap("Int")); + } + } + if("oneOf".equals(entryElement.getKey())) { + List list = entryElement.getValue().asList(); + Any t = list.get(0).asMap().get("type"); + if(t != null) { + propMap.put("type", Any.wrap(typeMapping.get(t.toString()))); + } else { + // maybe reference? default type to object. + propMap.put("type", Any.wrap("Object")); + } + } + if("anyOf".equals(entryElement.getKey())) { + List list = entryElement.getValue().asList(); + Any t = list.get(0).asMap().get("type"); + if(t != null) { + propMap.put("type", Any.wrap(typeMapping.get(t.toString()))); + } else { + // maybe reference? default type to object. + propMap.put("type", Any.wrap("Object")); + } + } + if("allOf".equals(entryElement.getKey())) { + List list = entryElement.getValue().asList(); + Any t = list.get(0).asMap().get("type"); + if(t != null) { + propMap.put("type", Any.wrap(typeMapping.get(t.toString()))); + } else { + // maybe reference? default type to object. + propMap.put("type", Any.wrap("Object")); + } + } + if("not".equals(entryElement.getKey())) { + Map m = entryElement.getValue().asMap(); + Any t = m.get("type"); + if(t != null) { + propMap.put("type", t); + } else { + propMap.put("type", Any.wrap("Object")); + } + } + } + props.add(propMap); + } + } + + public List> getOperationList(Object model) { + List> result = new ArrayList<>(); + String s; + if(model instanceof Any) { + s = ((Any)model).toString(); + } else if(model instanceof String){ + s = (String)model; + } else { + throw new RuntimeException("Invalid Model Class: " + model.getClass()); + } + OpenApi3 openApi3 = null; + try { + openApi3 = (OpenApi3) new OpenApiParser().parse(s, new URL("https://oas.lightapi.net/")); + } catch (MalformedURLException e) { + } + String basePath = getBasePath(openApi3); + + Map paths = openApi3.getPaths(); + for(Map.Entry entryPath: paths.entrySet()) { + String path = entryPath.getKey(); + Path pathValue = entryPath.getValue(); + for(Map.Entry entryOps: pathValue.getOperations().entrySet()) { + // skip all the entries that are not http method. The only possible entries + // here are extensions. which will be just a key value pair. + if(entryOps.getKey().startsWith("x-")) continue; + Map flattened = new HashMap<>(); + flattened.put("method", entryOps.getKey().toUpperCase()); + flattened.put("capMethod", entryOps.getKey().substring(0, 1).toUpperCase() + entryOps.getKey().substring(1)); + flattened.put("path", basePath + path); + String normalizedPath = path.replace("{", "").replace("}", ""); + flattened.put("handlerName", Utils.camelize(normalizedPath) + Utils.camelize(entryOps.getKey()) + "Handler"); + Operation operation = entryOps.getValue(); + flattened.put("normalizedPath", UrlGenerator.generateUrl(basePath, path, entryOps.getValue().getParameters())); + //eg. 200 || statusCode == 400 || statusCode == 500 + flattened.put("supportedStatusCodesStr", operation.getResponses().keySet().stream().collect(Collectors.joining(" || statusCode = "))); + Map headerNameValueMap = operation.getParameters() + .stream() + .filter(parameter -> parameter.getIn().equals("header")) + .collect(Collectors.toMap(k -> k.getName(), v -> UrlGenerator.generateValidParam(v))); + flattened.put("headerNameValueMap", headerNameValueMap); + if (enableParamDescription) { + //get parameters info and put into result + List parameterRawList = operation.getParameters(); + List parametersResultList = new LinkedList<>(); + parameterRawList.forEach(parameter -> { + Map parameterMap = new HashMap<>(); + parameterMap.put("name", parameter.getName()); + parameterMap.put("description", parameter.getDescription()); + if(parameter.getRequired() != null) { + parameterMap.put("required", String.valueOf(parameter.getRequired())); + } + Schema schema = parameter.getSchema(); + if(schema != null) { + parameterMap.put("type", schema.getType()); + if(schema.getMinLength() != null) { + parameterMap.put("minLength", String.valueOf(schema.getMinLength())); + } + if(schema.getMaxLength() != null) { + parameterMap.put("maxLength", String.valueOf(schema.getMaxLength())); + } + } + parametersResultList.add(parameterMap); + }); + flattened.put("parameters", parametersResultList); + } + Response response = operation.getResponse("200"); + if(response != null) { + MediaType mediaType = response.getContentMediaType("application/json"); + if(mediaType != null) { + // first check if there is a single example defined. + Object example = mediaType.getExample(); + if(example != null) { + flattened.put("example", example); + } else { + // check if there are multiple examples + Map exampleMap = mediaType.getExamples(); + // use the first example if there are multiple + if(exampleMap.size() > 0) { + Map.Entry entry = exampleMap.entrySet().iterator().next(); + Example e = entry.getValue(); + if(e != null) { + flattened.put("example", e.getValue()); + } + } + } + } + } + result.add(flattened); + } + } + return result; + } + + private static String getBasePath(OpenApi3 openApi3) { + String basePath = ""; + String url = null; + if (openApi3.getServers().size() > 0) { + Server server = openApi3.getServer(0); + url = server.getUrl(); + } + if(url != null) { + // find "://" index + int protocolIndex = url.indexOf("://"); + int pathIndex = url.indexOf('/', protocolIndex + 3); + if(pathIndex > 0) { + basePath = url.substring(pathIndex); + } + } + return basePath; + } + + private static void addUnderscores(Map.Entry entryElement) { + Iterator iterator = entryElement.getValue().iterator(); + List list = new ArrayList<>(); + while (iterator.hasNext()) { + Any any = iterator.next(); + String value = any.toString().trim().replaceAll(" ", "_"); + list.add(Any.wrap(value)); + } + entryElement.setValue(Any.wrap(list)); + } + + +} diff --git a/light-rest-4j/src/main/java/com/networknt/codegen/rest/UrlGenerator.java b/light-rest-4j/src/main/java/com/networknt/codegen/rest/UrlGenerator.java index 2f20e5380..071460fd8 100644 --- a/light-rest-4j/src/main/java/com/networknt/codegen/rest/UrlGenerator.java +++ b/light-rest-4j/src/main/java/com/networknt/codegen/rest/UrlGenerator.java @@ -64,6 +64,8 @@ public static String generateUrl(String basePath, String path, List p /** * based on parameter schemas generate query parameter part of the url + * @param parameters a list of Parameters + * @return String of URL */ public static String generateQueryParamUrl(List parameters) { String url = ""; diff --git a/light-rest-4j/src/main/resources/binaries/gradle-wrapper.jar b/light-rest-4j/src/main/resources/binaries/gradle-wrapper.jar new file mode 100644 index 000000000..d4046a663 Binary files /dev/null and b/light-rest-4j/src/main/resources/binaries/gradle-wrapper.jar differ diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/LICENSE.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/LICENSE.rocker.raw new file mode 100644 index 000000000..8dada3eda --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/LICENSE.rocker.raw @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/README.md.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/README.md.rocker.raw new file mode 100644 index 000000000..0e55a809f --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/README.md.rocker.raw @@ -0,0 +1,38 @@ +# OpenAPI Kotlin Light-4J Server + +## Start server + +On Linux or Mac, the gradlew need to change the mode to executable. + +``` +chmod +x gradlew +``` + +To build the server + +``` +./gradlew build +``` + + +To start the server + +``` +./gradlew run +``` + +## Test + +By default, all endpoints are protected by OAuth jwt token verifier. It can be turned off with config change through for development. + + +In order to access the server, there is a long lived token below issued by my +oauth2 server [light-oauth2](https://github.com/networknt/light-oauth2) + +``` +Bearer eyJraWQiOiIxMDAiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ1cm46Y29tOm5ldHdvcmtudDpvYXV0aDI6djEiLCJhdWQiOiJ1cm46Y29tLm5ldHdvcmtudCIsImV4cCI6MTc5MDAzNTcwOSwianRpIjoiSTJnSmdBSHN6NzJEV2JWdUFMdUU2QSIsImlhdCI6MTQ3NDY3NTcwOSwibmJmIjoxNDc0Njc1NTg5LCJ2ZXJzaW9uIjoiMS4wIiwidXNlcl9pZCI6InN0ZXZlIiwidXNlcl90eXBlIjoiRU1QTE9ZRUUiLCJjbGllbnRfaWQiOiJmN2Q0MjM0OC1jNjQ3LTRlZmItYTUyZC00YzU3ODc0MjFlNzIiLCJzY29wZSI6WyJ3cml0ZTpwZXRzIiwicmVhZDpwZXRzIl19.mue6eh70kGS3Nt2BCYz7ViqwO7lh_4JSFwcHYdJMY6VfgKTHhsIGKq2uEDt3zwT56JFAePwAxENMGUTGvgceVneQzyfQsJeVGbqw55E9IfM_uSM-YcHwTfR7eSLExN4pbqzVDI353sSOvXxA98ZtJlUZKgXNE1Ngun3XFORCRIB_eH8B0FY_nT_D1Dq2WJrR-re-fbR6_va95vwoUdCofLRa4IpDfXXx19ZlAtfiVO44nw6CS8O87eGfAm7rCMZIzkWlCOFWjNHnCeRsh7CVdEH34LF-B48beiG5lM7h4N12-EME8_VDefgMjZ8eqs1ICvJMxdIut58oYbdnkwTjkA +``` + +Postman is the best tool to test REST APIs + +Add "Authorization" header with value as above token and a dummy message will return from the generated stub. diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/buildGradleKts.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/buildGradleKts.rocker.raw new file mode 100644 index 000000000..6d5f6b518 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/buildGradleKts.rocker.raw @@ -0,0 +1,102 @@ +@import com.jsoniter.any.Any +@args (Any config) +plugins { + application + kotlin("jvm") version "1.3.21" + // https://github.com/ben-manes/gradle-versions-plugin + id("com.github.ben-manes.versions") version "0.20.0" + // https://github.com/johnrengelman/shadow + id("com.github.johnrengelman.shadow") version "4.0.4" +} + +application { + mainClassName = "com.networknt.server.Server" +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" +} + +dependencies { + compile(kotlin("stdlib")) + val light4jVersion: String by project + + // light-4j + compile("com.networknt", "server", light4jVersion) + compile("com.networknt", "handler", light4jVersion) + compile("com.networknt", "info", light4jVersion) + compile("com.networknt", "health", light4jVersion) + compile("com.networknt", "metrics", light4jVersion) + compile("com.networknt", "traceability", light4jVersion) + compile("com.networknt", "correlation", light4jVersion) + compile("com.networknt", "encode-decode", light4jVersion) + compile("com.networknt", "body", light4jVersion) + compile("com.networknt", "audit", light4jVersion) + compile("com.networknt", "sanitizer", light4jVersion) + + compile("com.networknt", "openapi-parser", light4jVersion) + compile("com.networknt", "openapi-meta", light4jVersion) + compile("com.networknt", "openapi-security", light4jVersion) + compile("com.networknt", "openapi-validator", light4jVersion) + compile("com.networknt", "specification", light4jVersion) + + // json-schema-validator + val jsonSchemaValidatorVersion : String by project + compile("com.networknt", "json-schema-validator", jsonSchemaValidatorVersion) + + + // jackson json/xml/yaml serialisation + val jacksonVersion: String by project + compile("com.fasterxml.jackson.core", "jackson-core", jacksonVersion) + compile("com.fasterxml.jackson.core", "jackson-databind", jacksonVersion) + compile("com.fasterxml.jackson.module", "jackson-module-kotlin", jacksonVersion) + compile("com.fasterxml.jackson.datatype", "jackson-datatype-jsr310", jacksonVersion) + + // undertow version for the http core + val undertowVersion: String by project + compile("io.undertow", "undertow-core", undertowVersion) + + val logbackVersion: String by project + compile("ch.qos.logback", "logback-classic", logbackVersion) + + val kotlinLoggingVersion: String by project + compile("io.github.microutils", "kotlin-logging", kotlinLoggingVersion) + + @if(config.toBoolean("supportDb") || config.toBoolean("supportH2ForTest")){ + val hikaricpVersion: String by project + compile("com.zaxxer", "HikariCP", hikaricpVersion) + } + @if(config.toBoolean("supportDb") && "oracle".equalsIgnoreCase(config.toString("dbInfo", "name"))){ + val oracleVersion: String by project + compile("com.oracle", "ojdbc6", oracleVersion) + } + @if(config.toBoolean("supportDb") && "mysql".equalsIgnoreCase(config.toString("dbInfo", "name"))){ + val mysqlVersion: String by project + compile("mysql", "mysql-connector-java", mysqlVersion) + } + @if(config.toBoolean("supportDb") && "postgres".equalsIgnoreCase(config.toString("dbInfo", "name"))){ + val postgresVersion: String by project + compile("org.postgresql", "postgresql", postgresVersion) + } + + // standard testing libraries + val junitVersion: String by project + testImplementation("org.junit.jupiter", "junit-jupiter-api", junitVersion) + testImplementation("org.junit.jupiter", "junit-jupiter-params", junitVersion) + testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine", junitVersion) + + // assertk + val assertkVersion: String by project + testCompile("com.willowtreeapps.assertk", "assertk-jvm", assertkVersion) + @if(config.toBoolean("supportH2ForTest")){ + val h2Version: String by project + testCompile("com.h2database", "h2", h2Version) + } + +} + +repositories { + mavenLocal() // mavenLocal must be added first. + mavenCentral() + jcenter() +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/buildSh.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/buildSh.rocker.raw new file mode 100644 index 000000000..2792d37a7 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/buildSh.rocker.raw @@ -0,0 +1,52 @@ +@args (String org, String serviceId) +#!/bin/bash + +set -ex + +VERSION=$1 +IMAGE_NAME="@org/@serviceId" + +showHelp() { + echo " " + echo "Error: $1" + echo " " + echo " build.sh [VERSION]" + echo " " + echo " where [VERSION] version of the docker image that you want to publish (example: 0.0.1)" + echo " " + echo " example: ./build.sh 0.0.1" + echo " " +} + +build() { + echo "Building ..." + mvn clean install + echo "Successfully built!" +} + +cleanup() { + if [[ "$(docker images -q $IMAGE_NAME 2> /dev/null)" != "" ]]; then + echo "Removing old $IMAGE_NAME images" + docker images | grep $IMAGE_NAME | awk '{print $3}' | xargs docker rmi -f + echo "Cleanup completed!" + fi +} + +publish() { + echo "Building Docker image with version $VERSION" + docker build -t $IMAGE_NAME:$VERSION -t $IMAGE_NAME:latest -f ./docker/Dockerfile . --no-cache=true + docker build -t $IMAGE_NAME:$VERSION-redhat -f ./docker/Dockerfile-Redhat . --no-cache=true + echo "Images built with version $VERSION" + echo "Pushing image to DockerHub" + docker push $IMAGE_NAME + echo "Image successfully published!" +} + +if [ -z $VERSION ]; then + showHelp "[VERSION] parameter is missing" + exit +fi + +build; +cleanup; +publish; diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/clientYml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/clientYml.rocker.raw new file mode 100644 index 000000000..3656b1fda --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/clientYml.rocker.raw @@ -0,0 +1,74 @@ +# This is the configuration file for Http2Client. +--- +# Settings for TLS +tls: + # if the server is using self-signed certificate, this need to be false. If true, you have to use CA signed certificate + # or load truststore that contains the self-signed cretificate. + verifyHostname: true + # trust store contains certifictes that server needs. Enable if tls is used. + loadTrustStore: true + # trust store location can be specified here or system properties javax.net.ssl.trustStore and password javax.net.ssl.trustStorePassword + trustStore: client.truststore + # key store contains client key and it should be loaded if two-way ssl is uesed. + loadKeyStore: false + # key store location + keyStore: client.keystore +# settings for OAuth2 server communication +oauth: + # OAuth 2.0 token endpoint configuration + token: + # The scope token will be renewed automatically 1 minutes before expiry + tokenRenewBeforeExpired: 60000 + # if scope token is expired, we need short delay so that we can retry faster. + expiredRefreshRetryDelay: 2000 + # if scope token is not expired but in renew windown, we need slow retry delay. + earlyRefreshRetryDelay: 4000 + # token server url. The default port number for token service is 6882. + server_url: https://localhost:6882 + # token service unique id for OAuth 2.0 provider + serviceId: com.networknt.oauth2-token-1.0.0 + # set to true if the oauth2 provider supports HTTP/2 + enableHttp2: true + # the following section defines uri and parameters for authorization code grant type + authorization_code: + # token endpoint for authorization code grant + uri: "/oauth2/token" + # client_id for authorization code grant flow. client_secret is in secret.yml + client_id: f7d42348-c647-4efb-a52d-4c5787421e72 + # the web server uri that will receive the redirected authorization code + redirect_uri: https://localhost:8080/authorization_code + # optional scope, default scope in the client registration will be used if not defined. + scope: + - petstore.r + - petstore.w + # the following section defines uri and parameters for client credentials grant type + client_credentials: + # token endpoint for client credentials grant + uri: "/oauth2/token" + # client_id for client credentials grant flow. client_secret is in secret.yml + client_id: f7d42348-c647-4efb-a52d-4c5787421e72 + # optional scope, default scope in the client registration will be used if not defined. + scope: + - petstore.r + - petstore.w + refresh_token: + # token endpoint for refresh token grant + uri: "/oauth2/token" + # client_id for refresh token grant flow. client_secret is in secret.yml + client_id: f7d42348-c647-4efb-a52d-4c5787421e72 + # optional scope, default scope in the client registration will be used if not defined. + scope: + - petstore.r + - petstore.w + # light-oauth2 key distribution endpoint configuration + key: + # key distribution server url + server_url: https://localhost:6886 + # the unique service id for key distribution service + serviceId: com.networknt.oauth2-key-1.0.0 + # the path for the key distribution endpoint + uri: "/oauth2/key" + # client_id used to access key distribution service. It can be the same client_id with token service or not. + client_id: f7d42348-c647-4efb-a52d-4c5787421e72 + # set to true if the oauth2 provider supports HTTP/2 + enableHttp2: true diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/dockerfile.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/dockerfile.rocker.raw new file mode 100644 index 000000000..ae91011ec --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/dockerfile.rocker.raw @@ -0,0 +1,6 @@ +@import com.jsoniter.any.Any +@args (Any config, String expose) +FROM openjdk:8-jre-alpine +#EXPOSE @expose +@with (name = config.get("artifactId") + ".jar") {ADD /build/libs/@name server.jar} +CMD ["/bin/sh","-c","java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Dlight-4j-config-dir=/config -Dlogback.configurationFile=/config/logback.xml -jar /server.jar"] \ No newline at end of file diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/dockerfileredhat.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/dockerfileredhat.rocker.raw new file mode 100644 index 000000000..a6ca886e9 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/dockerfileredhat.rocker.raw @@ -0,0 +1,6 @@ +@import com.jsoniter.any.Any +@args (Any config, String expose) +FROM registry.access.redhat.com/redhat-openjdk-18/openjdk18-openshift +#EXPOSE @expose +@with (name = config.get("artifactId") + ".jar") {ADD /build/libs/@name server.jar} +CMD ["/bin/sh","-c","java -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Dlight-4j-config-dir=/config -Dlogback.configurationFile=/config/logback.xml -jar server.jar"] diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/enumClass.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/enumClass.rocker.raw new file mode 100644 index 000000000..0111716a0 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/enumClass.rocker.raw @@ -0,0 +1,6 @@ +@args (String modelPackage, String className, String enums) +package @modelPackage; + +enum class @className { + @enums +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/enumInline.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/enumInline.rocker.raw new file mode 100644 index 000000000..59e831aa7 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/enumInline.rocker.raw @@ -0,0 +1,34 @@ +@import com.jsoniter.any.Any +@import java.util.Map +@import java.util.List +@args (Map prop) + @with (v = prop.get("nameWithEnum") + ".values()", value = prop.get("value").asList()) { + public enum @prop.get("nameWithEnum") { + @for((i, item) : value) { + @with(u = item.toString().toUpperCase().replaceAll("-", "_")) {@if (i.index() < value.size() - 1) {@u ("@item"),}@if(i.index() == value.size() - 1) {@u ("@item");}} + } + + private final @prop.get("type") value; + + @prop.get("nameWithEnum")(@prop.get("type") value) { + this.value = value; + } + + @@Override + public String toString() { + return String.valueOf(value); + } + + public static @prop.get("nameWithEnum") fromValue(String text) { + for (@prop.get("nameWithEnum") b : @v) { + if (String.valueOf(b.value).equals(text)) { + return b; + } + } + return null; + } + } + + private @prop.get("nameWithEnum") @prop.get("name"); + + } \ No newline at end of file diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/gitignore.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/gitignore.rocker.raw new file mode 100644 index 000000000..6438a4e9e --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/gitignore.rocker.raw @@ -0,0 +1,23 @@ +target/ +bower_components/ +node_modules/ +dist/ +build/ +out/ +.idea/ +.tmp/ +.project +.classpath +.settings +.gradle +.metadata/ +*.iml +*.log +*.tmp +*.zip +*.bak +*.versionsBackup +dependency-reduced-pom.xml + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/gradleProperties.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/gradleProperties.rocker.raw new file mode 100644 index 000000000..3cfc9bf12 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/gradleProperties.rocker.raw @@ -0,0 +1,27 @@ +@import com.jsoniter.any.Any +@args (Any config) +# Versions of Frequently used Libraries +kafkaVersion=2.0.0 +light4jVersion=1.5.29 +jacksonVersion=2.9.8 +undertowVersion=2.0.16.Final +logbackVersion=1.2.3 +jsonSchemaValidatorVersion=1.0.2 +junitVersion=5.3.1 +kotlinLoggingVersion=1.6.22 +assertkVersion=0.13 +@if(config.toBoolean("supportDb") || config.toBoolean("supportH2ForTest")){ +hikaricpVersion=3.1.0 +} +@if(config.toBoolean("supportDb") && "oracle".equalsIgnoreCase(config.toString("dbInfo", "name"))){ +oracleVersion=11.2.0.3 +} +@if(config.toBoolean("supportDb") && "mysql".equalsIgnoreCase(config.toString("dbInfo", "name"))){ +mysqlVersion=6.0.5 +} +@if(config.toBoolean("supportDb") && "postgres".equalsIgnoreCase(config.toString("dbInfo", "name"))){ +postgresVersion=42.1.1 +} +@if(config.toBoolean("supportH2ForTest")){ +h2Version=1.3.176 +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/gradleWrapperProperties.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/gradleWrapperProperties.rocker.raw new file mode 100644 index 000000000..15be713e5 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/gradleWrapperProperties.rocker.raw @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://repo.gradle.org/gradle/kotlin-dsl-snapshots-local/gradle-kotlin-dsl-5.2-20190201174223+0000-all.zip diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/gradlew.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/gradlew.rocker.raw new file mode 100644 index 000000000..fe73055bc --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/gradlew.rocker.raw @@ -0,0 +1,169 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "${JVM_OPTS[@@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@@" diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/gradlewBat.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/gradlewBat.rocker.raw new file mode 100644 index 000000000..16191a852 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/gradlewBat.rocker.raw @@ -0,0 +1,84 @@ +@@if "%DEBUG%" == "" @@echo off +@@rem ########################################################################## +@@rem +@@rem Gradle startup script for Windows +@@rem +@@rem ########################################################################## + +@@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/handler.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/handler.rocker.raw new file mode 100644 index 000000000..85fb5bec4 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/handler.rocker.raw @@ -0,0 +1,28 @@ +@import org.apache.commons.text.StringEscapeUtils +@import java.util.Map +@import java.util.List +@option discardLogicWhitespace=true +@args (String handlerPackage, String className, String example, List parameters) +package @handlerPackage + +import com.networknt.handler.LightHttpHandler +import io.undertow.server.HttpServerExchange +import io.undertow.util.HttpString + +class @className : LightHttpHandler { + @if(parameters != null && !parameters.isEmpty()) {/**@for (parameter : parameters) { + * @@param @?parameter.get("name") @if(parameter.get("type") != null) + { @with (String typeStr = ((String)parameter.get("type")).substring(0, 1).toUpperCase()+((String)parameter.get("type")).substring(1)) + { @?typeStr }} @if ( parameter.get("required").equals("true") ) {@@Required } else{@@Optional }@if(parameter.get("minLength") != null){minLength:@parameter.get("minLength");}@if(parameter.get("maxLength") != null) {maxLength:@parameter.get("maxLength");}@if(parameter.get("description") != null ){ + * @parameter.get("description")}} + */} + @@Throws(Exception::class) + override fun handleRequest(exchange: HttpServerExchange) { + @if(example != null) { + exchange.responseHeaders.add(HttpString("Content-Type"), "application/json") + @with (e = StringEscapeUtils.escapeJson(example)) {exchange.responseSender.send("@e")} + } else { + exchange.endExchange() + } + } +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/handlerTest.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/handlerTest.rocker.raw new file mode 100644 index 000000000..fbdb2663b --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/handlerTest.rocker.raw @@ -0,0 +1,40 @@ +@import java.util.Map +@import com.jsoniter.any.Any +@import java.util.stream.Collectors +@args (String handlerPackage, Map map) +package @handlerPackage +import assertk.all +import assertk.assertThat +import mu.KotlinLogging +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@with (className = map.get("handlerName") + "Test", + method = map.get("handlerName") + "Test()", + handlerName = map.get("handlerName"), + loggerName = map.get("handlerName") + "Test" + ".class", + httpMethod = map.get("method"), + hasBody = ("POST".equals(map.get("method").toString()) || "PUT".equals(map.get("method").toString()) || "PATCH".equals(map.get("method").toString())), + path = map.get("normalizedPath")) { +@@ExtendWith(LightTestServer::class) +class @className { + companion object { + val log = KotlinLogging.logger {} + } + + @@Test + fun `test @method @handlerName success` () { + /* + @if(hasBody) { + val response = LightTestServer.makePostRequest("@path", "request body to be replaced") + } else { + val response = LightTestServer.makeGetRequest("@path") + } + assertThat(response).all { + rcIsEqualTo(200) + bodyContains("any string from the body to be replaced") + } + */ + } +} +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/junitPlatformProperties.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/junitPlatformProperties.rocker.raw new file mode 100644 index 000000000..faa4f7765 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/junitPlatformProperties.rocker.raw @@ -0,0 +1,2 @@ +# suppress inspection "UnusedProperty" +junit.jupiter.testinstance.lifecycle.default=per_class diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/lightTestServerKt.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/lightTestServerKt.rocker.raw new file mode 100644 index 000000000..24a35b0a2 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/lightTestServerKt.rocker.raw @@ -0,0 +1,180 @@ +@args (String handlerPackage) +package @handlerPackage + +import assertk.Assert +import assertk.assertions.contains +import assertk.assertions.isEqualTo +import com.networknt.client.Http2Client +import com.networknt.server.Server +import io.undertow.UndertowOptions +import io.undertow.client.ClientCallback +import io.undertow.client.ClientExchange +import io.undertow.client.ClientRequest +import io.undertow.client.ClientResponse +import io.undertow.util.FlexBase64 +import io.undertow.util.Headers +import io.undertow.util.HttpString +import io.undertow.util.Methods +import mu.KotlinLogging +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.xnio.OptionMap +import java.io.IOException +import java.net.ServerSocket +import java.net.URI +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference + +/** + * Junit5 Extension which sets up a light-server BeforeAll tests and tears it down AfterAll. + * Use with `@@ExtendWith(LightTestServer::class)` + * + * The first time a server is started in a particular VM, a random port is assigned to it to avoid clashes between + * concurrent test runs or other active servers. + * + * There are also static utility methods to make requests to the configured server. + */ +class LightTestServer() : BeforeAllCallback, AfterAllCallback { + + // EXTENSION LIFE-CYCLE METHODS + + var oldIsDynamicPort: Boolean? = null + var oldHttpsPort: Int? = null + + // Patch Server.config and start server + override fun beforeAll(context: ExtensionContext?) { + oldIsDynamicPort = Server.config.isDynamicPort + oldHttpsPort = Server.config.httpsPort + Server.config.isDynamicPort = false + Server.config.httpsPort = httpsPort + Server.start() + } + + // Stop server and unpatch config + override fun afterAll(context: ExtensionContext?) { + Server.stop() + Server.config.isDynamicPort = oldIsDynamicPort!! + Server.config.httpsPort = oldHttpsPort!! + } + + companion object { + + val log = KotlinLogging.logger {} + + // SERVER STATE + + val httpsPort = randomFreePort(40000, 60000) + val baseUrl = "https://localhost:$httpsPort" + + + // MAKE REQUESTS TO SERVER + + /** Make a GET request to the server maintained by this extension. */ + fun makeGetRequest(path: String, auth: String? = null): ClientResponse { + return makeRequest(path, Methods.GET, null, auth) + } + + /** Make a POST request to the server maintained by this extension. */ + fun makePostRequest(path: String, body: String, auth: String? = null): ClientResponse { + return makeRequest(path, Methods.POST, body, auth) + } + + /** Make a PUT request to the server maintained by this extension. */ + fun makePutRequest(path: String, body: String, auth: String? = null): ClientResponse { + return makeRequest(path, Methods.PUT, body, auth) + } + + /** Make a DELETE request to the server maintained by this extension. */ + fun makeDeleteRequest(path: String, auth: String? = null): ClientResponse { + return makeRequest(path, Methods.DELETE, null, auth) + } + + /** Finds a random free port by attempting to listen on random ports until it succeeds. */ + fun randomFreePort(minPort: Int, maxPort: Int): Int { + val random = Random() + while (true) { + val port = random.nextInt(maxPort - minPort) + minPort + try { + val ss = ServerSocket(port) + ss.close() + return port + } catch (e: IOException) { + log.info { "Port ${port} was busy" } + } + } + } + + /** Make a request to the server maintained by this extension. */ + fun makeRequest(path: String, method: HttpString, body: String?, auth: String? = null): ClientResponse { + log.info { "${method} :: $baseUrl :: ${path}" } + + val client = Http2Client.getInstance() + + client.connect( + URI(baseUrl), + Http2Client.WORKER, + Http2Client.SSL, + Http2Client.BUFFER_POOL, + OptionMap.create(UndertowOptions.ENABLE_HTTP2, true) + ).get().use { connection -> + + val request = ClientRequest().setPath(path).setMethod(method) + authenticate(request, auth) + val latch = CountDownLatch(1) + val reference = AtomicReference() + val callback: ClientCallback + + if (body == null) { + callback = client.createClientCallback(reference, latch) + } else { + log.info { "body: ${body}" } + val firstChar = if (body.length > 0) body[0] else '\u0000' + if (firstChar == '[' || firstChar == '@{') { + request.requestHeaders.put(Headers.CONTENT_TYPE, "application/json") + } else { + request.requestHeaders.put(Headers.CONTENT_TYPE, "text/plain") + } + request.requestHeaders.put(Headers.TRANSFER_ENCODING, "chunked") + callback = client.createClientCallback(reference, latch, body) + } + + connection.sendRequest(request, callback) + latch.await() + + val response = reference.get() + log.info { "Response code = ${response.responseCode}" } + log.info { "Response body = ${response.getAttachment(Http2Client.RESPONSE_BODY)}" } + return response + } + + } + + private fun authenticate(request: ClientRequest, auth: String?) { + if (auth == null) return + + log.info { "auth = ${auth}" } + val bytes = auth.toByteArray(Charsets.UTF_8) + log.info { "bytes = ${bytes}" } + val encoded = FlexBase64.encodeString(bytes, false) + log.info { "encoded = ${encoded}" } + request.requestHeaders.add( + Headers.AUTHORIZATION, + "Basic ${encoded}" + ) + } + } +} + +fun Assert.rcIsEqualTo(expected: Int) = given { actual -> + assertThat(actual.responseCode, "Response Code").isEqualTo(expected) +} + +fun Assert.bodyIsEqualTo(expected: String) = given { actual -> + assertThat(actual.getAttachment(com.networknt.client.Http2Client.RESPONSE_BODY), "Body").isEqualTo(expected) +} + +fun Assert.bodyContains(expected: String) = given { actual -> + assertThat(actual.getAttachment(com.networknt.client.Http2Client.RESPONSE_BODY), "Body").contains(expected) +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/logback.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/logback.rocker.raw new file mode 100644 index 000000000..9018d8433 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/logback.rocker.raw @@ -0,0 +1,97 @@ + + + + + TODO create logger for audit only. + http://stackoverflow.com/questions/2488558/logback-to-log-different-messages-to-two-files + + PROFILER + + NEUTRAL + + + + + + %d{HH:mm:ss.SSS} [%thread] %X{sId} %X{cId} %-5level %logger{36} %M - %msg%n + + + + + target/test.log + false + + %d{HH:mm:ss.SSS} [%thread] %X{sId} %X{cId} %-5level %class{36}:%L %M - %msg%n + + + + + + target/audit.log + + %-5level [%thread] %date{ISO8601} %X{sId} %X{cId} %F:%L - %msg%n + + true + + + target/audit.log.%i.zip + 1 + 5 + + + 200MB + + + + + + + target/dump.log + + %-5level [%thread] %date{ISO8601} %X{sId} %X{cId} %F:%L - %msg%n + + true + + + target/audit.log.%i.zip + 1 + 5 + + + 200MB + + + + + + + + + + + + + + + + + + + + + diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/maskYml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/maskYml.rocker.raw new file mode 100644 index 000000000..f2807eff4 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/maskYml.rocker.raw @@ -0,0 +1,25 @@ +description: mask configuration for different type of inputs +string: + uri: +# password=[^&]*: password=****** +# number=\d{1,16}: number=---------------- +# sin=\d{1,9}: sin=masked +regex: + queryParameter: +# accountNo: "(.*)" + requestHeader: +# header1: "(.*)" +# header2: "(.*)" + responseHeader: +# header3: "(.*)" + requestCookies: +# userName: "(.*)" + responseCookies: +# sensitiveData: "(.*)" +json: + requestBody: +# "$.*.email": "(.*)" +# "$.product[*].item[*].name": "(.*)" + responseBody: +# "$.product[*].item[*].name": "(.*)" +# "$.product[*].item[*].name[0]": "(.*)" \ No newline at end of file diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/model.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/model.rocker.raw new file mode 100644 index 000000000..dbee02280 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/model.rocker.raw @@ -0,0 +1,9 @@ +@import com.jsoniter.any.Any +@args (String modelPackage, String className, Any props) +package @modelPackage; +import java.io.Serializable; + +public class @className implements Serializable { + public @className () { + } +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/openapi/config.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/config.rocker.raw new file mode 100644 index 000000000..be3f0e8d9 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/config.rocker.raw @@ -0,0 +1,17 @@ +#---------------------------------------------------------------------------------------------------------------- +# Scalable Config file +# +# This file serves as a configuration extension platform. Functions are list below: +# +# [1] exclusionConfigFileList: this configuration will be used by the light-4j/config module, when reading +# config files. it allows the listing of files which will be excluded from parameterized values set at +# the command-line or in a values.yml file +# Notes: +# File name included in the list will be excluded +# If the file is not provided, the config module will safely ignore it +# Simply list the config file names without extensions(.json, .yaml, .yml) +#---------------------------------------------------------------------------------------------------------------- +exclusionConfigFileList: + - openapi + - values + - status \ No newline at end of file diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/openapi/handlerYml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/handlerYml.rocker.raw new file mode 100644 index 000000000..45b374991 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/handlerYml.rocker.raw @@ -0,0 +1,105 @@ +@import java.util.Map +@import java.util.List +@import com.jsoniter.any.Any +@args (String serviceId, String handlerPackage, List> items, boolean prometheusMetrics, boolean healthCheck, boolean serverInfo) + +# Handler middleware chain configuration +--- +enabled: true + +#------------------------------------------------------------------------------ +# Support individual handler chains for each separate endpoint. It allows framework +# handlers like health check, server info to bypass majority of the middleware handlers +# and allows mixing multiple frameworks like OpenAPI and GraphQL in the same instance. +# +# handlers -- list of handlers to be used across chains in this microservice +# including the routing handlers for ALL endpoints +# -- format: fully qualified handler class name@@optional:given name +# chains -- allows forming of [1..N] chains, which could be wholly or +# used to form handler chains for each endpoint +# ex.: default chain below, reused partially across multiple endpoints +# paths -- list all the paths to be used for routing within the microservice +# ---- path: the URI for the endpoint (ex.: path: '/v1/pets') +# ---- method: the operation in use (ex.: 'post') +# ---- exec: handlers to be executed -- this element forms the list and +# the order of execution for the handlers +# +# IMPORTANT NOTES: +# - to avoid executing a handler, it has to be removed/commented out in the chain +# or change the enabled:boolean to false for a middleware handler configuration. +# - all handlers, routing handler included, are to be listed in the execution chain +# - for consistency, give a name to each handler; it is easier to refer to a name +# vs a fully qualified class name and is more elegant +# - you can list in chains the fully qualified handler class names, and avoid using the +# handlers element altogether +#------------------------------------------------------------------------------ +handlers: + # Light-framework cross-cutting concerns implemented in the microservice + - com.networknt.exception.ExceptionHandler@@exception +@if(prometheusMetrics){ - com.networknt.metrics.prometheus.PrometheusHandler@@prometheus} else { - com.networknt.metrics.MetricsHandler@@metrics} + - com.networknt.traceability.TraceabilityHandler@@traceability + - com.networknt.correlation.CorrelationHandler@@correlation + - com.networknt.openapi.OpenApiHandler@@specification + - com.networknt.openapi.JwtVerifyHandler@@security + - com.networknt.body.BodyHandler@@body + - com.networknt.audit.AuditHandler@@audit + # DumpHandler is to dump detail request/response info to log, useful for troubleshooting but not suggested to use in production due to it may lower the performance + # - com.networknt.dump.DumpHandler@@dump + - com.networknt.sanitizer.SanitizerHandler@@sanitizer + - com.networknt.openapi.ValidatorHandler@@validator + # Customer business domain specific cross-cutting concerns handlers + # - com.example.validator.CustomizedValidator@@custvalidator + # Framework endpoint handlers + - com.networknt.health.HealthGetHandler@@health + - com.networknt.info.ServerInfoGetHandler@@info + - com.networknt.specification.SpecDisplayHandler@@spec + - com.networknt.specification.SpecSwaggerUIHandler@@swaggerui + # - com.networknt.metrics.prometheus.PrometheusGetHandler@@getprometheus + # Business Handlers +@for(item: items) { - @with (p = handlerPackage + ".") {@p}@item.get("handlerName") +} + +chains: + default: + - exception +@if(prometheusMetrics){ - prometheus} else { - metrics} + - traceability + - correlation + - specification + - security + - body + - audit +# - dump + - sanitizer + - validator + +paths: +@for(item: items){ - path: '@item.get("path")' + method: '@item.get("method")' + exec: + - default + - @with (p = handlerPackage + ".") {@p}@item.get("handlerName") +} +@if(healthCheck){ - path: '/health/@serviceId' + method: 'get' + exec: + - health +} +@if(serverInfo){ - path: '/server/info' + method: 'get' + exec: + - info +} +@if(prometheusMetrics){ - path: '/prometheus' + method: 'get' + exec: + - getprometheus +} + - path: '/spec.yaml' + method: 'get' + exec: + - spec + - path: '/specui.html' + method: 'get' + exec: + - swaggerui \ No newline at end of file diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/openapi/service.yml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/service.yml.rocker.raw new file mode 100644 index 000000000..3e7157a99 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/service.yml.rocker.raw @@ -0,0 +1,30 @@ +@import com.jsoniter.any.Any +@args (Any config) +# Singleton service factory configuration/IoC injection +singletons: +# StartupHookProvider implementations, there are one to many and they are called in the same sequence defined. +# - com.networknt.server.StartupHookProvider: + # If you are using mask module to remove sensitive info before logging, uncomment the following line. + # - com.networknt.server.JsonPathStartupHookProvider + # - com.networknt.server.Test1StartupHook + # - com.networknt.server.Test2StartupHook +# ShutdownHookProvider implementations, there are one to many and they are called in the same sequence defined. +# - com.networknt.server.ShutdownHookProvider: + # - com.networknt.server.Test1ShutdownHook +@if(config.toBoolean("supportDb") ){ +@with (driverClassName = config.toString("dbInfo", "driverClassName"), jdbcUrl=config.toString("dbInfo", "jdbcUrl"), username=config.toString("dbInfo", "username"), password=config.toString("dbInfo", "password")) { +- javax.sql.DataSource: + - com.zaxxer.hikari.HikariDataSource: + DriverClassName: @driverClassName + jdbcUrl: @jdbcUrl + username: @username + password: @password + maximumPoolSize: 10 + useServerPrepStmts: true + cachePrepStmts: true + cacheCallableStmts: true + prepStmtCacheSize: 10 + prepStmtCacheSqlLimit: 2048 + connectionTimeout: 2000 +} +} diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/openapi/values.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/values.rocker.raw new file mode 100644 index 000000000..8fcb4b0c7 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/openapi/values.rocker.raw @@ -0,0 +1,97 @@ +#-------------------------------------------------------------------------------- +# values.yml : Set of values commonly overridden in microservices +# The file can be extended with other elements, as necessary +#-------------------------------------------------------------------------------- + +#-------------------------------------------------------------------------------- +# client.yml +#-------------------------------------------------------------------------------- +# key distribution server url +client.server_url: set-real-value-here +# client_id used to access key distribution service. It can be the same client_id with token service or not. +client.client_id: set-real-value-here + +#-------------------------------------------------------------------------------- +# server.yml +#-------------------------------------------------------------------------------- +# Https port if enableHttps is true. It will be ignored if dynamicPort is true +server.httpsPort: set-real-value-here + +# Enable HTTPS should be true on official environment and most dev environments. +server.enableHttps: set-real-value-here + +# Http/2 is enabled by default for better performance and it works with the client module. +server.enableHttp2: set-real-value-here + +# Minimum port range. This define a range for the dynamic allocated ports so that it is easier to setup +# firewall rule to enable this range. Default 2400 to 2500 block has 100 port numbers and should be +# enough for most cases unless you are using a big bare metal box as Kubernetes node that can run 1000s pods +server.minPort: 2400 + +# Maximum port rang. The range can be customized to adopt your network security policy and can be increased or +# reduced to ease firewall rules. +server.maxPort: 2500 + +# environment tag that will be registered on consul to support multiple instances per env for testing. +# https://github.com/networknt/light-doc/blob/master/docs/content/design/env-segregation.md +# This tag should only be set for testing env, not production. The production certification process will enforce it. +server.environment: dev + +# Build number, to be set by teams for auditing purposes. +# Allows teams to audit the value and set it according to their release management processes +server.buildNumber: latest + +#-------------------------------------------------------------------------------- +# security.yml +#-------------------------------------------------------------------------------- +# Enable JWT verification flag. +security.enableVerifyJwt: set-real-value-here + +#-------------------------------------------------------------------------------- +# datasource.yml +#-------------------------------------------------------------------------------- +datasource.jdbcUrl: set-real-value-here +datasource.username: set-real-value-here + +datasource.maximumPoolSize: set-real-value-here +datasource.prepStmtCacheSize: set-real-value-here +datasource.prepStmtCacheSqlLimit: set-real-value-here +datasource.conectionTimeout: set-real-value-here + +#-------------------------------------------------------------------------------- +# secret.yml +#-------------------------------------------------------------------------------- +# Sever section +# Key store password, the path of keystore is defined in server.yml +secret.serverKeystorePass: set-real-value-here +# Key password, the key is in keystore +secret.serverKeyPass: set-real-value-here +# Trust store password, the path of truststore is defined in server.yml +secret.serverTruststorePass: set-real-value-here + +# Client section +# Key store password, the path of keystore is defined in server.yml +secret.clientKeystorePass: set-real-value-here +# Key password, the key is in keystore +secret.clientKeyPass: set-real-value-here +# Trust store password, the path of truststore is defined in server.yml +secret.clientTruststorePass: set-real-value-here +# Authorization code client secret for OAuth2 server +secret.authorizationCodeClientSecret: set-real-value-here +# Client credentials client secret for OAuth2 server +secret.clientCredentialsClientSecret: set-real-value-here +# Fresh token client secret for OAuth2 server +secret.refreshTokenClientSecret: set-real-value-here +# Key distribution client secret for OAuth2 server +secret.keyClientSecret: set-real-value-here + +# Consul section +# Consul Token for service registry and discovery +secret.consulToken: set-real-value-here + +# EmailSender password +secret.emailPassword: set-real-value-here + +# Database Section +secret.mysqlDatabasePassword: set-real-value-here +secret.oracleDatabasePassword: set-real-value-here diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/openapiSecurity.yml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/openapiSecurity.yml.rocker.raw new file mode 100644 index 000000000..2d59fa349 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/openapiSecurity.yml.rocker.raw @@ -0,0 +1,36 @@ +# Security configuration for openapi-security in light-rest-4j. It is a specific config +# for OpenAPI framework security. It is introduced to support multiple frameworks in the +# same server instance. If this file cannot be found, the generic security.yml will be +# loaded for backward compatibility. +--- +# Enable JWT verification flag. +enableVerifyJwt: false + +# Enable JWT scope verification. Only valid when enableVerifyJwt is true. +enableVerifyScope: true + +# User for test only. should be always be false on official environment. +enableMockJwt: false + +# JWT signature public certificates. kid and certificate path mappings. +jwt: + certificate: + '100': primary.crt + '101': secondary.crt + clockSkewInSeconds: 60 + +# Enable or disable JWT token logging +logJwtToken: true + +# Enable or disable client_id, user_id and scope logging. +logClientUserScope: false + +# Enable JWT token cache to speed up verification. This will only verify expired time +# and skip the signature verification as it takes more CPU power and long time. +enableJwtCache: true + +# If you are using light-oauth2, then you don't need to have oauth subfolder for public +# key certificate to verify JWT token, the key will be retrieved from key endpoint once +# the first token is arrived. Default to false for dev environment without oauth2 server +# or official environment that use other OAuth 2.0 providers. +bootstrapFromKeyService: false diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/openapiValidator.yml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/openapiValidator.yml.rocker.raw new file mode 100644 index 000000000..a37638ab1 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/openapiValidator.yml.rocker.raw @@ -0,0 +1,8 @@ +# This is specific OpenAPI validator configuration file. It is introduced to support multiple +# frameworks in the same server instance and it is recommended. If this file cannot be found, +# the generic validator.yml will be loaded as a fallback. +--- +# Enable request validation. Response validation is not done on the server but client. +enabled: true +# Log error message if validation error occurs +logError: true diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/pojo.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/pojo.rocker.raw new file mode 100644 index 000000000..38afae1ea --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/pojo.rocker.raw @@ -0,0 +1,13 @@ +@import com.jsoniter.any.Any +@option discardLogicWhitespace=true +@import java.util.Map +@import java.util.List +@args (String modelPackage, String className, String classVarName, List> props) +package @modelPackage; +data class @className ( + @for ((i, prop): props) { + @if(i.index() < props.size() - 1) {val @prop.get("name"): @prop.get("type"),} + @if(i.index() == props.size() - 1) {val @prop.get("name"): @prop.get("type")} + } +) + diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/primaryCrt.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/primaryCrt.rocker.raw new file mode 100644 index 000000000..34f9272f8 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/primaryCrt.rocker.raw @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDmzCCAoOgAwIBAgIEHnAgtDANBgkqhkiG9w0BAQsFADB+MQswCQYDVQQGEwJDQTEQMA4GA1UE +CBMHT250YXJpbzEUMBIGA1UEBxMLTWlzc2lzc2F1Z2ExJjAkBgNVBAoTHU5ldHdvcmsgTmV3IFRl +Y2hub2xvZ2llcyBJbmMuMQwwCgYDVQQLEwNERVYxETAPBgNVBAMTCFN0ZXZlIEh1MB4XDTE2MDkw +MTE2MTYxNVoXDTI2MDcxMTE2MTYxNVowfjELMAkGA1UEBhMCQ0ExEDAOBgNVBAgTB09udGFyaW8x +FDASBgNVBAcTC01pc3Npc3NhdWdhMSYwJAYDVQQKEx1OZXR3b3JrIE5ldyBUZWNobm9sb2dpZXMg +SW5jLjEMMAoGA1UECxMDREVWMREwDwYDVQQDEwhTdGV2ZSBIdTCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBALrlxMtDb60DogElf4TBz504tRheZimAE0dJL/Yby4nacJdqvc5l4z+WWpDf +rI9krQ2Yi9yvhwAP+PrR6gWcIqWP4cpNE7XIAUDgr4CtyI7CptT/lpjtbkz4DGCMmaeDn0jqHqJt +SeSZGfwVu5zAGm8n4sHatjnnxBI/iWzkTII3V4xv0WeK37szNTEd+ly2ag7n2IV5zNnYmqZTeMQm +J2ENS+IwAG3ENtiVtrVTx/2bGtqutJjtdxsN58/cUG/guRyMT6OPI8Yi3ZzevdvRbxadyhEl/Kaw +6vJcdxmJI3tp4lx+p6sAxOWa7aapJe4JxutAQqzv0GKdVjoHKQ1wB60CAwEAAaMhMB8wHQYDVR0O +BBYEFIPF9SBd06RWU1eDL73CKfy01lavMA0GCSqGSIb3DQEBCwUAA4IBAQAoaKZGOak3Upz/ordF +slZoJuZlCu7jnKQEjYwHf3DNxcd1WmgFPtMcna6pW0VUxPIfidEA6VCMsGoK1RvshB0SjrRdCht6 +5qPXs9kV3NW0WvMiwDSYZZ9HgaZ9efTe5E9Fzc7ltKrE43L6k8NJcaEEWEdpdjFbrAqH4I+j/Vro +K3OhIo062fXjas5ipL4gF+3ECImjWzirQP8UiAfM0/36x7rtAu3btH/qI9hSyx39LBPPE5AsDJZ4 +dSMwNTW1gqmBAZIj+zQ/RD5dyWfPwON7Q+t96YbK6WBuYo0xy+I+PjcUgrWYWP3N24hlq8ZBIei+ +BudoEVJlIlmS0aRCuP8n +-----END CERTIFICATE----- diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/secondaryCrt.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/secondaryCrt.rocker.raw new file mode 100644 index 000000000..dfbd3ceac --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/secondaryCrt.rocker.raw @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDkzCCAnugAwIBAgIEUBGbJDANBgkqhkiG9w0BAQsFADB6MQswCQYDVQQGEwJDQTEQMA4GA1UE +CBMHT250YXJpbzEQMA4GA1UEBxMHVG9yb250bzEmMCQGA1UEChMdTmV0d29yayBOZXcgVGVjaG5v +bG9naWVzIEluYy4xDDAKBgNVBAsTA0FQSTERMA8GA1UEAxMIU3RldmUgSHUwHhcNMTYwOTIyMjI1 +OTIxWhcNMjYwODAxMjI1OTIxWjB6MQswCQYDVQQGEwJDQTEQMA4GA1UECBMHT250YXJpbzEQMA4G +A1UEBxMHVG9yb250bzEmMCQGA1UEChMdTmV0d29yayBOZXcgVGVjaG5vbG9naWVzIEluYy4xDDAK +BgNVBAsTA0FQSTERMA8GA1UEAxMIU3RldmUgSHUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCqYfarFwug2DwpG/mmcW77OluaHVNsKEVJ/BptLp5suJAH/Z70SS5pwM4x2QwMOVO2ke8U +rsAws8allxcuKXrbpVt4evpO1Ly2sFwqB1bjN3+VMp6wcT+tSjzYdVGFpQAYHpeA+OLuoHtQyfpB +0KCveTEe3KAG33zXDNfGKTGmupZ3ZfmBLINoey/X13rY71ITt67AY78VHUKb+D53MBahCcjJ9YpJ +UHG+Sd3d4oeXiQcqJCBCVpD97awWARf8WYRIgU1xfCe06wQ3CzH3+GyfozLeu76Ni5PwE1tm7Dhg +EDSSZo5khmzVzo4G0T2sOeshePc5weZBNRHdHlJA0L0fAgMBAAGjITAfMB0GA1UdDgQWBBT9rnek +spnrFus5wTszjdzYgKll9TANBgkqhkiG9w0BAQsFAAOCAQEAT8udTfUGBgeWbN6ZAXRI64VsSJj5 +1sNUN1GPDADLxZF6jArKU7LjBNXn9bG5VjJqlx8hQ1SNvi/t7FqBRCUt/3MxDmGZrVZqLY1kZ2e7 +x+5RykbspA8neEUtU8sOr/NP3O5jBjU77EVec9hNNT5zwKLevZNL/Q5mfHoc4GrIAolQvi/5fEqC +8OMdOIWS6sERgjaeI4tXxQtHDcMo5PeLW0/7t5sgEsadZ+pkdeEMVTmLfgf97bpNNI7KF5uEbYnQ +NpwCT+NNC5ACmJmKidrfW23kml1C7vr7YzTevw9QuH/hN8l/Rh0fr+iPEVpgN6Zv00ymoKGmjuuW +owVmdKg/0w== +-----END CERTIFICATE----- diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/secret.yml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/secret.yml.rocker.raw new file mode 100644 index 000000000..a80784bc4 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/secret.yml.rocker.raw @@ -0,0 +1,48 @@ +# This file contains all the secrets for the server and client in order to manage and +# secure all of them in the same place. In Kubernetes, this file will be mapped to +# Secrets and all other config files will be mapped to mapConfig + +--- + +# Sever section + +# Key store password, the path of keystore is defined in server.yml +serverKeystorePass: password + +# Key password, the key is in keystore +serverKeyPass: password + +# Trust store password, the path of truststore is defined in server.yml +serverTruststorePass: password + + +# Client section + +# Key store password, the path of keystore is defined in server.yml +clientKeystorePass: password + +# Key password, the key is in keystore +clientKeyPass: password + +# Trust store password, the path of truststore is defined in server.yml +clientTruststorePass: password + +# Authorization code client secret for OAuth2 server +authorizationCodeClientSecret: f6h1FTI8Q3-7UScPZDzfXA + +# Client credentials client secret for OAuth2 server +clientCredentialsClientSecret: f6h1FTI8Q3-7UScPZDzfXA + +# Fresh token client secret for OAuth2 server +refreshTokenClientSecret: f6h1FTI8Q3-7UScPZDzfXA + +# Key distribution client secret for OAuth2 server +keyClientSecret: f6h1FTI8Q3-7UScPZDzfXA + +# Consul service registry and discovery + +# Consul Token for service registry and discovery +# consulToken: the_one_ring + +# EmailSender password +emailPassword: change-to-real-password diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/server.yml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/server.yml.rocker.raw new file mode 100644 index 000000000..d26dd8346 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/server.yml.rocker.raw @@ -0,0 +1,63 @@ +@args (String serviceId, Boolean enableHttp, String httpPort, Boolean enableHttps, String httpsPort, Boolean enableRegistry, String version) +# Server configuration +--- +# This is the default binding address if the service is dockerized. +ip: 0.0.0.0 + +# Http port if enableHttp is true. It will be ignored if dynamicPort is true. +httpPort: @httpPort + +# Enable HTTP should be false by default. It should be only used for testing with clients or tools +# that don't support https or very hard to import the certificate. Otherwise, https should be used. +# When enableHttp, you must set enableHttps to false, otherwise, this flag will be ignored. There is +# only one protocol will be used for the server at anytime. If both http and https are true, only +# https listener will be created and the server will bind to https port only. +enableHttp: @enableHttp + +# Https port if enableHttps is true. It will be ignored if dynamicPort is true. +httpsPort: @httpsPort + +# Enable HTTPS should be true on official environment and most dev environments. +enableHttps: @enableHttps + +# Http/2 is enabled by default for better performance and it works with the client module +enableHttp2: true + +# Keystore file name in config folder. KeystorePass is in secret.yml to access it. +keystoreName: server.keystore + +# Flag that indicate if two way TLS is enabled. Not recommended in docker container. +enableTwoWayTls: false + +# Truststore file name in config folder. TruststorePass is in secret.yml to access it. +truststoreName: server.truststore + +# Unique service identifier. Used in service registration and discovery etc. +serviceId: @serviceId + +# Flag to enable self service registration. This should be turned on on official test and production. And +# dyanmicPort should be enabled if any orchestration tool is used like Kubernetes. +enableRegistry: @enableRegistry + +# Dynamic port is used in situation that multiple services will be deployed on the same host and normally +# you will have enableRegistry set to true so that other services can find the dynamic port service. When +# deployed to Kubernetes cluster, the Pod must be annotated as hostNetwork: true +dynamicPort: false + +# Minimum port range. This define a range for the dynamic allocated ports so that it is easier to setup +# firewall rule to enable this range. Default 2400 to 2500 block has 100 port numbers and should be +# enough for most cases unless you are using a big bare metal box as Kubernetes node that can run 1000s pods +minPort: 2400 + +# Maximum port rang. The range can be customized to adopt your network security policy and can be increased or +# reduced to ease firewall rules. +maxPort: 2500 + +# environment tag that will be registered on consul to support multiple instances per env for testing. +# https://github.com/networknt/light-doc/blob/master/docs/content/design/env-segregation.md +# This tag should only be set for testing env, not production. The production certification process will enforce it. +# environment: test1 + +# Build Number +# Allows teams to audit the value and set it according to their release management processes +buildNumber: @version \ No newline at end of file diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/settingsGradleKts.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/settingsGradleKts.rocker.raw new file mode 100644 index 000000000..dcf063d85 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/settingsGradleKts.rocker.raw @@ -0,0 +1,3 @@ +@import com.jsoniter.any.Any +@args (Any config) +rootProject.name = "@config.get("artifactId")" \ No newline at end of file diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/swaggerSecurity.yml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/swaggerSecurity.yml.rocker.raw new file mode 100644 index 000000000..b194e258c --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/swaggerSecurity.yml.rocker.raw @@ -0,0 +1,36 @@ +# Security configuration for swagger-security in light-rest-4j. It is a specific config +# for Swagger framework security. It is introduced to support multiple frameworks in the +# same server instance. If this file cannot be found, the generic security.yml will be +# loaded for backward compatibility. +--- +# Enable JWT verification flag. +enableVerifyJwt: false + +# Enable JWT scope verification. Only valid when enableVerifyJwt is true. +enableVerifyScope: true + +# User for test only. should be always be false on official environment. +enableMockJwt: false + +# JWT signature public certificates. kid and certificate path mappings. +jwt: + certificate: + '100': primary.crt + '101': secondary.crt + clockSkewInSeconds: 60 + +# Enable or disable JWT token logging +logJwtToken: true + +# Enable or disable client_id, user_id and scope logging. +logClientUserScope: false + +# Enable JWT token cache to speed up verification. This will only verify expired time +# and skip the signature verification as it takes more CPU power and long time. +enableJwtCache: true + +# If you are using light-oauth2, then you don't need to have oauth subfolder for public +# key certificate to verify JWT token, the key will be retrieved from key endpoint once +# the first token is arrived. Default to false for dev environment without oauth2 server +# or official environment that use other OAuth 2.0 providers. +bootstrapFromKeyService: false diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/swaggerValidator.yml.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/swaggerValidator.yml.rocker.raw new file mode 100644 index 000000000..78db16de1 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/swaggerValidator.yml.rocker.raw @@ -0,0 +1,8 @@ +# This is specific Swagger validator configuration file. It is introduced to support multiple +# frameworks in the same server instance and it is recommended. If this file cannot be found, +# the generic validator.yml will be loaded as a fallback. +--- +# Enable request validation. Response validation is not done on the server but client. +enabled: true +# Log error message if validation error occurs +logError: true diff --git a/light-rest-4j/src/main/resources/templates.restkotlin/testServer.rocker.raw b/light-rest-4j/src/main/resources/templates.restkotlin/testServer.rocker.raw new file mode 100644 index 000000000..63b407df0 --- /dev/null +++ b/light-rest-4j/src/main/resources/templates.restkotlin/testServer.rocker.raw @@ -0,0 +1,52 @@ +@args (String handlerPackage) +package @handlerPackage; + +import com.networknt.server.Server; +import org.junit.rules.ExternalResource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.concurrent.atomic.AtomicInteger; + +import com.networknt.server.Server; +import com.networknt.server.ServerConfig; + +public class TestServer extends ExternalResource { + static final Logger logger = LoggerFactory.getLogger(TestServer.class); + + private static final AtomicInteger refCount = new AtomicInteger(0); + private static Server server; + + private static final TestServer instance = new TestServer(); + + public static TestServer getInstance () { + return instance; + } + + private TestServer() { + + } + + public ServerConfig getServerConfig() { + return Server.config; + } + + @@Override + protected void before() { + try { + if (refCount.get() == 0) { + Server.start(); + } + } + finally { + refCount.getAndIncrement(); + } + } + + @@Override + protected void after() { + refCount.getAndDecrement(); + if (refCount.get() == 0) { + Server.stop(); + } + } +} diff --git a/light-rest-4j/src/test/java/com/networknt/codegen/OpenApiKotlinGeneratorTest.java b/light-rest-4j/src/test/java/com/networknt/codegen/OpenApiKotlinGeneratorTest.java new file mode 100644 index 000000000..7b855e735 --- /dev/null +++ b/light-rest-4j/src/test/java/com/networknt/codegen/OpenApiKotlinGeneratorTest.java @@ -0,0 +1,133 @@ +package com.networknt.codegen; + +import com.jsoniter.JsonIterator; +import com.jsoniter.any.Any; +import com.networknt.codegen.rest.OpenApiGenerator; +import com.thoughtworks.qdox.JavaProjectBuilder; +import com.thoughtworks.qdox.model.JavaClass; +import com.thoughtworks.qdox.model.JavaField; +import com.thoughtworks.qdox.model.JavaPackage; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Scanner; + +public class OpenApiKotlinGeneratorTest { + public static String targetPath = "/tmp/openapikotlin"; + public static String configName = "/config.json"; + public static String openapiJson = "/openapi.json"; + public static String openapiYaml = "/openapi.yaml"; + public static String openapiNoServersYaml = "/openapi-noServers.yaml"; + public static String packageName = "com.networknt.petstore.model"; + + @BeforeClass + public static void setUp() throws IOException { + // create the output directory + Files.createDirectories(Paths.get(targetPath)); + } + + // @AfterClass + public static void tearDown() throws IOException { + Files.deleteIfExists(Paths.get(targetPath)); + } + + @Test + public void testGeneratorJson() throws IOException { + Any anyConfig = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(configName), 1024).readAny(); + Any anyModel = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(openapiJson), 1024).readAny(); + + OpenApiGenerator generator = new OpenApiGenerator(); + generator.generate(targetPath, anyModel, anyConfig); + } + + @Test + public void testGeneratorYaml() throws IOException { + Any anyConfig = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(configName), 1024).readAny(); + String strModel = new Scanner(OpenApiGeneratorTest.class.getResourceAsStream(openapiYaml), "UTF-8").useDelimiter("\\A").next(); + OpenApiGenerator generator = new OpenApiGenerator(); + generator.generate(targetPath, strModel, anyConfig); + } + + @Test + public void testGetOperationList() throws IOException { + Any anyModel = JsonIterator.parse(SwaggerGeneratorTest.class.getResourceAsStream(openapiJson), 1024).readAny(); + OpenApiGenerator generator = new OpenApiGenerator(); + List list = generator.getOperationList(anyModel); + System.out.println(list); + } + + @Test + public void testGetFramework() { + OpenApiGenerator generator = new OpenApiGenerator(); + Assert.assertEquals("openapi", generator.getFramework()); + } + + @Test + public void testGetConfigSchema() throws IOException { + OpenApiGenerator generator = new OpenApiGenerator(); + ByteBuffer bf = generator.getConfigSchema(); + Assert.assertNotNull(bf); + System.out.println(bf.toString()); + } + @Test + public void testNoServersGeneratorYaml() throws IOException { + Any anyConfig = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(configName), 1024).readAny(); + String strModel = new Scanner(OpenApiGeneratorTest.class.getResourceAsStream(openapiNoServersYaml), "UTF-8").useDelimiter("\\A").next(); + OpenApiGenerator generator = new OpenApiGenerator(); + generator.generate(targetPath, strModel, anyConfig); + } + + @Test + public void testInvalidVaribleNameGeneratorYaml() throws IOException { + Any anyConfig = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(configName), 1024).readAny(); + String strModel = new Scanner(OpenApiGeneratorTest.class.getResourceAsStream(openapiYaml), "UTF-8").useDelimiter("\\A").next(); + OpenApiGenerator generator = new OpenApiGenerator(); + generator.generate(targetPath, strModel, anyConfig); + + File file = new File(targetPath); + JavaProjectBuilder javaProjectBuilder = new JavaProjectBuilder(); + javaProjectBuilder.addSourceTree(file); + JavaPackage javaPackage = javaProjectBuilder.getPackageByName(packageName); + + for (JavaClass javaClass : javaPackage.getClasses()) { + List fields = javaClass.getFields(); + for (JavaClass javaNestedClass : javaClass.getNestedClasses()) { + fields.addAll(javaNestedClass.getFields()); + } + for (JavaField field : fields) { + Assert.assertFalse(field.getName().contains(" ")); + } + } + } + + @Test + public void testInvalidVaribleNameGeneratorJson() throws IOException { + Any anyConfig = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(configName), 1024).readAny(); + Any anyModel = JsonIterator.parse(OpenApiGeneratorTest.class.getResourceAsStream(openapiJson), 1024).readAny(); + + OpenApiGenerator generator = new OpenApiGenerator(); + generator.generate(targetPath, anyModel, anyConfig); + + File file = new File(targetPath); + JavaProjectBuilder javaProjectBuilder = new JavaProjectBuilder(); + javaProjectBuilder.addSourceTree(file); + JavaPackage javaPackage = javaProjectBuilder.getPackageByName(packageName); + + for (JavaClass javaClass : javaPackage.getClasses()) { + List fields = javaClass.getFields(); + for (JavaClass javaNestedClass : javaClass.getNestedClasses()) { + fields.addAll(javaNestedClass.getFields()); + } + for (JavaField field : fields) { + Assert.assertFalse(field.getName().contains(" ")); + } + } + } +}