diff --git a/.travis.yml b/.travis.yml index 0a8e7c43..ee81afd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: java install: ./gradlew -i -S assemble -script: ./gradlew -i -S check +script: ./gradlew -i -S -Poffline=true check before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ diff --git a/README.md b/README.md index fa6fa178..d5d768dd 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,12 @@ Documentation for: ![Fork Props Dialog](docs/fork-props-dialog.png) -3. Setup local AEM instances with dependencies and AEM dispatcher (see [prerequisites](https://github.com/Cognifide/gradle-aem-plugin/tree/develop#environment-configuration)) then build application using command: +3. Setup the local DEV environment containing: +- AEM instances with dependencies +- AEM dispatcher (see [prerequisites](https://github.com/Cognifide/gradle-aem-plugin/tree/develop#environment-configuration)) +- [Knot.x](https://github.com/Knotx/knotx) (see [prerequisites](https://github.com/Cognifide/gradle-aem-plugin/tree/develop#environment-configuration)) + +and build AEM application using command: ```bash aem/hosts @@ -128,6 +133,7 @@ Project is configured to have local environment which consists of: * native AEM instances running on local file system, * virtualized Apache HTTP Server with AEM Dispatcher module running on Docker ([official httpd image](https://hub.docker.com/_/httpd)). +* virtualized [Knot.x](https://github.com/Knotx/knotx) reactive integration framework running on Docker Assumptions: @@ -137,6 +143,7 @@ Assumptions: * http://example.com -> which maps to `/content/example/live` content root on publish * http://demo.example.com -> which maps to `/content/example/demo` content root on publish * http://author.example.com -> which is proxy to the author instance + * http:://knotx.demo.example.com -> which maps to `/content/example/demo` content root on publish and [processes `` tags](https://github.com/Knotx/knotx-fragments) ## Building @@ -168,7 +175,7 @@ Task `setup` will: * set up AEM environment (run HTTPD service on Docker) and install AEM dispatcher module * build AEM application (compose assembly CRX package from many) * migrate AEM application (for projects already deployed on production to upgrade JCR content in case of changed application behavior) -* clean AEM environment (restart HTTPD service then clean AEM dispatcher caches) +* clean AEM environment (restart HTTPD service then clean AEM dispatcher caches, restart Knot.x) * check AEM environment (quickly check responsiveness of deployed application) * run integration tests * run functional tests @@ -176,6 +183,21 @@ Task `setup` will: To sum up, all things needed by developer are fully automated in one place / Gradle build. Still all separate concerns like running tests, only building application, only running tests, could be used separately by running particular Gradle tasks. +## Validation + +### AEM mappings +You can check AEM Dispatcher url rewrites with: + +[example.com/en-us.html](http://example.com/en-us.html) +[demo.example.com/en-us.html](http://demo.example.com/en-us.html) + +### Knot.x Fragments processing +Fully static template page containing "dynamic" placeholder from Dispatcher: +[demo.example.com/products/details.html](http://demo.example.com/products/details.html) + +Page containing data assembled by Knot.x: +[knotx.demo.example.com/products/details.html](http://knotx.demo.example.com/products/details.html) + ## Tips & tricks * To run some task only for subproject, use project path as a prefix, for instance: `gradlew :aem:site.demo:sync`. diff --git a/aem/gradle/environment.gradle.kts b/aem/gradle/environment.gradle.kts index 924d9038..0bc9d2b2 100644 --- a/aem/gradle/environment.gradle.kts +++ b/aem/gradle/environment.gradle.kts @@ -6,7 +6,8 @@ configure { "example.com", "demo.example.com", "author.example.com", - "dispatcher.example.com" + "dispatcher.example.com", + "knotx.demo.example.com" ) distributions { download("http://download.macromedia.com/dispatcher/download/dispatcher-apache2.4-linux-x86_64-4.3.2.tar.gz").then { @@ -15,7 +16,8 @@ configure { } directories { regular( - "httpd/logs" + "httpd/logs", + "knotx/logs" ) cache( "httpd/cache/content/example/live", @@ -27,6 +29,7 @@ configure { url("Demo site", "http://demo.example.com/en-us.html", text = "English") url("Author login", "http://author.example.com/libs/granite/core/content/login.html" + "?resource=%2F&\$\$login\$\$=%24%24login%24%24&j_reason=unknown&j_reason_code=unknown", text = "AEM Sign In") + url("Knot.x", "http://knotx.demo.example.com/products/details.html", text = "Content filled by Knot.x integration framework.") } } diff --git a/aem/gradle/environment/docker-compose.yml.peb b/aem/gradle/environment/docker-compose.yml.peb index 7591370c..14fcd714 100644 --- a/aem/gradle/environment/docker-compose.yml.peb +++ b/aem/gradle/environment/docker-compose.yml.peb @@ -20,5 +20,15 @@ services: extra_hosts: - "host.docker.internal:10.0.2.2" {% endif %} + knotx: + image: knotx/knotx:2.0.0-RC5 + command: ["knotx", "run-knotx"] + volumes: + - "{{ environment.dockerConfigPath }}/knotx/conf:/usr/local/knotx/conf" + - "{{ environment.dockerRootPath }}/knotx/logs:/var/log/knotx" + ports: + - "8092:8092" + networks: + - docker-net networks: docker-net: diff --git a/aem/gradle/environment/httpd/conf/httpd.conf b/aem/gradle/environment/httpd/conf/httpd.conf index 20c91dce..1fc48101 100644 --- a/aem/gradle/environment/httpd/conf/httpd.conf +++ b/aem/gradle/environment/httpd/conf/httpd.conf @@ -194,7 +194,7 @@ LoadModule dir_module modules/mod_dir.so #LoadModule speling_module modules/mod_speling.so #LoadModule userdir_module modules/mod_userdir.so LoadModule alias_module modules/mod_alias.so -#LoadModule rewrite_module modules/mod_rewrite.so +LoadModule rewrite_module modules/mod_rewrite.so LoadModule dispatcher_module modules/mod_dispatcher.so diff --git a/aem/gradle/environment/httpd/conf/vhost/knotx.demo.example.com.conf b/aem/gradle/environment/httpd/conf/vhost/knotx.demo.example.com.conf new file mode 100644 index 00000000..ed8892f5 --- /dev/null +++ b/aem/gradle/environment/httpd/conf/vhost/knotx.demo.example.com.conf @@ -0,0 +1,14 @@ + + ServerName knotx.demo.example.com + + RewriteEngine On + + RewriteCond %{REQUEST_METHOD} =GET + RewriteRule ^(.*\.html)$ - [E=KNOTX_REQ:true] + + RewriteCond %{ENV:KNOTX_REQ} ^true$ + RewriteRule ^/(.*)$ http://knotx:8092/$1 [P,L] + + RewriteRule ^/(.*)$ http://demo.example.com/$1 [P,L] + + diff --git a/aem/gradle/environment/knotx/conf/application.conf b/aem/gradle/environment/knotx/conf/application.conf new file mode 100644 index 00000000..28bc3799 --- /dev/null +++ b/aem/gradle/environment/knotx/conf/application.conf @@ -0,0 +1,12 @@ +########### Modules to start ########### +# Modules map specify a list of verticles to be started by Knot.x. +# Each line should have a form of = +# where alias is just a name that you can use later in order to define configuration for the module +# verticle-class-name is a fully qualified class name of the verticle. +# +# This JSON object is filled in included files. +modules {} + + +include required(classpath("server.conf")) +include required(classpath("knots/templateEngineStack.conf")) diff --git a/aem/gradle/environment/knotx/conf/bootstrap.json b/aem/gradle/environment/knotx/conf/bootstrap.json new file mode 100644 index 00000000..9610e6fb --- /dev/null +++ b/aem/gradle/environment/knotx/conf/bootstrap.json @@ -0,0 +1,15 @@ +{ + "configRetrieverOptions": { + "scanPeriod": 5000, + "stores": [ + { + "type": "file", + "format": "conf", + "config": { + "path": "${KNOTX_HOME}/conf/application.conf" + }, + "optional": false + } + ] + } +} diff --git a/aem/gradle/environment/knotx/conf/knots/templateEngineKnot.conf b/aem/gradle/environment/knotx/conf/knots/templateEngineKnot.conf new file mode 100644 index 00000000..c8624e70 --- /dev/null +++ b/aem/gradle/environment/knotx/conf/knots/templateEngineKnot.conf @@ -0,0 +1,26 @@ +# Vert.x event bus delivery options used when communicating with other verticles +address = knotx.knot.te.handlebars + +engine { + factory = handlebars + config = { + # Algorithm used to build a hash key of the compiled handlebars snippets. + # The hash is computed for the snippet handlebars source code using a selected algorithm. + # The name should be a standard Java Security name (such as "SHA", "MD5", and so on). + # Default value is MD5 + # + # cacheKeyAlgorithm = MD5 + + # Size of the compiled snippets cache. After reaching the max size, new elements will replace the oldest one. + cacheSize = 1000 + + # Symbol used as a start delimiter of handlebars expression. If not use, a default '{{' is used + # + # startDelimiter = + + # Symbol used as a end delimiter of handlebars expression. If not use, a default '}}' is used + # + # endDelimiter = + } +} + diff --git a/aem/gradle/environment/knotx/conf/knots/templateEngineStack.conf b/aem/gradle/environment/knotx/conf/knots/templateEngineStack.conf new file mode 100644 index 00000000..2ad64f1c --- /dev/null +++ b/aem/gradle/environment/knotx/conf/knots/templateEngineStack.conf @@ -0,0 +1,11 @@ +########### Data Bridge Stack ########### +modules { + templateEngine = "io.knotx.te.core.TemplateEngineKnot" +} + +########### Modules configurations ########### +config.templateEngine { + options.config { + include required(classpath("knots/templateEngineKnot.conf")) + } +} diff --git a/aem/gradle/environment/knotx/conf/logback.xml b/aem/gradle/environment/knotx/conf/logback.xml new file mode 100644 index 00000000..5522764b --- /dev/null +++ b/aem/gradle/environment/knotx/conf/logback.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + diff --git a/aem/gradle/environment/knotx/conf/openapi.yaml b/aem/gradle/environment/knotx/conf/openapi.yaml new file mode 100644 index 00000000..d77b51c4 --- /dev/null +++ b/aem/gradle/environment/knotx/conf/openapi.yaml @@ -0,0 +1,32 @@ +openapi: "3.0.0" +info: + version: 1.0.0 + title: Knot.x Stack OAS + description: This is the server API specification. + +servers: +- url: https://{domain}:{port} + description: The local API server + variables: + domain: + default: localhost + description: api domain + port: + enum: + - '8092' + - '443' + default: '8092' + +paths: + /products/*: + get: + operationId: products-get + responses: + default: + description: Remote repository template processing with http action and te + /assets/*: + get: + operationId: assets-get + responses: + default: + description: Return assets as is from the repository diff --git a/aem/gradle/environment/knotx/conf/routes/handlers/fragmentsHandler.conf b/aem/gradle/environment/knotx/conf/routes/handlers/fragmentsHandler.conf new file mode 100644 index 00000000..a5df82cc --- /dev/null +++ b/aem/gradle/environment/knotx/conf/routes/handlers/fragmentsHandler.conf @@ -0,0 +1,34 @@ +########### This configuration is overloaded in integration tests! ########### +tasks { + hello-world { + action = hello-inline + onTransitions { + _success { + action = te-hbs + } + } + } +} + +actions { + hello-inline { + factory = inline-payload + config { + alias = hello + payload { + _result { + message = "Content filled by Knot.x integration framework." + } + } + } + } + te-hbs { + factory = knot + config { + address = knotx.knot.te.handlebars + deliveryOptions { + sendTimeout = 3000 + } + } + } +} diff --git a/aem/gradle/environment/knotx/conf/routes/handlers/httpRepoConnectorHandler.conf b/aem/gradle/environment/knotx/conf/routes/handlers/httpRepoConnectorHandler.conf new file mode 100644 index 00000000..cf5472e1 --- /dev/null +++ b/aem/gradle/environment/knotx/conf/routes/handlers/httpRepoConnectorHandler.conf @@ -0,0 +1,62 @@ +# Vert.x event bus delivery options used when communicating with other verticles +# see http://vertx.io/docs/vertx-core/dataobjects.html#HttpClientOptions for the details what can be configured +# +clientOptions { + maxPoolSize = 1000 + idleTimeout = 120 # seconds + tryUseCompression = true + + # If you're going to use SSL (clientDestination.scheme='https') then here you'd need to configure + # some aspects related to the SSL negotiation and validation. + # + # Whether all server certificated should be trusted or not (e.g. self-signed certificates) + # trustAll = true + # + # Hostname verification + # verifyHost = false + # + # It will force SSL SNI (Server Name Indication). The SNI will be set to the same value as 'hostHeader' (set in ClientDestination) + # forceSni = true +} + +# HTTP Repository connection details +clientDestination { + # Connection scheme: http or https + scheme = http + + # domain or the IP of the host: e.g. localhost, 10.0.11.2 + domain = httpd + + ## Port on which the host listens, e.g. 8080, 3001, etc. + port = 80 + + # Host header override to be used with a communication to the repository. If it's set, it overrides any value in the 'Host' header, and sets the SNI SSL to the same value. + hostHeader = demo.example.com +} + +# List of allowed request headers that will be send to HTTP repository. +# Each item is a string that defines regex, e.g. to match any char use `.*` +# +allowedRequestHeaders = [ + "Accept.*" + Authorization + Connection + Cookie + Date + "Edge.*" + "If.*" + Origin + Pragma + Proxy-Authorization + "Surrogate.*" + User-Agent + Via + "X-.*" + "Host.*" +] + +# Statically defined HTTP request header sent in every request to the repository +customHttpHeader = { + name = X-User-Agent + value = Knot.x +} diff --git a/aem/gradle/environment/knotx/conf/routes/operations.conf b/aem/gradle/environment/knotx/conf/routes/operations.conf new file mode 100644 index 00000000..061a27f4 --- /dev/null +++ b/aem/gradle/environment/knotx/conf/routes/operations.conf @@ -0,0 +1,30 @@ +routingOperations = ${routingOperations} [ + { + operationId = products-get + handlers = ${config.server.handlers.common.request} [ + { + name = httpRepoConnectorHandler + config = { include required(classpath("routes/handlers/httpRepoConnectorHandler.conf")) } + }, + { + name = fragmentsProviderHtmlSplitter + }, + { + name = fragmentsHandler + config = { include required(classpath("routes/handlers/fragmentsHandler.conf")) } + }, + { + name = fragmentsAssemblerHandler + } + ] ${config.server.handlers.common.response} + } + { + operationId = assets-get + handlers = ${config.server.handlers.common.request} [ + { + name = httpRepoConnectorHandler + config = { include required(classpath("routes/handlers/httpRepoConnectorHandler.conf")) } + } + ] ${config.server.handlers.common.response} + } +] diff --git a/aem/gradle/environment/knotx/conf/server.conf b/aem/gradle/environment/knotx/conf/server.conf new file mode 100644 index 00000000..ac69a109 --- /dev/null +++ b/aem/gradle/environment/knotx/conf/server.conf @@ -0,0 +1,97 @@ +########### Knot.x Server ########### +modules { + server = "io.knotx.server.KnotxServerVerticle" +} + +########### Modules configurations ########### +config.server { + handlers.common { + request = [ + { + name = cookieHandler + }, + { + name = bodyHandler + }, + { + name = requestContextHandler + } + ], + response = [ + { + name = headerHandler + # Statically defined HTTP response header returned to the client in every HTTP response + config { + name = X-Server + value = Knot.x-Custom-Header + } + }, + { + name = writerHandler + # List of HTTP response headers Knot.x can return to the client + config.allowedResponseHeaders = [ + Access-Control-Allow-Origin + Content-Type + Content-Length + ] + } + ] + } + options.config { + # Configuraiton of HTTP server + serverOptions { + # Knot.x server HTTP port + port = 8092 + + # If you want a server to serve SSL connections you can configure it here + # + # Enable SSL + # ssl = true + # + # Path on the server the keystore.jks file is located + # keyStoreOptions.path = + # + # Keystore password + # keyStoreOptions.password = + } + + # Location of your Open API spec. It can be an absolute path, a local path or remote url (with HTTP protocol). + routingSpecificationLocation = /openapi.yaml + + displayExceptionDetails = true + dropRequestOptions { + # FixMe this should be enabled by default, see https://github.com/Knotx/knotx-server-http/issues/20 for details + enabled = false + + # Status code that is served if the response is dropped, default is 429, "Too Many Requests" + # dropRequestResponseCode = + + # Number of request that single Server insance can support concurrently. Default value is 1000. + # backpressureBufferCapacity + + # Strategy how to deal with backpressure buffer overflow. Default is DROP_LATEST. + # backpressureStrategy = + } + + routingOperations = [] + include required(classpath("routes/operations.conf")) + + # Global handlers section + globalHandlers = [ + # access log, by default logged to the knotx-access.log in the logs directory + { + name = loggerHandler + config { + immediate = true + format = DEFAULT + } + } + ] + } + + # The options object carries-on configuration called DeploymentOptions for a given verticle. + # It allows you to control the verticle behaviour, such as how many instances, classpath isolation, workers, etc. + # See available options http://vertx.io/docs/vertx-core/dataobjects.html#DeploymentOptions + # + # options {} +} diff --git a/aem/site.demo/src/main/content/jcr_root/content/example/demo/products/.content.xml b/aem/site.demo/src/main/content/jcr_root/content/example/demo/products/.content.xml new file mode 100644 index 00000000..cfb9f94a --- /dev/null +++ b/aem/site.demo/src/main/content/jcr_root/content/example/demo/products/.content.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/aem/site.demo/src/main/content/jcr_root/content/example/demo/products/details/.content.xml b/aem/site.demo/src/main/content/jcr_root/content/example/demo/products/details/.content.xml new file mode 100644 index 00000000..02d758bd --- /dev/null +++ b/aem/site.demo/src/main/content/jcr_root/content/example/demo/products/details/.content.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/aem/sites/src/main/content/jcr_root/apps/example/sites/components/products-detail-page/.content.xml b/aem/sites/src/main/content/jcr_root/apps/example/sites/components/products-detail-page/.content.xml new file mode 100644 index 00000000..6b9d615e --- /dev/null +++ b/aem/sites/src/main/content/jcr_root/apps/example/sites/components/products-detail-page/.content.xml @@ -0,0 +1,7 @@ + + diff --git a/aem/sites/src/main/content/jcr_root/apps/example/sites/components/products-detail-page/products-detail-page.html b/aem/sites/src/main/content/jcr_root/apps/example/sites/components/products-detail-page/products-detail-page.html new file mode 100644 index 00000000..5aabd9ef --- /dev/null +++ b/aem/sites/src/main/content/jcr_root/apps/example/sites/components/products-detail-page/products-detail-page.html @@ -0,0 +1,43 @@ + + + + + + Example | ${page.title} + + + + + + +
+ +

{{hello._result.message}}

+
+
+
+
+
+ AEM application built by +
+
+ + + +
+
+ +
+ All rights reserved | 2019 © Company +
+
+ + + + + diff --git a/aem/sites/src/main/content/jcr_root/apps/example/sites/templates/products-detail-page/.content.xml b/aem/sites/src/main/content/jcr_root/apps/example/sites/templates/products-detail-page/.content.xml new file mode 100644 index 00000000..eceb96e6 --- /dev/null +++ b/aem/sites/src/main/content/jcr_root/apps/example/sites/templates/products-detail-page/.content.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/aem/sites/src/main/kotlin/com/company/example/aem/sites/components/page/PageModel.kt b/aem/sites/src/main/kotlin/com/company/example/aem/sites/components/page/PageModel.kt index 9edcbed2..554bffe7 100644 --- a/aem/sites/src/main/kotlin/com/company/example/aem/sites/components/page/PageModel.kt +++ b/aem/sites/src/main/kotlin/com/company/example/aem/sites/components/page/PageModel.kt @@ -56,10 +56,11 @@ class PageModel : Serializable { } private fun determineLanguageCode(): String { - val languagePath = LanguageUtil.getLanguageRoot(resource.path) - val languagePart = languagePath.substringAfterLast("/") +// val languagePath = LanguageUtil.getLanguageRoot(resource.path) +// val languagePart = languagePath.substringAfterLast("/") - return LanguageUtil.getLanguage(languagePart).languageCode ?: "en" +// return LanguageUtil.getLanguage(languagePart).languageCode ?: "en" + return "en" } }