From 9d9c0e814764ad73ccad2e411019039246d856b3 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 10 Feb 2019 11:48:38 -0500 Subject: [PATCH] fixes #222 add Kotlin OpenAPI generator --- .../src/main/resources/config/service.yml | 51 +- .../java/com/networknt/codegen/Generator.java | 1 + .../src/test/resources/config/service.yml | 2 +- .../src/main/resources/config/service.yml | 2 +- .../codegen/rest/OpenApiKotlinGenerator.java | 598 ++++++++++++++++++ .../networknt/codegen/rest/UrlGenerator.java | 2 + .../resources/binaries/gradle-wrapper.jar | Bin 0 -> 52804 bytes .../templates.restkotlin/LICENSE.rocker.raw | 201 ++++++ .../templates.restkotlin/README.md.rocker.raw | 38 ++ .../buildGradleKts.rocker.raw | 102 +++ .../templates.restkotlin/buildSh.rocker.raw | 52 ++ .../templates.restkotlin/clientYml.rocker.raw | 74 +++ .../dockerfile.rocker.raw | 6 + .../dockerfileredhat.rocker.raw | 6 + .../templates.restkotlin/enumClass.rocker.raw | 6 + .../enumInline.rocker.raw | 34 + .../templates.restkotlin/gitignore.rocker.raw | 23 + .../gradleProperties.rocker.raw | 27 + .../gradleWrapperProperties.rocker.raw | 5 + .../templates.restkotlin/gradlew.rocker.raw | 169 +++++ .../gradlewBat.rocker.raw | 84 +++ .../templates.restkotlin/handler.rocker.raw | 28 + .../handlerTest.rocker.raw | 40 ++ .../junitPlatformProperties.rocker.raw | 2 + .../lightTestServerKt.rocker.raw | 180 ++++++ .../templates.restkotlin/logback.rocker.raw | 97 +++ .../templates.restkotlin/maskYml.rocker.raw | 25 + .../templates.restkotlin/model.rocker.raw | 9 + .../openapi/config.rocker.raw | 17 + .../openapi/handlerYml.rocker.raw | 105 +++ .../openapi/service.yml.rocker.raw | 30 + .../openapi/values.rocker.raw | 97 +++ .../openapiSecurity.yml.rocker.raw | 36 ++ .../openapiValidator.yml.rocker.raw | 8 + .../templates.restkotlin/pojo.rocker.raw | 13 + .../primaryCrt.rocker.raw | 19 + .../secondaryCrt.rocker.raw | 19 + .../secret.yml.rocker.raw | 48 ++ .../server.yml.rocker.raw | 63 ++ .../settingsGradleKts.rocker.raw | 3 + .../swaggerSecurity.yml.rocker.raw | 36 ++ .../swaggerValidator.yml.rocker.raw | 8 + .../testServer.rocker.raw | 52 ++ .../codegen/OpenApiKotlinGeneratorTest.java | 133 ++++ 44 files changed, 2499 insertions(+), 52 deletions(-) create mode 100644 light-rest-4j/src/main/java/com/networknt/codegen/rest/OpenApiKotlinGenerator.java create mode 100644 light-rest-4j/src/main/resources/binaries/gradle-wrapper.jar create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/LICENSE.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/README.md.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/buildGradleKts.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/buildSh.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/clientYml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/dockerfile.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/dockerfileredhat.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/enumClass.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/enumInline.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/gitignore.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/gradleProperties.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/gradleWrapperProperties.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/gradlew.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/gradlewBat.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/handler.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/handlerTest.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/junitPlatformProperties.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/lightTestServerKt.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/logback.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/maskYml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/model.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/openapi/config.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/openapi/handlerYml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/openapi/service.yml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/openapi/values.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/openapiSecurity.yml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/openapiValidator.yml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/pojo.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/primaryCrt.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/secondaryCrt.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/secret.yml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/server.yml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/settingsGradleKts.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/swaggerSecurity.yml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/swaggerValidator.yml.rocker.raw create mode 100644 light-rest-4j/src/main/resources/templates.restkotlin/testServer.rocker.raw create mode 100644 light-rest-4j/src/test/java/com/networknt/codegen/OpenApiKotlinGeneratorTest.java 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 0000000000000000000000000000000000000000..d4046a6637410b7fe15a04b5b0746c0ba4b7a365 GIT binary patch literal 52804 zcmZ^Kb8x3kw{4P%or!JRwr$(CJ<%_=ZQGdGwryu(>(03cb-wSux9jQWuhmt%t9Naz zT3bQ-2PiZU5F{kfdESWx(BB2}AHTl~>hG2nRS~3>loO->0i^IRK-DhfYvsQHtG^rN ze*E__1X{n|sXBw3l7MXXC?j~tRC8uPks3mEDq0a#( zDThT`lx^swC5J^uqy5cT7nrA*r&yR68JKCVVq^L*(0{)?^1njeoeUiuOr8FpBkNzGz$1`AKuLe`6#slg z{XdV2dYBr!y4X9>8QT~-J147$dn2zPeOJfRcxu9qV$?B)1N{(6V03RwK*3htd{dys zwptk0l(zN`uR^Uv-DR;9@+vyhU2h`nlJ^>EGMH?n|hx-k`SC5E?a4^Mmej@@YKMuK{1q&sAJFP9`VU1xAvvj{6A$nF3JdcO zE)E7h<~g=J99)SP03c2(@+MbNx-^}Yk$WzqU6PZ_MPMk|8c(lfwary>ER_+wO!ccq z_zVw0h?UvZ;-xIKy(1!>&&$GRD?y%o2x|zrxJL@{gZvD;h`m6|^HH z5-wG1iJ-jZ%vyc790sx%D5p=qoh?oK7NIN3ahD^LB#=qLF&SN3692#cV0#=?>7ky}&Z@s;RPRq0M9E|_2=FO^|Fsl1Ca zQp_T-q05d@Ue^u_gQ8|_C7^8Gl_(5{f)T!Kp?glvsfAa7hk}u-ntv#hAen-2+gUrj^cYiZjX$;N%>H zq4&6&NZ+9%ExZ?HPm`j9%c`V=3iJkq9chtdM^cSDQFFDRGjn#(BLVz8m1 z3BLUXGb%`wDp;#`M`&)LnKQd4ADX1>UtwWjs~ri_VBxvbF@#oC0Cg{0cHc!*lX+QtL>cieL_yHNIMtE?EfkWog&c)iU%tH?oL8NX_LNh8x>1Ytw99;W z6UIx|Xws}pJd9bYDt}*5RaTULPYZIoN89vjYl*eNE(f{NLpw#Lboe2#T8_>OD$_WK z8z(btr@a|oK64oJOAblVf*{SsOzYrJckmA68_}q?TaYt9D3!j^RE@{_bSo@wo;9o% z_)zmti|6|Ye32TaJEr6J&B)3dLqsz&^SaXNR)31Idsry#ePS_^g)KjX&HZ$J`Uh`~ zv6=!>AG_zzs4_?Vi_;5##?Ns}0T{gAAd#+!GA zIbJ+3VxrAFOWZv?TC?+Y7J9-h-&682;DQcCdFnh<;Q3RKbFqVXQBzPfL>$_}?x@+U zU)V)+Xq&+((Gk9owaBw@7*4?WLJ~I#x1PbdBj;c&QDTR^(i%J+6gl0{#Zjay8k(E( z;>2ik801l)>$7`v$u65f{4p(bX9Qzl7Upz%V#Is2q@AYfbv!wpff789V6sq5`ykWg z3F`Gke(4;hEHlQ%8e(d1TaCfo6kOaB z7VSF5c?a8LYrN_8Vtv#4{IUjsxxRr2aR$LQW6@97m5H2y3)GE3M?^?Y9I~bbsyO@- zG#rf<=kR&gSnjG#&vbC7u2Aws@I`4ogOmg3_n0z{=LEU6DQS=AI8)0u)kH*kGX1*o z!2J=b$Z>6kq_8LEa0k0M0@&mJu>VBOdnMeD{oS!(gg{gmKicAH(*w+jx2856a{#l- zNtN<@d{sA-?6b%PNVcdUf$RdO7*&#a7VcgcT&2>FTau0FS^sSGkH)ZF!Ae%{$)| zsW-)lA>+fo0}WZtRRDSTcM^HbRSoTbd@;10T+z%#G1b8@d11>{*j*^if+<$WTgP8{ z3$|iEbUN9!?PPIsoXiu}{9A`5|E(Np|G5|vvbT3} zcKKTo$r@VPDZ3atxtRV}MHH>3slB0w$lKolR`Ijfh7r^O1SNAwPFV^&j|D2E4kN)J zg{vsCj$TY8rHlsoG=E=F%g(n)WqCPsa1vN_Nh1x9mcC<%aI#GKIB8~F6%*-Z$2)JR55 zWgsa;vi`YjYGa_>5Ai7Bz#jWjHxxSZ_@Xc@{-V)O{;2Tkq%DS?@KXuIM>!*ZZ2^QQ z{{{f$I4r{m9L+doK;1sc*0{WAn@#eSmag&$w>1CKO@MNxOG{0F!(8pcVp~p`Et;!! z{>9f=;4Zr`z22m$#!75gnKD%+3A|0C4}FSYJM^^pgSDbkj0>xkc1l9N%)w9Bc?vI6>tIEVVIfS;VS2ku5A{ywTWmDXN z*|byx=ZQ+5+8)tnP#{4Kr3XYSsEbY<7qC>hSRefSPd8MH)x;Z-fS4$dAOyj*pHHYh z(9(ivF63UPSfZO1q7NVkqrB!b^%SFfzh>_jk*gyvDLo>Dfn`tj-#5HfprFK|`36oz zO+r{^Lspt?WAypMrk% zeZMxHMrZw?7Cda8dQ_!yQKr!G+CnFrpl(*Ei&E7b9&M;5S{c%6ElJ1aA77_Y{;G?> zqmt*SO;|%viNsX7cBqFyA=Y(>U=K{_sJYUTDK>YfOmA!}-0yCtsiHpGZJf+0zy07A zu7zC5Tj;p?f)HAoDvdA*(5ffhtZ_7}JePS&iSpi==tmLR zvx@)K9l@p)RFnwWk)tz%ZAhWBBuzrpRd5Hclk?R`wxvQ@$c(vE5-NbVVO9KugtW;> z%Tz(2K^)5pnIl?r=JR9a^m`fM>t0}&HhTD?DRQ1>_7ay1a1)5;dRjPl0?9oqU$~ED z093mm6N^mPupjzO#vOh`=u3LRbxzH|f%InN=x>AIhx#t3%n zVQN+wAE*m{(i2?@m`p!}@largH`!xM9ocSQwgHPe z&dt?6-Y$!TNOGHu{y|=0bc<2+VXo69VtKHx=L+M>(}gCsGwUfT($?sC<`mZw+1{zb zQ|A`q?(_qy``7czNx{uTkAHRn^Ovc-rCjSKO4koWo>yZL6k4Cc-`ekr7DW%7AtK~ z?KQ0{)rG-{if|Q30w{Tt5U3iu-zJ(iAL?BsR#x*Pd=KKRyaP(qqd~vN+Na#RosTCc zulf1?Ks5R*0u?96JPLLxb0otkAr17zbY8*{#p($5+5_W&x)8k-#gr^$`=z0zVH36A zp56qh^w-B~?28}f1?6T^+!k_O01oG?9IPx8b(LDv&a%Hd#u>JvsZATsqH?CtrMa>e(a?Q4 zOb4hiYA8S475+K5 zNlWdDQTS7}cZ)S(knD}QLWp%8MTpkpOZv0RZp?$8AlotSw9QuvU@=w0K(ig*zzgIk zkaG}<@n^()ahPW7P00`XPSm zPyKe6q(V+5s%}s8|LUedAcvdSf9Z=$N+2MDf6m&HcFrz_Ha7q1nc139J}O7e{N&8( z-03<5ph6hSNZ}?3+*pWkl5wkuadjAok7mbY?M%|0&JaH@)bQ1`t^iuCYE{ztDQb$6 zvlOe#Tg%HgcuRfpEayoLZ!@1*4rYPrVOXV@L1vi|(r=aT5RkhfAs^X~PW3^_-}>fR?a~Bx>Z3c>0FpGtEeG+}QNg^xzi~SxggkG&pn9LS#mTMa=kF8r|?+Vxoz1!BOJeNK( zcA~gkQ3FN+!kr2SewOXegY)1_NA7Qt?LQ!Y427FI`e(QgF;NYTphtczf=L%hqBzm( zQ%09NFbQzCX<|u}m7r{w-_!s-j&FiKj|KWEK2!ktXVj+5R32Sn-Ht6m>raTtSV{BL zWJW8}rY|PS^pzYh!DVW(>n$(NcRLDCRLEj$FE{J9bQ>OEmFzE5H)U(-szkc4G;5b2 zUF%J=)G#8+YiVt+SBWam9&wTBv&?N(XUZ&SBb&MFww8+)cx}-sUtBFsb*OnGQh&w| z)_Abwi>8^i%i35<#locOO`Ti*3ag(kQ^+}JG4TC^Lz7YR98y?bRIjXWEAj|%(2yrv zv2vVBsh*A&x`#slm1Q4-#9oywlbYfmrH5ZTHa1kVI(cX-BjQp5RV}7tFFnNN>RbpA zycHm-5@ok8A>28Wt6ehgb!OJ#jI=LMl)#6~>@Yi%6`MEW8c_|RokoVgV(L&XF~bah zxq}`#6DyFjA*z8qiD6dFVxqJ_0rp-vl^J^`V;*POCd6NwyR9AG8$pJrPd^ml$)y99^jpz1KoK*e z?07B5#_1frBvZttJK0SGYHEfU#u_RfANzD29r!@5X0`)(AG50uPZv`u0&%9JS|R`q%` z)hyLLW-n-hrM9SfqCrK-Cz`r|OFx>T8dtOGjMGUs+L_Q(N#2g?F> zCFZWvs8qtY3|45_k!E70)SB@QL?vC-rpIn9#zZL~MiE`oqNzdFh#}Y$1jpMTxI!c+ zC!-ZFGCmG6XV%4KILtE9K<-gEYM8gM5$=;^y!dD))NoOegyc$LC|@XPOgW<)8Yl0fO*^Q-gaF4Q|Ot^CJ&VoN)?p9thp>ApIqf=`Pz z%@G%oeyp~c|1x?@c6}2(XSt>riDVzr8V*4~B4GnvslLVrO&5}`8;IyBR>OVjv4UMp zsgW0(HMP3wPyI8EKbB!~kmO$P&Q6HRw(Zt-PO0O4SXD>$bBEy(M<5xtbM5yhGv-#V zTMbO_8-yOWPFyYRP;~nEA@uD2{e`JYPv1yDI}Iw`g#5-<`&(15eU(?~57V-^O2UG6Us++e z2$YbXf;+u#RK;v=LUJ|1;2iZAwq*O0+6o_to$@>YJZ=p z81ps`?Rta-In!3?r-p4AXV*QTN~8=%lc(s}FfryS6wT!1aiw4wa>j3wcO4C#=b9Iz^NM2Hjh6rxqdn}TKoO^#$FAti+A=-IRuPgc#@aNEM_AsiVlu+5F*JY89rFIm0MD;#FAiI_w321|cI6{MFXhcfabhe8F7`Bq}VG$~wXx?~N)BiV z%5{SzSutVE9X7Ck=J!)XjG>)D{m;VI76V1{ND`VI7u~47!}OJ3MyS4n_0?ZXKfs6G zI8F1$?eAt({388huoj!26wideLi+71KcodmYG1OWibobLsJ^lIj&~s4%%a?eJCKGp z7B`c!xJmpx)HefZPl?}x`g>Gg-FVg~1BlAPw3V@EZm=d)uh#vPX~b^?5Ju0ppI z^;e0)YF&F$EWbS0GDEg<+b%IJo0Zk3yYTKU&l+WdF8T(=*q-_wpfUOFmRN;2!e^1( zai))^iI`GG+FJl+{X*3BW-kpzMV&*a_Ix<%9o2Z;@WH98nsTWN6WAluGuyQ) zxf<%NXMWW2@x~vccX(2mW5*h`A@}*U6LJ$zgl4dhw#Hir74qSkCQkI}EH!~pybfy^ z#wJ@=3$-H9axn!7hUDT_CnQ%Q{=()=NV1w8gA1dw4fx5xAE$k2@!nth2$q1EhD4nX z!x+8Si1Dt3ZSz$;H=Y=J?nl(ot|*}O?x!*g=RGA0l1wHrY>`lAb^uQIOBzRsG8@@cUESmQNQQEt{Ba4D9@+1?1Za*7#KPliA+wb;*z;0oqVZA!UD*}Hh31#>!izGE z>17Z4l%ST+L8LXm{m!TplF2RMT;X?5A6*1!J)RRacqPFjL@=%h81i}Ppd!2y$7Iz| z$?_^bneOq70hX`0sXeo%h0#zqcr}0pQ#KUl@jMOHz=oy^j2IPe#sk{pZmF zrwjEKI2Ev#OzS{5UR8Bb?Y!=+4c9|w=AXVQPL`l<06V@fZe^E-C;jqn=lSUX?J11G z;qZRBI7ORVSc$6&-24OEjMDWWk=MF@uKxbihB&iT;Oh?-UxySNF2ZH+H{&UUZr%`W#27N$Nx7*?&g2fhJ zK-ul2WfCi!w7)+)zhA5cO@EC8rW!KH3bsJ%=}Vv%fh}x8U|Q#FI)abq+%V3SxH4JN zh-J_fg4yEaQkBs$7iHB8uEcj-QJQ5}!NBN8&h2|8z+4Dv^WV>a?;__l8@DA8`X$0J zs=UIwp(x2_<<{jP43>?g=3DFfVXzdv(z@XqYd}|mJnr|}6S{?WLNcA;J|KBBz$X_l zkjk1>`)gipS>@+7Vew^g3p{a$j<(Iq3I>$7{DOb|sW8T<7Nc5A#jC;Oz4T15x~G66 zkcaLQrBPSxk#7{Kx3s+{I#Tcy^O5lcuRlv& zv3SBS76~RV-6?Ky<}f*3pY6nop(h>UFlaJpf~e@phQf&t%|ZxmUvJzS%7=bgvo~7_uXjX@4e>q+LkQ+fNw*|`~E?2u5%b6EUdDwsM_fiH?2KkE9~MIdW~sy zdV^9McoBiU$MI5I?i-hp{n3K;3Rm-c*NzJrN)EGtl|fDVHsr?xr6UX8k3}v4>*s}Y zLaPgm0#{rn|4#y4771(;dh z5d&`2dNA25((6FKBXo}cdVgx)zVxjVWJfAN0Rlnj=)L11B7{w<66LyY-Is6O>gTEp zUY$9O(8CJxow}hcd4Vr_!H1W~%T(B{Z`gS#A_RoNPN?98VkbZRZ~WmM#DW71f>-$7 z7jK_%dvD-r-LPTwQ&EjiFB^XPxryDjP_udJUj_J^GG57kkn}4H@NY<6;Pm`a?ffOV zdDnHHPaSYq2n{*kzx=Gc{eE@@^myCA>6fzGTAYlTs(Br3M$iX*HtkQu%pS$Yvk7EZ zs}Ah}M;!7>ylw~O1g*_dw|L@!`Nhnk?AlS)5o5_XQWSYsvgG*Lg{tOkrj)+&fKq~G zPE=falnn{%UubeRVrBMXCbM-`nA3rFgO5>Tk8UMhbGB^|*9Vrv074x0zriq0MeDqv z`fiwHo}iUhD1JsV7EGJMzC#;2N2HAndwXHv`8 zye<6R9@-@ltaA_RFzLB)i(4W-TZ?N9>VHxQiF;d%!+N-~|h>T)+$j zUo@YXf9X)T&dHXA+hH|}c8AQhmP4jM3O%>aHe-3^z{t*&J`0>Z=RFdVp5hCf2_G)| zgI32Xtb70`@d?r1j@bOnh^&5Uec)bo<8E1dKK11ux}I8hq_^P29?=s}Fi;PcIullS z5&-b#eaOf0CTeptd?B@Qv_G0XK#7c#c_}J4sV?QEoNAnRg$}E)tR@fEe2#oWGCUAQ zC>KVy@#lZKQ@rC-n0`e${ple**KZI2nyU&kRXF)YYlQj9j1t#n+tOw14V0+#hR&U9 zCQ5S`^wd)#YBP+3kRxS^%Et2TK)K%tCD)1x$fA6(w*)|gI@=To@T|!+dc%Z$lYgpn ziJadgX}%_;ZAw;7gNCx8ePepX=w5O+5BcYROVZcKlLP1K`nA=%1#YG;jh|S6aeYgF zpNNfk->{s0@PD$}9zWK_6k+jCMtQ9|k0vK5H@!3_edHZp9{a7XtxbKp_#E2l9foGF zE@gZEvEkd!em-nN*4X2DYQqfLvdrzpNE#{7!aG?gz3R*iWjXoy9aK7-J?Q`AUx_t~ zKohgVzl7S--%b3_#F~h`yPb`_p$Q4&e{yTSQL@ne42U5+7OYy-ZuEgLD}bCnd6vqpmV_v*TpW5dGC<5XBUJN54pBDDi3Ey&3!;a?aIW?3E zsZB7$7UuSrZkBV3%t~4evfvQc2x@X@TF(m+UFi{j|H>#JAtwg0j%c)PX?Y25td)KW zzYY~9d&qnKI2TMrUY}C@;8`MAAu$Ya{LttDI9FO+-~Xk+`VYy}+BA|!9ykzC6Fd+Q z(LdkuzsSS?zGAkzwLA6_>edRDW(1EU4luA{C@2E~^{Nys6p5S=GWRb*Mnbv$VOj0r zd4gEx97SY63LOL0Fi|2$LtXp@PCeX>U_ zzUOV`lbptD&Ue0tnQtE_yk4;nU(60ijX`%2AN9d!5gvm6uzUz(BopH>BJ==EVOV1( z4+XDRis9mB5oW^uFi7n|6WBtB0tZutxW4DYo52o@px6Y9l641^k-^ zcI(wbwLQ5BHdHkevM5So+jdLLXUe8<*=ee`A1S2{nzo0U#-uw=**|i4*~b_^9|C-X zvYMnO8*1+JlIcAGtCTLLI`p~I-eS{i3*x+m3JhHj60=V$nNEHMjFUHQbH6R`9W0dy z@jx5l$aC>Z0uoPx)%-W9-0T!iEznG7Dc=CLky~sN^&hytI%&aXh3i`MUW$>5 z?zk$mZ(BHx-Au-u#|oBClMheW#!8_cV&hJ;H#C^)$r)8jz|e1b2MmF>D3N5WD>U_K z)6qJuV&*g|-VK%Y-HiSX4CauJ(Gu2edB7)zbHqS0uA1m)y6j$?^Z)9rv0IOSEcm-*u` zr$40xLUe()+rmC1pY)Wi>8sBVxW6$qgj*XKQt-PXhcd+al zoGRoZaZ0Xw_@H!(3Vu;aJzxTE(+c%R7InDuiN;*Bba(s8%}6>1aVBddk2(8I(Ui^| z)Vc}geY@2ZRQlvA0XC_`{pCs^OP3Op9iRT#aNwnC>xa^sw9YK1vhi=3bJVGGM+ZEo zT7hl1vp5KA?1pO)ZiBC|=;PxK>qI#9xxgss8VX7Af-84VV)h$BD%~L8iEqFbgXe92 zw^2TzzWThlFOhF;>4Pbpe%Z-83VVp~9z)kcuDHCfQ`NvtA6lm7wj^@16L<^aJO{_M z_2^?jtQLPpc!Om2PYAd2~E1u zNEfwF5P6f{FlbF_xIG!L8N(Tmrh}#R8PKHTy91_; zZ67WS%wJeU>y#GxSGWsl8uQ<%&R?M&6B@ikeCCd(OALOf{L012@p-D4>JU3UgdKkx zPa*TU9a_wa0A^?_u(u0ZA=O$!+%S3-3#)Wp)~I~KF4k>?!>&>Bkj}usNmg+p9%w7E zS#Q^+sy!^HGzZ|}!{5uAYNq*_nIL7f0fin+7yztKHC#k9b=|MFUhK4fb17`w>|2IN)B)p zh#drL$J7qoAS;mPvV+mG#MMH*zBsRJ9^{hCgW5dg%Pq+g0EaX0A0zrG`myxijF7Tz zN=J%HvU_-bsvK2rdwNEehMoR~2mr)_6V3fuQlI|K zZ&-ix8`(eSHw8l%3qfaRQ(GgO|JJf3tG()D|5f+WNoZ+S+0qzu8%UVSAQJ*tYz^DW z?x#?<`~*#+9+DG6YeKsSOK$kxkgx95E_+SQJKyo_o46dmIV*qf{ax0wv!*MZ7e@9f zcs=Ev!G528J(=11{WWv(4dMX*9ks_3C7e_PgUa}z8sCjJv|{=thO!g2ug&O#y@PZ@FMhj4(M?XS zuXgK;%CF#H)6rCePA)srJk=awok9bqwcG&fT~Njyvq!<<>gi|WAe%%C_bcXL^9ib@|p&M>4uw-y!t zfO#?9Vl&0H0c^r0mc5rsyAdAz9^Y(ubXu!pcJBDiI9$e7Zn-d5f9SWee`V3<@O)pN zVPkqh>2KDwG9@&$Eb8uZL)!)48Sb!W<6DagMmyEyQby2G!aW>CCn9#wauJE^RzApJ z<6k_EuLCx(+E``4_IiBT6~v)!qF22-MBULS7>@HcFHGCn#N*?!;DFO*Ad>+$;E4M0 zDiu?&S1;?%ow)JN{y1R0QKMqM(d-BYZOl_Ya=y6Sp$PvH&XEH{w#>#R%^qD&yHbY} zdmMLEUl<2RDm@)I`JVL%F-kiyMKfiN0I7MZ(-8Sc)LkokU|OOp7$r86P8j?76|`od z;xF-YJP4*ZqRVcSjM`TpE_xISuFyke0K!pmh#HIRMi_48rU_8WP)QhRsdw0w!ukQ} z&cXwct=@q-{-rUH4q=sS%5->2XxS+`Fz+prB5J=j&_cZL7A;+E zc40NUR->lf^76IOy+DQX7+Gyh)~1dDxkR<54XTGlZ1AZvDy@I##vRW6HUm)cw}jht zWn&kBW8E~q3z{B|R5xVC9?NWr@VD^L{mBrayPRY4WfWMP%#bZI@m-Qgv>2#95m_ygxopjp3*5&P73areJ4D z*cN1-iDd^PJn@xn7<l^2kyTN?Ut>B;a(pA0509 z_tH!#f;gna3BUVwB7SdokF zpp+0xvIjg8l!7ySvOt)|bwW50io)u;B9K|LvMfdRgYcjFu31QLaFE@%2U6~A7wh3X zI8fv>?h?xOfDd_WZ12Fqwy$rpvT(w4J3=9xTvmCRMeKQhb9DVAD%i$)*# zqa(5<+lA+-nydKq;LO{A4&Y8gE7&zCu0gIhXYxn=-Fw?Y=#Ay#5!1^zg;T@S_0+rO z$^`d}WH}JF%KPMgGEW6pUrY`6_xSg}^@1i|lRF82wNAW$<(~}yoGX<*on1_A6`bt< z&eFJ8nmS7u+Wj5G`5y(Swz9S=$`_j?2O-XQ7mI{;QFE}i61@az6BP;xDhMfLvEDgh z7Fj63d4Eh2*#7ZNOCQJlJjfn?nqzt~m0>i>_gTDrM~>)ldEBJoY2Dh_`>MI8c}4E) z?LC$sxHHV&_jITzkP`o#;cUQFhC1!Gk2u=?HP1jzj!5Zu{;n;UGxV{qMSl^xA}5_h zJ;jhLz6y<`M-|e9M=27uM)H2j=3xoCHQ(m&Z>3K1q4%F}g2aDzg~2icT?1NHwQM}S zzNYu*`-z4gWDe1yoio>cX_MwBMJSF}0qKx1EV7zr!*ep`qSB_W0dG(PELqkdHNCwN zzr4rS^O`-U86GX%taV#cn;()+;ki~c+eqY*S?0zJ$<`^9b>t4^GgJ3Yhq9$0L~1lo5R{$##!O*>g(&vlS!(LuV#6d5(VfmAxq$FrZ$jM#=}6bIhypZF(* zN5JG2kvg{R!@uAdi4Cpfmd&Eh%$#QUSD1sIqwyRZi3ixv{nA%WcY;#6BT{EsMYZko-u!GS_ zYq6d9vn6+KWasUA$3%%oi6fEcVL0u~2M6ZJX2a&;J< zUhFt5FE3CR75@MkHFuvGHTNJOg8rsBuvWz<+8z{Kep?BIHaS&8fVMc=)()kXdgFub zLctT*Def6T!n;HCxE_Xqwz*>geuL08)Qj< z2;AC4!AMhXeYxc_)935)@+B!jB_TEg>8*@tIw| z!D~+mepBD=Q}JI)v_K5M1Y_VZ`WX~;Xu5+kC3ahd7pt!ZSlQEp-o-rtc=-Lo!k-_6 zNvvW|Vxi(r7P~905U!iFs6FpvcwU8}3M=q)A`j&)$2eLT1+1Yh@xD#`nA74%>0OhI#!EnsaA451$Wp&ChsyInmXgzc?|x zV9{Ss#pA*u969;l7y*tf^TY`FqTENC9**!+hxN&@Z)5uSninVLVNIZ%F^bsaw1z($ zBal{`_y`ISFdrel3}3-&ch#egC3TFN_*{OX9?}^;AiAfo3%ek%iF?_0Mx@>_Q7$C$BgA0PB}NiRpRY>;EgCH?$qHjPh5{ z>F_tkiTrbX`yW2<-viy>kmunk=;~r&YUg5UZ1_KOfW^wT_CEy?{m>{vN9Oj+4XTsZ zm%UVuBJAf3u%M#Dp%`T=WM%km4S$%|xSQC=-_<@u+Oph2yc9+?3M3N%BVP|pU-H~f z@^JrtJ~&yq1{(LUB8m=#f&u$sO}WWd9}|tjrmV622Nzkp&6)^)q_-u&8+Hs`Z?{|* z(5B2|BC*(CVxUps9pJIR%ZYYJ%l|}av@!txjL`n@;D;hp)`HGhh5tZID)(f%ilpR8 zM0DMxe^t=}nY6M?l?1zdL4eqBd{6On&{Gq^bi!70`ypf_u~S&Zo&FNcZ!|OW9!3o# zMO)}fxm}MU(>M(s92Q%y{wu)$(2gL9o44F(m+yx}H?y*IdjSM8_m&~Wq@)Q0LVt}- zn0gML{4ofXB_$e@MiG{?2_sEQ@_7>1+vqhBAskPgLZGz!4Mf|nmC1;}TE8&QJT4x# zM}OVB|A6V}ry3`4)pQn+u$#bx=d4qxFUI^^4q)pIR$I=OMpTmCLN&ra6eCAg@rfN>#OwiwP@S%F1SgRmgIzhd5IJ{uQZRN-_GVhwy7#F3Y%2Zhe?jN#EfN_P8|M$*AhD zL_ek%)-GzCmmC}0(r}^q5CBC?yRO|g1#IgY=MSSkwtiTwIEb3oCWp4R_;2#eVo!z+ zwf`8&1p@-Y|7Wk0u{SsW?<5+M_-;S1fHKmPIzm;;IP4&khMWy8st&McBml)r zk-b+ZW!6Yr=A%S^NBD zlIlPmuu(>)7(^qY8^$fPne0Fy7(Z(e7F!DLj=2gou+BWdw0Qzse=d2Irf2Kji`k$% zuwPp`z~{bQ?wcsb_PeDdW2C|ulA9x{y>vJ@f3X=?cMjLp!0D*4Fh*=sv7@ENxpWAT z)iyg{;VP#bUu8&zR`;wu#_)_bUIFWFOk}AGmKt6q_m}1mz_5|eyNauq56@A`uy%7u zBzjIOg&fH;nK8gf14o!SY3(dn=>S>Xb?|{JsgK&WbRGWD42#zmU^0`#g~iV_xT^GJ z11}0J|({HpV3s>?fe~1x1$XM{t*11<58+Lo9X2WVA{qg~T~? z*BPDy!6U0+nY}7RXwVItruoWElpFs6uKQM^r*(;}|5DD6SpnK}?~_&XZSyEnye=6fNYBMZTnX%+hI#6 zK&W5?6w;&+NP)Hxn6heQp_K<$42Ie|#-`h`6`d(7?MuLS{_c8nnqA05OsfGgj43WY z62x^7aN9qGNuPs3k2ko}nwip#3;&AueRBQ%#>W5kz61w^aF9sQlHUk|R5Un$B<_x= zhx2?Og2?9*F(K)A4wa{VX(`A_n$k@mSGW@!05lD%NF2t>fj!9V!6u7o!;cZNDy0@n?=`l!emh>Uz#x~%L=JX-VyTWdVw8I1}3r-?VoB3 zYcCpOtn>oYasaM0MF$;E3(I8W+sm#etMNiwE}^zt<`hOOQlg!i7|CLL0yvyG0Chm& zG4LS3Fe*%@{CUD4D?LH5*4kXLh2MLU<^#d{eCpe@~!8OtFPU^Ku3=`3McO~g!1Z@9%IhM`fb z*~C=Lv|PLC>K)P$MRCW3*Dr_pyG}WfhhGYlzBi%H@zw;F(#lJ%*#yO4g2E{3s$P92 zY7_%Kn1XIJtvAxIljOj(lgMyEN;Gx1e%MrJu+mQ_;TB6THNIEbuaodVR;EG!&+8vb zEF+(j2Eu%%s~G1&oCru9d&dW&lnN8Tl8uSZoQ+X5Y`U6BO*`?QEKwU~CXr{yimodm zzmZsX)6UT6&liE=(9!qX_^8-;^PE)5kl>Q{3`h8Z{hR~^G*s5w@$x4q(Fb`;s{**l z&>8u7JJY1e$2q_e9#`&-y-7G#M^cwcp^5-wb<0@-$)!fi5IV+BTrRGJrK-RmUz34l zUgXd%T;l+G*&qtJ)RKz~>d8`yQHGz5#ND=R390~}iwcm*NL$&Wtdpw%)aqosHcx?t zflH+0iq5=Bs*8(gJmmN#(ZovA3c&6LI82FN(-Lw7?yWawyhQ0HafO# z+jhsc&5n(ZadN-+v(LVtcR%}#_k5UR)rT52MvXOR)%CyT_5T$cJv?9ZPfDc#KdmA; z4*1sg&~-|s%%oi)CD0pwVS<+u4;rdV6pF5~YmjayRf+cvZi1-^Y$e5luitNHu+XA;Tz3{zzaSz{{ z>ytTBe}%YLwWmB#I4pNdu_@HSK&4#!a~D3cRpP%q*_X`1JyETuz_;>J)yo7gf{eBb7DzQfZZSeHZy07SuZ&>)MHq!X*n#j1d5$n)k3R7?#}hfl2MHxm z;UrY$ZUd=_<`;A!I?vd~Uc-?~iFwCo3yM|{n^T}69YlG_L6bR6BpA8)IcaBf2J>3u z7ED1BwCQkNx zj%GI2|7v%Nw~|HSM;!WCJ6!bV_4CckBRVI{$k}FsK~QE;hC>tc6s)pMBHoW$*Jim^ zG$UfT4#e+9Fv{rE%&A!GXJe{oy3Kw&*Lr!Gn$iI|xS|M_Oc~K-4abRqyRhRMn&4)8 zXm#tJ5WosI<2IbWx8l1xQ(>#o4%Vx&GxcVKAA*gjocL1=7r6NxcB~0kSW;}jX_YU| zQcUyE$>a|fl)I^P+nu4KX6YP_IjeKFXNnx1w>i(WR_jk1uOf(R&B&omYb)g+n~&ERqez4jSF`& z6T)@RHP}Vkjt;rX5u2$Od&OG!+Ti-6Liw3Fs^UG`s?xLawBWLj`!W&H_Ba!v6{ON; zlO1C3Erlp0d>KB`$x9xgMAMd%5W%@~caO!d>6hM0joa$)Snz0YGe8|{+G>`QcFl?B zgNyP=HYa%hVJ5f?Z^HNQ*DRsM;DtBY|77bVR&na=ikwcbex?8N9t@%o zavh2~@OYj}2d#tJMdHbE2d0~meMX<@OffYaeahE;r*H;t*O|c1&lsVom+_81N*9kS z5@!hk6K0=4shg0;5Fw`$4k>RO!QF?Zg_ex@+l{8wNA%!yr6vI}BRt+@*G>y%m-Hm+ z+S=_s4XrVDuErqPyvRKe-G{Rs6Yg(69M{z{&iPV zBBlV2F(16O5>kAWz@Z@Z5=;S+AYLM-0P(#l7^|!D8KDQ6g)Evq_mCoAd z_`k&*{#Why_kICle@E^e_T-MwvBD6awmr3Dq|A zOb~d+JiHicOCAFF8(!5XP6d0_(h~}U$Q5cvuksLsSg?Ap4Cq=VR_+tRTSy|_ z^A8k*f7r@FwFaPTjaf-LoiE>6UUvWY&`O+v1UBFkXd{UL0z&wowcWqpXrOAS52^{k z+qk}+i3^Sc9a)VcrU>5hf(SOQVV)$$d5yV;8b({ru8wPkdSl$u#27}&sfvQ`&m;TL<8Um|C))?2+xb<| z$RMx=jALe<4olN$kHS{@x1H2r9xTL=az-~86>k7S_yMv0ls^?-uc(oy)ZmaZ^BxLS z+LbsO!k(i3(;O_G{58N+g*jXC+Rc-H0OOi;h8rvIdro>L#akls*5Hf1369+HcROW7h+5KiU(p#^$H9^cH}WPZ0<2OqS%O= zcy9=qfImF7NH5yhX@~&_2KO2z%7Xf=xiUkq9niOP^ccF+!DW+TO;*~TM3ZaxG5;G^ z@n!d#INwD^cAbVlOk9n}JT~tb@fHt~2%+k|Jq?E?i8f=BN6|{u4Yo)VDFdQk^cKrdcPkYbF^Ti_ zOl_G+w{g>#lO`!c(tmI+8__2!3X3rx;Kj!>#tLqmY8iVrVX*Rg>*J^>#g>t2AWg6? zy6==KP|XLiFvkqx>7>+U3oZL{F9%mEg$Rwdu>KnR5XOx%=ZK3 z6%J@M8gHrsn(Vpb-Pn>{Qr!i0a-8@a`dycnTuH~Ih$;Q{zv%>>RvNvfs8`~Vv5$DF zxI`ZBQ;=3fIxpnMO8Ac6$qxhp`lMj0s;OioBxw>0Vb`dO#inESmA(d_|K^SysO-pO z7AkWSO#)2QotZ9&+rc;Q@n8ip`N+r#g4694V|fReke;=EtF(Uq za8YgXlo z44q=V6Cz*qtou}ex2dEhc3V=(U{E2i*gs1nI40&})n4>;)y@Hq99Q!*Cu1Jglv#em zm&@7O-%}h@@6o{9>`|drgVF1i)?<}Z+qB_vHPIDM-fAYcm6?XY>2uBoY3|Ozz-Z<> zQ5kev%*!`#st{elDr-J*RzFshMD1L#9Nc*(VK^Bm_mr<3u1uFb4SA6EE?8YqPNpZ@ zxD^gVU|dKVSq^=#3{HYikSGZ7wWw`UqEs>0lDu2hzz$ukR!^Lkca4_Vn@QA?bz8@ZD!67Qf z)7q4(;AJkIw}@4wGMkI>oSmcNJBidc;jP1&%uKfYP9JaR*C+CY?6RhiHKrLV=~M^C zMCeb}VL0OHxnvUK;OVGio@_9!4?pZVcWrEjg0SkcL2}^jwoeCZa{Q_^?}F01_S#{m z=4Al#o&5Q7u@dfOImGVJx-{kO^?+P(kyU@?^pGVi=(45$NC}&9U9r*Q4CZIaX7^wm zPy-QZI)o+C@C~;m{`-b7SI7?SWo6suEu9-N($6XEP)scLgP|(a9?V1irB&N2oZn-n z9U!EHG=z@d%m+M(UA|;`+S?Q^l^n`{KS|`4Xn{gmr?HfAXkOl$*X%9>ZSddV>6BFK zoYnedN7%0)rdrUIB_K=(h@U-ZG4FJy@*ft&h4E~*1bwIdJPS73mhpj1;MHdV8aW-F>_GbU*pY5y+X7> z(!_NhMCK+C0K4BuikFee*F_J_S)bTB7UhymqP>-Icm}c67CUyUFJLnY z+$}dAN1CQV;Z?Gn}%@CWx4? zGGd$J1GIrfs6-@XlNUX!A{mH}-oY*w_}zyd>nOs>khT!(NMf}*o7y1lGUpiAkhsAPb1ap9;dt3O+xhVLtG%9DGWfch zZ~J)jq3g}-@}+Ai{=@B&0VKU29gWMjIc&l;R0~LzoJ`4{pC{%Qeq|3)eYKb9-63%S ztlVX38H)CJbFK7SDlLgoLv!uq?vNe8z7m13QIJ3P-Kx+h-z0MDQl_&f07`0<78lj|Q07}`Ap70mh{taO( z_t{Vaessyzo_wp1R1XJQ^j;qouU~Z8BPMUvE{dx_4>_8r!~iRI>F(TP1b%iRbN21t zEo>J7-!BS;?6_c^xQMKqy|K6%)z`-|ziM&Ki=?row-&*7@4jf8vPbl;%55%hPgP_e z*@1^^u+~?(f#@p(G`=2Lv{$JC5N}$!`ts_=&hou0T_4e{osG^7++dFfc*pyDXz;eE zWAZy&DcOjimr6rshE*cDMPo9Bq^z2tJGE6rXtbDS7q|vzXQu{}C#6=37bq?+$hztM z`Wy(Mfwlb5l14#YJ?*y`-|7jR+=j3!)7~TTp=ivJFK=SwF%oG=Jgf!e`SzEfUW;nX zq%~b*Qc*#o@y}x8(AiQadwU{`9b=n#w$2;w%>4PDcJHv%rCds;bu`+tQE@tnL$$sq z_M7?S5Tm#eC4fypB!HU+Up9@qAx}g?06d|TWk|9G1zLV$ENXBs1F7QF&yH0+E?7Mky-%c?t_4VRf-v>#P+{*5&CRe^8bHijRvWh$Quq>T&G**0sV+RBW;`cUR9z@F=0XCEw>zLI>E z=&5FTh5SW2Y4RYon%Yq|@mh)_d45 zvq!G?T*5#umW)9r7PS`19zK@WuOH}Y2RmId>d6KSK=$Z9=UYP};}hbFFOwtU@KXcl zv*919=S^l|UTrLkX%YZCmHC4H7dnhKkVd>=)%v1BYHLH!XPOOG73f;u(xh}e=A~TA zG|KR}a{=K;W_gk@mR$P0UfBg97;6(iYtoKP6&l3^DN-3lq9fMWfTwu}(I~S#EfuPK zX{az!P6(B>QusiaT3#qtZGuetUf1jv)6;CSjM_{~EKhF>ONqH=I$F>(=#!qxHu z3;jy9;?O^ND%*@VwieP9dKl?)=IbFs(ISzta?Eyk@6urU@v_%goBs*zuXBl`F(r~1QA_JTV7wSCsrEHlw(`0V+ zE5B2;2M!J=sT#`S-Zl<8P!AKh#pY>^u=D3PUy4?n>~ho@>F}qDPFrdgU`9<@Q!NeYz=lnCH0?pv^~a zP+df?{m3J0vf!%etG}!=eJ2Qf+M(<{=E^bwdi5kZ)E=59UHU;HBt3aKvni624P{du zYzLZj_rldVsLI}Kaxl=1To~0eSKw)X3)Yy$j6Sxf5HE7Ask8>_-C?bnABsxcemBms zsyE0snpM?94#pjbKlRpk#*Y=u!D#vzj@NAA z4PH0k<^MuIjN_tdD10YCa^2tK+;a^SrT^wO?FwOE3svbAdrG6gCc8iP^*KZGr>)}CO?{q?eazuS7z}V zX8d_pnNEmH6Gl}wK3O)UL<|1pO8Gke_)4;!4~2=23@jl&y;e0j2^@zSm@qsFBEl*~a4Y&+cVf%W@&ke& zLrCvQ1b#^Rv1%E)EegGO+5LW1gW}X@2fL@K0y9_OVd-A{I*_A3bE=QzAA>OIJx{n1vr{Oc(I&x!T^AJB!_G` zzHs6_$7hek&%VB*)v%9*)?CHkybm=A0ZOu{#?L}`pF~BPfH`oXkN*RFQEGdavVyeO z2~d$;>wy5OwKOwvYGy{Wp&X6HtP6ovi-ui;ExjyTtnDv-H-U~BkkxC{0>5Spm3B>J zIRwxObuex8V!L<@b`?F}=IJ%r!+cZsgAsZn1-n7Sk1=|A2`jyAC{=2O7Z?8G?Gz*gl0zqJ__Tg@W}qgm%6*ZhiF5h=YPJ%jC-pD0(T zvv7RRni#8^%s8WJd;_g(Asx$G7$~nro74sSu=+%5^a8XPe@Z`{wK*&=7_M9=UevWm zSH-(umi8XNH5t~h_)9XiSYEzSoTl|Q?F_0^pH2ea+z*oxahBR_&bgr2GN02s&eD2|r7a zP1IlG^uFCFpPf823##>R5uRX~j^%jS^r2AH|2eizSRR?*SXdB~c4mX*WLlo<)Wb5U z?q8nn%dd(wN#BP*fqkAlZ=7;>o$#27eW7ZY?wK92x~H>6i~p6r<#DDnlr?cj{-`W4 zF{TD$o+4JB5@wM)8F^=w0_^gWx6xa@(CfW0s*}LRrpS#=+T>p|iZma%z4Gi;MS2D$ zt0gd|@{l_x`OfyjbQI5LV3S z%}fV-WTRJ`_Q8tw;f4|}#tc9zrAyBjZ9Tf9aAA+sVUBz{@bTZ;zRU?e-NY{-ASz%W zAngBH>Hb?}oJ7jTK+jUh>=TPE%8JJO-bxe0@$SS#)4zHU1QU#W`kRZ#p&v< zrX)WyIAKE|I=&}`R2-p9+X{TVbohrvkK9!*UpKxoH&f7T38Lq6s>2Jd!}8cK3?Gn= zfpU&<-AZT#S?RlRoGGu4J9)B4E0*DCrEL1Gqp)lu74?p4(u93kDQWZAg-{sfCzI#BkJ;W;~0ZIru?bpcXjcJ zJ{!~!7g`PFV{5NVM1fVVAH88_t+>tJx#m;+VajcbiY54?)R21hn+-C;Q^w&L7fy>O zov0?U+#R;eoqK@5oiX@B#OXJ5>fluQ@p+?p1ZqP5(CM}H6dCWt50>(pz1!}(Qev*2RSi{h1iA$Zh|qdHzC= z?*&fl;C;>joX_!B1K{7-aT=Guf#5X%1p>GI>>i-C{eJ_IpUodr64JDkP11A{QX}FE z(vu^DnXThLuKCF< z{q|`^|JdPC2XNB0x-+ucy0!PH?YctXB@Gd1L%GlU_B$8<%V0Ll$KtDR}+w5F=Gl}JlOy0xxS z9WZx;@U=cLOFpe$PkBK7?-DTMztP%1-R1wHwf~F6ZeVHlZ)o+uPG_`KZIbxhPXhl; z8i@R#rwiIxS$(EQrOd32=T6iA z0<%NX!N19=n5%QIBf0xVV5~^9rvL;8%2^WC#YUtq^#K&3QPoEs_Y$p z&BwaE$JuM;oJk|Ps{|(N_IRe9(C$B<2+uvSYOAkX$q83&r|t$8XQ8qY@J8`Hm;qdEgTPh~s>wj3uO^lgu@n1Zg@POx>N#4)xi9LV)KLhf^|@G) zm}-ZxFE8(rKrnZ?lYy2YTQ zpUu`^(^y6vwDHHB!BQmpVEH@(cPYM&x9>g-ssJhtm;(qqJrYOu=VcqsL!kdw<&>8x z&J|7#j~{xXdPVo!*3eA)gg|ze^XUM`7yfr{HjFqXX*4u0d3yB%9U|B2Q$+uy)XVjX zA7wQXcOZ!@aUT*Zx10SZQ4cxO}Fmu~(0O{K)&-iTdko$K=}SAvsKyJ!uqrOSJw8gC;s0v z_>+h0X!N(~(WyM9fbuy_c}7U|7=J(`UI&Iyj37eq1c_eMN?sT`+jT>F7-xYv$fT9@T9^b36R?D9Bf{!J!EuwT}=FbyKKG$`E94k z54Q))Nk7A1o5AJY;AGFC_H)0 zJylPCz%DXl=O(Z45H=}Q(^-Pl8beh|vwBzLWcwD5@B#~NB+=v*VoH6T)!rh@Vigj1 z;A^m2&`$n1uWW5pHbE@J9BZdAB#&1{EeF5HfXO1DcT6TW>=d1{JaM+@g7+Xji5B=9 zwWV=VTk*_aciF={P7F-;?6B!}*1KgtHR>%ngC@ZiIVEe$=xEam2(*us5O3yDV9Q*| zsWDLe^!iWMHsfME7&Fb9Wy1{cXE)7qtk)tKfP2LDqnmY(4Ek)C-B%}k%r$P#n>KX~ zJd{$v6iU4!fQZJIxv|BJFwN=9)V#M+0}hnkNGkUk^6`!^YZ9e9f>Htg||L!lwB0_BVw2L~6^j{VUaNamdOlCt`H8P+Hgp z1jpeitVy_hAZr{73sCrciW8;EXnl!44lzZ*G7ZFDIK!0(FCzjNvT4F)Zu(F|4baWi zUJWL9nU&o_xsmlPjqP|UUjLCO`|DZHhhh3L^tp0^KRJ{<|C7}t`TwzYHqVg8Wn|PKz;gK==|q-9VUE5cC6EDxzUk&qWH7jcF})1^jG4Li2nBIL)f! zQ4785{!;>3pkp3bllNyaPxj;9?U~!_c`Lh;$HdsVK7FGml<(y*{j(Quz02Wc@?zj4 z%je4vuFb?+2$!lHQo?)`17s-xp<0Ca&5aw-5{LuTDv}k)L@*HOMFK2sm%u@|Fx&;w zcd7lP+ZmH`Y?j*n)w||F;;B67?#UD3VPx6o#n|=J=4PbX@UzWMulF+OnrCob=jY`f zRL84aZ}j6MJ3b%59wuJGl>xx1ruPxXhuqDLA|m_dK@SkUZ6ft@3;PQCAo)mI?xxgR z{VAa2Dn2^`4ty=J4?+Us16Gm)bFHe)PMl>09B?*}Owa4jVi%D#!^!U!)p||OYX=9J znKn&Qp1miW9oxy$eS&|qR`YM4jMY4I3uz;zi=yjZEA`i7h8B+7v8X27hP8O1Kw=V3 zE37Y4yR$lJNftaNC3*!n+EudbV}&mA_Sqd<4)`+Yn_G{P^{{-6a(R9UFpg?~e7MlJ zyC~V2Z_s`}=z@^yoTG-L3thY?|-zy8?X`an2V&uH6_+=(N$ z>S(x~?HNM{a=NXIdg;RGQ|N-Jj7A-^VP+hh-k55zo5J4bsP@3O#O9KNFOCP-tu-~~ zubVs`Ex|hHiuR~js@mc@Q!Ix@QyvL4`|-oUFwhI3haU4kHiHjMz;LR*sZ=g5 za5|Z<&*CmLXe}&U%qveQ%>lLq-t2hy9&kA;hKB35w#8X5DIsL{06#M~ipru%H}V0j zP67_|1y+&Rc05&^dMoCZNDDN`GT+(#x2Rem*uJ}>3&Kw7i}faSH|5c8S+IXIO3dbP z+|Th*LhG-em?LT2dL{^>O6lQ%sfV33km+%773sZ&1$KwSdJpv+t)@$~KrWbzwB^ej zHFX(2(d|4Szvpa6pZW9c2t-}J-N;`tw!cle3ik+iP}O)74pb$?hMcngSq95*E8Avy z4NP8wU@cexL}4w(H{lUy_b(U|xr0@UxhnQ(QomZgD)(-I&+8|aZ6j|YwcLZIkE+zs%U|sEZ3U7e8SYJoXj*IV_K%?m18}=h1 z!g;!tx}xkbj@LVwAsM6eSurCTVxNOvbDa&yN{)FG(bj8_A`tIP_vuX{(PKTSjRr~% zuGR%1(>G15yac@!?1p!*pLUAEEL(X^`3>Jj#-?t2;A9XG__j6TRb{XJt3-gjO+MIG;$)@q{S#e2Yx?P$ly8Aq!_X0)kBBSOafN=THkw zZ&7<^PNq>+gvw#QXJ3q%+$O*jE5-8k1QMir?^%qw1#owvx zejNV!+6*qn3KYuLL3a;PU3QPvTwKx>OF6#8*sETRoIW(sL5**Ga>JHW^$%57mu}gu z+<-$C=Kuz32?{qUqy#5-VUFpeMArB)rAdb?WjJ5P&M#7{0#{!gaT#=@vtjXhBlRPX z%C?*(m8*9g)@Kd2KTnqUc{5H}`1ih3fieX)-bS3Ds%TN&)Eg)s>BiScDw@LG3x3Ac z7SGt}#qtg(Z$3}@=5#s!IdKll+`*g@L(gJ;%z4ySSdp=0_{IW$Mf@E!?_EylR_d^< z1!xTqx6&b1d-E?JByw2@y-ui*Az#&qfkhRaVzoYK&EZS*guo%YYe=$zbXT>G!1!G& zE#%~@ELW%xq)P?r_Y01W@O7UId*_t8Jk$qW6zc*0xHmkA9VTGVhddWg9eUmqQgx1|9n7l0i0HWo6vB7xGfX2d80%s>sy;Q@t?Hj2 zW?Vr-0yqC`F8oU&G_%xquU^KTMTr5V5Uc0ig<}cT9nz&Q<|UI17$cIkW+<4!tJjQkjWq*@v|Nq7ZV`+V2@m+1JI=K59Wko3 zDM;KW?5RI!`}#_4U0Xb>dUGt>oXlBC!MG}o7f@wg7m9}EMk?*vdjm<|2ug;X0gTbW z=cH51jsqgO?=S1c9bM<_MavDXhp7RYpLpZ7?9g|MwvjCr>6-wb5IxLPs`G2r4qb<0 zd5V`_d#v)-VmNtPth${nlE{81XRX5!0=J5lm$#!{F5b-|6=%&nV{7#rS4N&A_0k;T zz=o)LwP}VBCQfSurGdOCQtEQGnc6Uxpg_6(NRuwgChDE1eH9ilCb);Ml8sz&Oyqmq z-1t~2o97bWRvQNtCu0ad=uAp?@xU<2`c%2NwtyQg`?&DV)Rs#CNVm@S?MOP;z%`m% zx-q5W!V~%8)LV^-yNW$rqMyBb%(>>k%%N%t)wpf0cYQdM7;55Wcg`Il5VIGA}0t2j2Q%K;r2^{ z&V+N`NLYya-C**++`>K~qcIiz`tdZq6V6|yTpLclxD&d=SS6RWLp2DR#RJzMFTBT; zC&cn2$xHAImk~HuDTH0~WR}L+G5n`B_|h87UdxX~ zf_)M%oyqS=oMSjulsDhw1+x%g>L7MIh0%{6ZT`Ka6lsEf;Q2HWM?d>%x&9}jnBVyG zYbqjU_70AUPWu1yNB%A+2UJcK=Hw9HWi+7+zDPjybLku9&@@0G)wohB(<)G?3D4-{ z_G3}N#6w=Ad4g<#nK;U&+Pxk|MDGf+Ux>q^N~ZVH?`79}I6rurxYW0E{Cf9%`9=^V zj5HC#Jd2Dr;};@U$4?hzE7@J+yQalNCD5JX%LO_p>}n?)5b^?w2Q5ZFpKdcRKrlL; zIUA`(VlrIrMjAI!9$U~=OK4F}sjP@@n4g1bSBIy{vd}R^zi?{#UL8mZq^S*mpkZ*0 zdoJsp?uEG3#Qq+3tpps2@6VpCxAzujRqCRH}2+Zs}JnBW9&;t(D}pt)R<%l0eFoKQ^iwNr$jNPf62mw@y>|f7A*G_MJ8h))z#>Q z!((#fF!I+MpNTbDZ7Z^|7)tnILYf#L*)oosp1i5-4MYyaJG~h%B`=58!lU{OuVT`wZ%_E*}PhQ^X5Qn~jL+puD)>Y}bO=%_AAl^PS zxImYkcQFr)RWqvt&ZZE~dvIV9Khy}PnCLxwbhz)pj3E)A53OxNu<>FiTdJ(*I4!$}zGoa7D_2b*0BwP3j?TYB*OpQ=eJ#)(82@Habr`%N0MlX% zLYou4oiYGXS;B!Kusmx1E0DqWs}MN_*u>rL3CW0Z-V!&Fdr0rn4|09XFFxi2h~%Bg zN7`4S8gM9h7{;MR+`Iqi0{t&rQpYH|=yNq5eUAV3#mB!~%>UDt{1@6SFoORtw43mU zX`{wh=l!-23&ANlRB+)Qlz!(D8Z%JwKen{ozQM?fT5Yzxw|w}p4(1Jn% zN0!!2+g6n+HN(J%3M51F9F5z*kfVg((Ug1t2alwa}_z4G?r^yk-7HEVgB&J99bL1=hADUFp ze>lv4-K(anbxOi#Sw4jHf6#CKy4U}5tqlKh$A3R;5>;1xuvO9CMb?IAg=&?DqBh8s z=h%~sID$x~T}#9F$fR-BDDA{;HDFOJh-70-5@v=&H6@F19exF&%DlJa8R)nzYV-c6bf(fj!c1%I2U1!7=?rj6nY_ zpJYe*v!uPIKcA>iCTC!yl&m&jOkAs{&h;~#s}9JP%s~?YUAxvo&K@!XOp#xMb|DUh zGj^qH*V2(rM|EGx8V}aBjS^D4lO_=(gD8m_M9Z8h#STsn+;scDq3XFLs)-90Pu# zN+T^GK2k??!$_PZ%%8SwH`AUvDguc^ zxv3${c=^RV5UCUhY1gSMxr}B$ekD2P1n6 zO26j$$KyLPs$9nysg^)rlsmVYOCFhX+7^^0!={NTCWT!DUfbR28@ z`QAiblBmIRKFn%yHXdGUaLvIOnJ`QzbY|HCJsyhX6lxVqSE5jb4P~Ta3D(Fj1bbdcYDLoLMECx|t=BCd`4fJOJl7PR$9@;s3hB^y_)^=c#I}gW+g5|hVUM}ODKtu zU*_plRuiP)#Z-h9JT|31!pu>%P+J6~hPD3e_0d=}b)|-C#c_J@qEKObw z-!szR^0u6m*9NPpEd0m?yq^Ht5+SVsu7l!e8N-o+Vq9r&dCyITddD@F9!Mo`D~XSA zrD{&@BP1Gwj*9@}uN8PjShaHW{!FkG%v$l+0U^l~Zv^3JpD(j7z2QYiTss7%3wCa+ zCiK>i0R060Gsxdg3^KKW`n!;EZx(VSQin@5(!I(V1u=0~s+QTUkt!z*@S>2`M4dq% zByghbd09Xau_*l)Tj~d1BJJ9$VJznWJLmn$Un&9f z5FyzWm|5?DL`a6qK-J%Z_~Bb+@ht*+mS=U3{UYt;VI4Zu?!Xx4V~6V#wX=3a*8CtN zI;iy?Ozg)Kydo%YlM;F14i)P8+?BrU#G1W%-qDulRDQKQn)&O{X7}BKcjF~<^hgkg zgXBm!2`$;A64!9LkHjm($vFd6#@-~2g^U{=+@uyNS~09{Rs@HUxK}ZIe6Bz(25>ks zCnXSaG;|Ga_H_w~x?%DWJiD-;{|w2~kBf-ks`Wj6I0NmB@fk`$?T?uhJ8W;JD9#Y) zI#?Hq)%XME4oSuf$g(T_8Ht|kCd9os_x}EgbQc7e7x4TA?g5~(5m8;pOR5tH}E{nN$Wj$*~ba~m30_PyB~ z94XKl_==Vn5Ex}D$l1;&bf{5$>(e*6c9bf48P2}2P6vCB?*0*{Q@z@NqW4p}NWSX_ zIR*|n6RmEhAT1wdQ-x_K;{I*QLkgB-3RqsAIBSzoX%%T6UJkbKPuWP34GO$JbtTY< zBC$q)w6RSHd4FOkUup?&W-}oS*;1SC-?qG1dfWg{)D^H*4Jm z_k+J3x*u6H@bnQrQE#|KHFw`;ywE*fobOV2EmIbOd;C(766_iijLIhxB!zOg_WAe9 zg2;VDBkprsH5L9pzSsZpM*q8t{#OuJMblwU1@S!rbF5Yv8Loc^kHFUeWgQ(fp+FA0 z`G=Tqfw*9faILbaQmmHx!R$-t+t-e}%Kbuw2F2cjamVL=L6c|lXTeo(Q5D0LI?1B= z(8;WYtPLBNUpzPXJ|EB6zRA~CI9&9k2Aol>Qck*Dn@8Pn#({T5uH&S(V7D;q{WRCE zl7nk7>p@fo{35(jlP9HqG0cRW#07b-)Pp%gwxSeUkv)iIq;`v_2$CY=gC59+<-d=T z)RU%JyhS_952&K7e-|;Es0j=u0GXAG^(~O^7k}ZFTEj0;9~9wevW54Ny9vmj>l=wP zWgbJ3;;kaqV6n{Gr;?>(IS~Ww80Du&-85Z##KeL&6xF9CnvzrlM=H$I=Osc;jl1f8 zWG;)bLARM|CeBt|P)o3{L$B6_x8_x%!_g$O-L022HWZ`<+HK5}Z46r@&me8gXT~Ed z9D_tKh}JJa2x8-<#3BM3Vbdr8vNB3Z=*%7(9Vp$E9p>XDO|0bOHF z0h=i$$NU@^NXJ>1%=-M}nrfC@Dda`7zM2b&+NNSCc0A2DRaIy)I)FwuNf)P{27j+H zTk};v<-AVG#IjsVpG8`oVcSn?KUvYakG^Ede#&zF!^YdT91q#ylbEZrZ34^L2K1dg z4WJ<^1lMQGPW`8UC+vNQsE8>Q?8RzL*PsIR>1T-=T4pB}pd=dUWT^%y_Kj1)0MAdMbKS0?v0j3dZvi;Gt$j(vB>X|Mx5As zxXsS6-3Qwot#XitrG0i*_Z}ogc8j?oH1`(eBGRHW$`Tmm7F=6bY~bT3xn)}9;cqC4 zt^Xb(-ePYHfs2f%j~&?3e;XP)P8R}K^~0?1EZ^t-IdpS1cFzGp-}9p21GQ~B9Pgkm zT`eedpSzGjqeCbhjwUE}`!e;7JM|!4)i&iZ1Xh$Dyk%AfhxEoE0j)}T!j7+6l`=CY z*<6L=B4C2^%XzIuj6*m@!pnM&GWlr-h%!0xd}mO6R=kB41T;rHTB7UOA)3nq7TT`@W9JVrYj5EWNqdm5_e(oTeW+g4|hObuX?!#o0IJN1lyk zzO_H5ad7y{PmN>)dsmqp>yEYeDu53T6za9K27S~^za0wd$Ce*)cULaDj4ab8$k@VN zdMS=!NARXt{~&foHg#5wFgDGDWD$_|s0=Rm(_Xl*1Zz`IdpqmL0Pys(s*w)V-?axh zw8-5KY-4{3&_Rq=yTJ_DhC3f>fo2Rr&^0Rd0G)%&n0kORO-F?(R6A!xPa00nz2BYL z93t#xx`J`s0>oaT;f2y+=iAq+9|}Kl;m!COkEvEChxWI><8pr=q^?h(kKkU#Nxn#7 z-TW9y9Y!e-n6#ct#p))0aUJx+gD9q3VfWIAKL)E}#?@}UuGzGABwzP1oOQ@^LlcJ~ z?`UbdF-O)nhFGImtSXVI>PDu+k6)+-RhD>KR@fZS#5TztdEWC(oMlmMd;bS1=C5i^ za0dP0^}(||IskAQbbWj^|r3FwW~K3gHLQ)7W4lBgyt{* zf%p|E207F!fpxiAT5WE@aB0P|%qN#G_oH{R!9S_=Uen~c@FSluZ_dHR0Gh0($F4}{ zVytzF=dg7u%j5q2&!r6=NP(Tsp!#@bo-t4a(*XwnKt?yB-qrw-Y?-_cO`&$EfL?|N zFsg7YLe@;$DGoyNCNhLbM`c7HHnHE}Gy|T`FaNkZ2I*0oXe25PD7chslxZy}s@JP1 zMs8$Sn>H(#SE&O8=st@eI)(~a_dXp__Ja*pEo1X^{?W8{tUiF=viZD3nv*Dv@(A*1 zs!?NGrAAu{TV!0$7$R`yB0Z_8NYkl(nu5iPkpep?SAC^SquK&I{gwo9ei9;sMS=gL zJ!H-eJ&1pfV!6!d5Pzl3%+_;~{&&=dT7zkPb>_%A*dHhsW#veTGF{C!`4LBrqK;CR zN3w6;3uVy9oUb#@Ot{>kzrU?Rq#YngD|SL!bKsfBQ*A}qX|cAZz={6+0*A0ALFHC2 z!*zV%!AP9eU}_A=>pU}v`D)xprI;3igLBa>^|ci(qc6lyuVc(En9z*~FmTWg+jI>v zlnfiTtQX=cUb2nXrJwQ;;ke{#ni3sJJK}m#LJEOFPp0NIhRDa+U8ig|&)f&}QdmsO zeixWCDo8~iq1qv`o)jh+ydbPsTGfO5fvxppqccPu(dyjAe~xpSFE~*hxO= zOJpmD>O{2vlt(Hw=k&Sx=*F_%J;y+|;?vsDJwHrV4=RC9AeA~za-%yBC!1jMsRF`- z+i2wHcje4alAgs-rvxFX4ACZxTb5Wkv2Z?#=FxqziAjo(~WzVE)>LK-daEp5LcJpSsJ&glP6be97= z#}xtLpVu)rjF4S&fA`=XZOmqNQ49{skfqk6~xg(#g zdYYGoJ{P1NG{Xu(0=l(D_vjxr+;L5!zK!%Ufj`jS+2iNtEyv=-0tvU=R`WdYGLDQx zPv9f!lp|_cNR~MnE&VscIfCDflGVbyf?%YOtr*hsViCTa5d?wbdL{f8*T+&oBvEg3 zYvHaGn|BbUan4csW!s0INzZ^W3){yadBSAkI5Hy{LAz&g=%HojsWlKhUmfFYa?IhG zMJ<0aM0%|S{E-11pgcjs@pYv&A;jzZ+9KRS|0i3V*Hik9E$;On5Qo2>AsrRcjDJBK z{6E#<|Br2!|D3hURGRvPI-tHAw;AbK<=*7$Hu`Dg${A7;h)IMLgu>J!p}>W17M&Nt zsXLEcBt2GlkEBC+T!V&^4c7Z4Tu(Sx3Rsx4J8q^wbh%xw*=*hoP0?+Epk9x6BObtG zn$Ohc7|i$vkv67qCMl;`$`6G_DF^+eIUd$cCrrxb)14l*-q6P^6&rG2;I7ix#FlTQ zWYn;;=;cMsQhK2%fe4;85xdL;pLMziljV%8^YA3%Mry$m)L*jue=7S5s4Ca3Yr4C; zJEXh2L8Mc;rtFVGy zeGm0s!@f0W{*&l({ZKN!Tqgl+L3wg3M!Kzg<4T+~oCp1q;$)-Owqz>IB!+yK6{*Z} z)U6KpX3%tK=qt?37+qrIFCw$BP<<;8E2Y|&wQ79T7D7cl**Is{gKD0lTyeyCvsg&+ zhxfNV(MQt9IyQtVIW$5BfJjW;iByX&;#Rg#E=gF!(g!wV`F2^%s(i%r%i-Hh?|KBJ@F( zfztFVNk(Ouf!g72K6e!REE+68EN-t?VCS271>AIntZC(WcDmdY&*#sSl}0+EHSf5c zPF!BsChR%*44DDRBXio^E90w3hXm75-@EP2#BJ36b1Af2G*x#Bvf3FN9OM@(Wck9; zJPjfQlny2RYtP09NrFAM%!}|z(j!9Jb7jM=9I`&)-;q2#W1Ks@C_jCh2!r2G{RK*BCUrG<;;#I^vs6p|)lsv(Ob`G@7+iD?NSh z67U#*$|>yf`Cw^&qm6!(DhbN<*hgsfIXsUsmDhGtX9X886xgw*W%_DuGQTZnXE9H` z%DE=}3A<4k#}Z<|OU?*N;G}%0mvcgk>lw4|VRzveG7kay!WEmAZv8+(vzQa^{271% zS4We6tSaUh*f5wMnBdMlbijnNnra0+_@;k_m~_$KrOt zirR^aq=PfiEi!W}HN>WcLtW0xI9-$%x#3RNQ|W`3WvUE~JWQPU6r1HheE`=B*e~*U zW)mPXIgRMzxQEUD}EOn-K(kK}M{WaM9*TReZ;?hC>v8J_n z#1H5(kL*DsR$e^otVEVo`aL42QxR^Aw`kev7-bF&<^)2g5N|U;D0%cmLWd>R;G-ov zvy!$O4Z9JV){^~F(nu*;#u)L31-rtNeJ^aXd>W!Ut(W%o#W$Lg%uN&JN+y_wXtcU> zo1(FnPx!ROa5&5Zqwao*Tk>r)M&7d z9ZSlrs2q5V><3;fSaM{^iSMkTO%Oes1-qJJwHz1e?*wi)!YKG}Aa5kFN3G|L{K3p8 z(h09qPrY_i7gxPse+97v>J!IVWM`fm$+pVDZQ*m^aVAG*IFUf#AzomV5#bPR*6Sc$XwDRQC{uH>gI+tY^$Ajuv_{>75!~#v!fG^ zFk8y~JY2P7WIoSJ7=pOX4y|!BGEj>jQRON3iOp%{^PQZm`b6ICo)D3=sK5RkP}VcNOcihC?}-&=W|z-kXN>HpvEEJ# z`k>Bu0p$kmI_IGNS^V9~1Mns;EriuqliHY%{Z;7-AnN5H6f8na5HwY?f(xm}W_u>o zm-sugT8t@7L*gjW9o-8!C%#yA_=pB;!BiqIBPC{!lBU@6Afc0N~4_Z|L;PIT9e`lM5i^^N%Q%amwGV@Ihh2XzN3)%T~t_w{x5p`ICet zp~lUGi;*X~0T(p0)_MtWB01ijtJw^!oN9?SIr}mJDO_J;Z<4c3lFp#x(0jCO^+A@o<}2*?#$1F! zl40a{6V>%0e&tdVmd~d7==vi7H`;4{`j)yKLmyTlnb2DIV&kw&sRIwWeCw&qjTm)b zRk^=xYU3EI>a#1^=T0XN_A5WqGiT5K28Wv%@-Q$aywS6#8B&kyCPm00BFcBM~WRs)Zuxm|Qe)a7c7IfaSDl+_z;O(gq^ax6?O6XD5d;e=J%5Nhao+U=rFrX85kESc7zWK=+S_~Q&MVH0N8Y_O?nwiYu_7y~x5#a&cEA*}j z*OUm*<=ayH#9ZvcIkw@YSv>z!k-WWEY}!FXD^;d*sslE7mXSO22>vhX>6WRJk`E68 z)Qjijb;@`avWiOBCZ9tuj+KfK?8NLeG_knH-Mqp?<(MM&omauEE+91(vsT@Kh^iU z1Z3<^!7%TseS*+JsmE45okJ0dUreFJwO;$8gt9_IwT|z+JuJ**)e}T#UangYPym4+ z2B zCHnmmxt>0j&0E`Kr+lAhLjFSLqbk`)VKE#x+K}#*2 zbEO-Edll>%Y?8f6vgTf{`Q$`6AlxFUFshwx8lTQWRS>0yhv-wLXD*Axb)F{`9RPQO z71(WA3!^`x?le*joGF$_)`>Td+JApzK6G=mw7U3r$x&sbPA#cj)H@U zdI>!5%0B7fO_$$>WS~1g{=qBCCek#}M zg-gEeDMJO&Rh5X$dBW?qxC!a)HM7G;+m;zL&&d-M2ZqI*euBX9S}XJv1e$YW7wAR$rXmUE^<4-s@RpvAxPr#SFE5)5b^2^_s7oVr-7t+0vs#xAudcH4 z2}iXRKRjp2oRNUZyWbMN^Mf}+gdL}Unk%Gc)heelqv?qN4)*g4{rr)XB0D>lc~__7L>1L?&PENrNP(iw(Znvj9m>5~GDK zymh54xo{qv^>-A}218w{D zR(0-1TPP$9isz7(y_!&d_25JDR<)Q~Z}$1SWJchMKwsiVv0*i3AgCHft)!rgiNVz+4P z4fle&4k%_V^T%E?_ibOQG>_}4>b$UK<zu*C{!b z53{91GkN-uHgep;ZcvFaR^V-PWK1diz*iXshv3F61B-E7@S@^n2a7L_@9?pjI}ZGe zCKvXYW*SDtLVQ@&-Z!SySTVfs zx^j!uxaWFgrt240q9L$w1B>Rx>6CIWA7|zpXLa{+g4XObY~o#(XNx@(?NXvPyL#+( z4>C6NOqeYUP8f*{Itig&0dLhRhzp{Xs9NE=BZ_m9FxS)Zhj|=mE9um+WNm#VJCQOl z=AZ35@NB}>M;;lDMBdYat1N-~g?OxsV7?nvo?CeT3~}~Lp%)RitB| z$m(ZNq@0s+(^n)+fQmnzurS?6O>kCn=oJ095q%j;w+>mr6TPYLF!m0whk^-R(@h-N zTrGy|Tw*U`&DoW9=N$zXY1?x`+|L8ctR`OAPNpKgFQ{m7FV>>{Kntr7jlLKr^xh&w z%-4V5B8+={vb%YS$Ul(-=mpdO6QzH(f&Y(z*}V zte+rkY*uy%lcE>YfGDcdn-(TlOZ@3_HdnDT(i)>_G0AI<58+(^q z4<>vKal`h4$+GhhrFN~ArSqz3AhF}%S?U^l*OpWDy=)wD>wy*+gJjp8ON~ibLCa3G zal^)Jqt2Op(HdPR6PDMMZPw zOdNL4cb?tS&$7eluUFrG9=%;@I(;ajxK@L3^_-5!V<3>~QGsAA#a~a18Q88wk4*!= zrD2i9GlurBU$+fwdgzEMV_Mi|VF(){4Q}z$8O$L}4kZuT7QJIRt=nxs_efot(^q2d zbw=m-tlldZ)M$7~)OBQEpP_$tvn<5H>8^&I-z0xgc}_!q%jC5;efXHAzfpEmu@8tT zf;&Wi_*I_BEv|X-ikQ%sM*5;>NVxv7@`yV6F0-GXbP&iKQA){dXx|Dk)W%E5B0(A4 z|AIg?RCQ*o?_ex35M*9_@q}sv3`rVgaF6Rv6jYOF&c%xtggm8iTjWcjOtAxVaxz_lNUs2fSW^|nl%pi9Fya+*)RKy5ATZD?@AsRmq&TXVW52Q^eg*Q5ZIH_X zixI|*Pjjz1ZPnk|y*|Y4;2Xhr!cRp>B~yYehtFDtEwK9VC6;=<9jknvb6W`76JNpI z0Hgv2nfvWvfHzK3QKLVLAFfnZ;s6`v(lj)pw7OBPG6@!hJ)YYYO1h_hejo#DO74=v zi~V*czTFr__7o;I=)8r-nnV0t_O|l0-e5GbMKyGTH!sZgIy@;aAne;McuUh>#BOu1 zD=^pHVUjnFdq8Wima7L09*?9{1e|!ob`HC2_+#9L)7H!@rwE$qIVaN#qtd4w&zBmX zvE$*gRlZbSN|x?ZMX*!vw%mg1>+$^j#7R8{h$rdWYUvB0e7Sxv->14B|GT`?q6ekB z1kr*<=Sr;W>Srvb#g^EFQAN80G2dZezF>I1|2mmqGKvh9Vp4cTTFnpkTPRpcbEG1n zZjDry^U=rQ)W#O?mNwuHV5JyE(qAp1gV?*ei zG)>`3gt;_wD3OGsg0SHl8^GAU8fcKn9G|&=>m)80>`~A4&d* z@v}-W8C5#?8)bF3f$Y%B_E-~9aOF%OF_2t|OuX*reQIdFo50f6!!`_~i#Nb2^K1`R*Q)CAC zewM8XQ_OF9r1=Q;_r+3RUM;363J_2)AnqK+?^XIs6@K3${$=f=0qdqbU8hTX&KVN} zN)`p=&r5FS~?DL6PR5=!m5h^IyO;SGEcR)5V2}rRpdy3{#u_RmXm0D4ms+d%W zVrki_fpy8NWqzyEEqtrSMCUszvUI)jN(Q=E94#7_2YfE#j?esxk-|S@Cl3X?X)@X+sV0#Niu@d zO_uFzuv=NQP$_Pmu9#Qabfl&OrlH}omuIYI1Cy`3>U&w7U>L);P$FcUiw&;qlWMm= zL-&Wm+$zKP)2#C-@z;nd)Y-TYWOO-hDP^8%r(rnO^`1LvemrN>&9sk@A9Tx-uWOME z!LV!*EjaIik#TN%Zh~>%h%>*%FTsmi5b5`S-2Aj$QTA9K%(xZ>T47|zR7 zi9@%%w})&t?@nlt8yEU4MNehCmZ;iiExd~4)u1+=wTW6UH$r_bKyY5}Y*Nh6LZoTB z=UlMgy49%fI;6e-gwCDPGTk?v4gF<2yQZg%-zH|jCBk2}+o%*OxXetaKfI?DYcf$K z?M#ns3F|FHm$h?Lq&$m7FR5b0dnUPZ7Q>lPyCIl9zq2)Y+LRFLRo%2U8=lDqnXbjI zC>?!IhdS&Cjq%jC~e#Or}1*3*+q{#Oin*;bh5_R%LOy zfL?-A&pI{3uv*>Za=fsez{+^5X2ar|tfF(KS4d+~r_o%ZnqqH;uG`N_V?f(n$}?c7 zjN{xo51qW!tEV*=?kZN*wVhNGc9cQPvZQLvqTaxaVqIbpmP}7vQp|-`Dt%;a%xE?L z@tHPAI=BfH+Pn(kl64)~P)|2*B`90^I>QiJy?UBr$-Hu#4;mFB?n1SH6b)M~KTbEJ zdIpo6Fm-W!I%as1mIwyji4GbZz29C1e z8aDjL)9y^36@ma9C4PO|dQ(v^Dd~y;X%Yqj&vyfp)G~=XaaMR%Jrh=nq24mdf_D9u z1UJwh5UhaG23Z#CAe^GYw-;;7mdoBR05|*}*bHoap+F zg_3Qm(Vb?t`2?7t4tFj{3fB*!A#i;#@2{*4QN9#8X;@J3OOFD* zkvygvc=?M0&;0rtRAd`dbd=Q)Bch8v$=aS!ikN-^^g1BhvXAHkQD0c=R3Qwri!7U` z;pgVegPMq@3|Z-zv68aH205f7 zmHN(1>z|k;CtC~keM*b3%bI*g@3as!$+X$vaWd8yv&ns3FNuU)Ur1*CN{0-sIytT{ zO?Uygl$VpDdthI(-r|s*5qU`|rYe@U+PDLqiB_fG?Y&D~eW*e&JW8$Xa7cqHH6iop zHE>YaMv_;-K78bS*cM?lc?1GMbi`||50t?FtbJ==KsA)2j zVayt0S;&<+LgBOSH2UdLXjgvxynfgir_C5*!(e7t3o4<#40SDbGzLb-iBE1 zgoJC0YuEc^{YA{*c9#dpj9kVr4?46b&KP2K05Wjzng8jI2#_@!!z8mzaJRYpo2t;^ zIAh5{R!%w9l);>F$*Hxj?2EY%8n|$ElEN%k&WPRvYo53K{->BCydADsUx);ZK6QEZ zuQ5#WK?rodwead*tAKiM1&Q#H-Jfu?f&!ETwVnKRTiW_so#LRMf07A&jSp^v{2S+X zii|$^*M9BkOQs-GU5jP$%622T6z4}WsUH+RD`9IDMTigEa51yK#_>g_V&6-+p!I(n z3Y@WS3eVrGV7_EHrqDRMwy80wR1osv6^i}Ie%1bZct;J>!rJZ)g#Z1mJ5*A6%LNDs zwW>B9EF3?vIcG0+1lw!Hy>?#`@-lJIh5967v}PTA&Qhg7P74Ol!ycCWJ`*%Z!yXeT zsIdv1B|j1(t2*US42CyJ?dG4}li^w%4B2s_FtBTBhLW0F%9X-XN+eq+t1pH3hPi@o z9`^veCzWxWV3&)}1|ixzD=fYw8+BjG;sy^?aiSnNV{%Az+OuE zT(2?-P=`CCu2!xw7T>0XC_VOyc*$`CuV^GZke}zt?yReV3$ogZtJE4wpE4*7|2hV_ zMJXtpTpM8oKiaH+*B@M@nsZsE({5w=HqF=ks-Rs09x_znT%5_(&5nC-z@Xta^}+4~ z+6`+$-ul|Bog9hV{eAow3=b{)kv(ORLWs~;_APdqrK8-1DjEVFWAxtsO*^Jp7$5m6 zuu+eqVaztynkt>_k_1RVn>qy1zpSyA=UzDF1M4v{)jzr|yV9Bbe7ERELLA-rBk!I_$147m5|&D%O15%gt9gcWux$>l`S z?X+5S0s*nqFrhg_JEb6ZXg_YT$A`TxXD=EWaS7!s=#U46RhekCOD9cS$c`!>xfF`h zN<@JFyhUu!VYoM>AZmtDO*Sgcwi{Tv@O%$-4RSx*P^2i>-!B~RJ)xes2}!2)*mlEc79yTVwsI`dK75PWxjeJDKK>V&6H(m_L2Cy-muJGmV`wl;aJWG@3JlUk$n{LN{w2xAI@Gqv1_2gIl z%GpS}aSc#PScp7L_+-h{INoKZA>f`I8T~$?R`)Ys8P-x*MP$4gbwsY?KK}(=O6o}g zWC&UbBx)&pjKsJ+-C>?~aO~@ zV(qc(0;@5g=#c1Q6cpE0V4LGoNl0nFX6qnFIA51LCqaDnT1in!!^hKHR>Yk!qG!7A z*!6mxGhf8(g~#iB+p!xQg}~KG5mSN<$}j1i(0r_CNZZ$wxfB}Qjq6aWVe1HvxT~8K%(e~5#6^5Zz`C&A_L@;a!;5#NH$J|)#B-K&PP1aA*}Un zIZ5S(@n+~1w&d(7l-PTkyCRry6O56Ata)MH=HQVi7mJ9|)59#T;KH#}85_*vF<|WU z#kHal`j6-_DgvVCRkUBCUkw+IvuGSZ$9sfsF9iW^C$^>rCCsRu2Bg$Dn7jk>5%Us| z_CFe*<3xJ#)?^3oK$4|v)1*hf09uV;jbtBJ?slWsk;fdz(3DYo_r`nCoYq;1MMsX_ z2~|U~N>x+?mrv&S0tI}(xi;c8MDaD1DB%mpv~0$=nNV-@QdXcyjhWo*|+xqX_a_q#hfq1(v=?gj6Ds)Txsv{f@~j8WJDkk zy>ZN7Uv#wJ@$x?*xNb28JvbtX9N0eHp(rQPzD%L9TBG47nFvYScj63>+xaoiK^ zZhfYsUCahwB3^#`r6M68WXd%qlei*ZMpPMlcdO4=Xb&b|Zbv2(?25QX;(RM0D66$5 zTxmRM&}a1E+K_1)TW|*U4C+g}9R`Ha59BG}%~_2>RdRl~t+knLEm<3)b&D#;)SuBB z35BIG+)ZZPKnIqJ2R^u9?u^|%5Okk1fV`>=Daz`EK*iCD>WJ>~WAr7^bl94@HwI58ykCUv*5w*3rax%Ez8-8U}%KamOy9oA~++Dpj@+{3K(B#ir z54E*LEeRvwUNa!732eaNhaUSaL7`}ESK|(e*%>3j`_BsP)u=3|fVxnwIH_9$gj(2B zU;~F?DitL|!rQ3^whS0G-VDuHJ!|~B1eV}DAtPA(jOfMjYoGj^SMpes5|?@|0ThU` z3Rt|a4iwufD%Uy7{Ll(3HRsx--9xYSCtzmvI*vy#4_`UGk=ZjPA==6`i{%S*(S?S{ zTPE8lubm9#^;4McGq1wFM8hM0YgiTA zT466)VT`%&3gTWc)UEdGq}QSX;wU$_8h6mFcH>rUkP9o~Hb+O`cSX`HQpsFq)_mUa z6X&}?Dq1rPj3(!9q&=H_xp~hR2H)K6CYWVG>P#u9btIXzdP?0|ZqaMkRF}T3&&Q-- zV+=_i6dDq##jX~!6QPZJE7ot z1{L@tY}}%)8P}`KmMX9Xu;(@pv=6vfI}!rGcbqid5ze_Dr(N|Rn1g!)9i)&;fum{r zfZ)5{{xd;$h(Og6`|qLOSqB>%%~F0m1f`5`C|(vjd4o7!E*RzM5+OlcLa7*bb6-?f zR7oENl-|2JwIU;=%o)A47=6B2v-|diB;Dk=$a)uavLyTPYYw3M-q)H-WO=3x&NGl` zH@TbFxuME5&2hRXf+)Z%uyA+`)2tw?0^??PEJP0?$qL@S*8JInd$v?0U*$a+UFpLe zU|9f>h?(ZZMmGzkgzwE_u_UIMQvBhN%tnR0EBstAkG%5yTsRN{kZH=7IpGWPG9N@? z78J-bA24pz97rfvgJo||yZ6-tRe4Di+F@f=P4gkRWZmPJ_=i%0&427 zz+$VP_u^|Q*{hIJjBq?Lora1x2AJ8)5jw6q!Zz?-9$|P!I#*A$xG7RzY zq3~m1l4t#(=ck5Pn#8uOG=Z_o&~ny``#_{CnN51uiMSTboL)fhh%3_@PjSqpFnXId zTl*hD*vtTQbfztpZpDxoR`Tf*8kEF%Q!(mcQ6}6THi(`DXLuP=ras1txtI*Q#qneK zgl2C4D?zAbU6#hpJ%ldp`RbCM;$hl+-6f`ojbxP)dqrwo;p+#ANWpmOkDUg>(zB#!*VvbFAxz(PhkBg@ZF6#BZ-UoKiBKP z7i%7+i%BTZkgIIkzIUFmn)Q}Ytlm!eg3gevtsI71E@v~?NFT)bzJ$4*lmR$=W$oLIKOmntyovCArqP8he*Lhpe%5uRLz4+d6 zcuO1>MdW5@rj5GiRpjf@cVWyP-6on>B3(o;KC@Q{^+3bJVog?t;Npy|w*--gO9++T ze!U9OFHXQ<8M;~#43fQeVU#5>Qq1v5M=3$yq?(T*{GiUDA|Ez^czGDx z{LZI`Sj}8r-k%Jdn2VSjo8*|^oY;fn^AS*%*E(hAeQr=co)H1Xs4JGu7%8|iYW%q>+8>Tc#afyd~I zX^gT+uz_NWnsJhwA>X3X>Vtj8RP7X-V7U;bxq5Q5oob~$S^zkI&j(m5Jc*wCPe!@l zPi}u{{c$gJzC};wtI?=&k&~-U-f#`47N@pBdr3xOCWz(-6UBSOE){g#Z=z~KN&cAA zo~sbZe*=1#6Y9zij0|h;LNJonyqiAA=lb=_!vowJ(n~L+%KfgM{NU(d>!70|<%v`# zqhe2Mi1*0XU?OeQaShlPJi@Ez@}fySCQI9jDVFA^o4J;40|EN$R|Ea(%l2td%gjq- z6-#w5ggHpk$^5ALhSh_G3f&n{ggBywNKdXC59axEnm+PGad&x@QcdlM!wokg@Wo>9 ztXveSTvN!jqhA_|Y5KQ44_eP&@Y-5+N3utUpwG#JIaHz@3GUQB+fs=e@{+_=+*TqB z|5Vup6tsL`bllan9Kce_r2I8lE<*E6(6uM8R))H56K@XMF%cle8n|wDr^1M)3kZb7 z8~P0X))^-O>tl?BK9w9vBe_TCv!jGSLsJqpz9#ShBPNq8?+>?9zW(#EMz0=y5z%(V z!ESD#8fw|;avMSvsGsR`^<)oY(m5K_y%ydJ@0oA!2_x;4CJyQ735`fB=0StkEuq*uKq#iKTIa>p?)B!rf#{Y?+`j>3cKeF|9XPN2CE7VASk_Y3h%V2&)p z!tD=)sY!fualoaANS%-^v(`%|WoA~?u8mZi;C7!6M_mwsN5lFCQCRl}Sd(qRAuxQMv`eS-$>zxu zSs~q{htNp!5_acp1l7wTrjm)fZ=Hrnww8o4|zU5k5eaM<> z@S(J>p`uWDOZ!QeS^g(ij&2ki%6b3;VjO^9*&pH%{jm1@Hx@(yi9c5y6|}DiaI(xv zJAPSg6lGFy=pgWA_2%@f(vwqq17byQ%xPVGpp(!{>`X#LAN^0uw>@7T!dh#~sAW9h ze?@T4@Fg;;%+u)H(7u{CJ+<||PknW>S@F^v(hgiek~4UbB%VCA2Y;nhvLQ@?yp&|P zAmePD4;s5vO1-ZaiT&&rsZB7R;~Mf)9vEUhL?vHhQsF{!7E5?A zY0|50!8Zx$t!SeQ`S}Tw&YgK45_RZhr5M=x9;F)KN|qXUbXa%gs>MlQTxk5z^`Hy+ z2ATn3sOuHnhC%bQhcWfz4wp2?6wDJCuyQb1HqB$46k+(yD00je^{SB-)YGC{-KorK zY*f&6a#9}I1UA&$$xSaRl8cSG-hs5vU6?s`I|Uc9l8aWs6NJyf^&TKc;Ntr$Tbm{k zrpD}zxDcjx9h|K<&XgrPbvenzH?+rj2w9V$oxvM(l*BppxK!V7;-1eJEcR1>D8GX5 zpKpv>RfBaQ*}zczc#z=kn8%ZIyuIPA)2XY*;9hKpcU2=kOjqd2mfmURjrq!VG5U<1 z0!v(BA@1y1zVVQrX15{ssR(jZf})^eFWpPUD%$LBwJT9G=BqPNQ4MkMCMXec^wp1y zA1XTNvDq!)-r4({8co^vr<`JsIQ7ty`nhxa7SL#+UEwkw&zy`&C*^4Zj%BXim?XZ| zsmpS}eZ4m#pPK=QT)Ejdt+t|6YN#%{uie1V)m2+kvh^(Jkus$Gu=CwfU%lna#jc*O ztVNY$B^%008^lN_r9jxj8)`NUTAhrUbpZL8)^d3KX|}ca7X##9XQlyzXBXn0{&bqu zwbESML3Tdo^j+^WkxEQ-^RHx6X7>@5o~{`^CS%2CB63%5RQpb&lw4RRxbw+OduNtWmF3Lv%oQ| zH&V+&`L08VW4sbt9mNV8ce;Qw)U^`mAscQHdI{|)VP>0P ztlu0IGAe9$XK=Sj=%-_(YKed_iAvEB45U2U@UcKU^aQhKdT`fN;0b9r4#zhR7q;i1 z!IyX%&C+K&mb<<(7*Y;ty&zIHYox*LDLK@i<1&$V_2TFpQYO4uuTVDO!7?s~tYb8a zjx~g3KHg_rmHCO=;=@!Q``3H<%y7xA%64GnPN3%L6QA_RQ?ioXw7NBnc_{q0{P z?gN9MfdX{PprC*b0OrO36d=Il^q&V1mhG8yK{~M|Q>1!=_$iC?YWIYa0?%(vKe>}Oqe1Q7- z1FEpCt6D`ORa>VTxJ zwXv+Nt);Ww_Z7%fXdsZSC0sx`904kt=*KGqzH@!gk^cw{P%v=Px3c;(nwg)GNe&?O zG~h$}H#AxDKcfB6>LvRt3fa@wn;$`T%LHsTYZ1N+8~?xrR29Hf;tx6h>-GK@p23Yd z?H&LR2Y?3v`0tbJiv%dxKj8gM3gyq4Hu4x+jRw>VAE4qH|BLvo2zgomi22k=M-reD zVfDlH%Z zV8v|Z>60&{(lNsKF#)1fuNtb zB3ggM{aYXTUvfdeviwPW<|m7s&VQ%EzXsqxPKUn&{z-1;CtxW6@PCQV{EG6;U*}t@ zAM@BZ5t#4Kx5-6Y_ivg1w%dQ2_^Bk!kL0<&VZSH-Ww-eKbl~3!#60DED(vx-lS}`% z7yg~|pJhItl0DTy_(|q!^e@Q%NfqHK)zg5=KdF+e{sq-vCysv%ul$tiX&~F5RN=P& zg6e;VwS7v&{3JH#Pa+b>e?jzzIr3YReHsPxCn|!|Z>axiYQMT({618D!+aWn>nG-p z=Wm$5w5nenIiBYJY5bR;m@U5lg!xzR_=jjPPnn)ZiTKF`6!b5czGvdkF(aN*J$2Im zNp%$R|4H@dnt19W{gYrc>|YT4cJTN#15bTrf8tX_{)YeKEajIPf8vAn6#l9I<4^di zcfY~^0{d5`f3FI_-|c?&hI~r$)P?LP$z;rLB>x`{WlwpYx;6deDTw=>=bxOMo+3YW z75Rx=miQa;@5S`H239?fDiQxaJ-lyj1KVgB>euI6wApIpD{|C)e6X~Bc zQvh|jUsg_kJcIvp$9j5~^%K*(>i@-v9U`5#BsPc!lKl=UY(TElPfKgNlFbLRS#=;^7*Pom+L|1J#w*BQxk8F0Yv?%U}+ P6A%($1542Q?YI929T5o( literal 0 HcmV?d00001 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(" ")); + } + } + } +}