diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 519ceef..c66c1a6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,7 +38,7 @@ jobs: # Validate wrapper - name: Gradle Wrapper Validation - uses: gradle/wrapper-validation-action@v1.1.0 + uses: gradle/wrapper-validation-action@v2.0.0 # Set up Java environment for the next steps - name: Setup Java @@ -49,7 +49,7 @@ jobs: # Setup Gradle - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: gradle-home-cache-cleanup: true @@ -88,7 +88,7 @@ jobs: # Store already-built plugin as an artifact for downloading - name: Upload artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ steps.artifact.outputs.filename }} path: ./build/distributions/content/*/* @@ -113,7 +113,7 @@ jobs: # Setup Gradle - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: gradle-home-cache-cleanup: true @@ -124,14 +124,14 @@ jobs: # Collect Tests Result of failed tests - name: Collect Tests Result if: ${{ failure() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: tests-result path: ${{ github.workspace }}/build/reports/tests # Upload the Kover report to CodeCov - name: Upload Code Coverage Report - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: ${{ github.workspace }}/build/reports/kover/report.xml @@ -166,7 +166,7 @@ jobs: # Run Qodana inspections - name: Qodana - Code Inspection - uses: JetBrains/qodana-action@v2023.3.0 + uses: JetBrains/qodana-action@v2023.3.1 with: cache-default-branch-only: true @@ -197,13 +197,13 @@ jobs: # Setup Gradle - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: gradle-home-cache-cleanup: true # Cache Plugin Verifier IDEs - name: Setup Plugin Verifier IDEs Cache - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} @@ -215,7 +215,7 @@ jobs: # Collect Plugin Verifier Result - name: Collect Plugin Verifier Result if: ${{ always() }} - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: pluginVerifier-result path: ${{ github.workspace }}/build/reports/pluginVerifier diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1d51521..0eafc2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,7 +33,7 @@ jobs: # Setup Gradle - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: gradle-home-cache-cleanup: true diff --git a/.github/workflows/run-ui-tests.yml b/.github/workflows/run-ui-tests.yml index 05e483b..aedb91d 100644 --- a/.github/workflows/run-ui-tests.yml +++ b/.github/workflows/run-ui-tests.yml @@ -44,7 +44,7 @@ jobs: # Setup Gradle - name: Setup Gradle - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: gradle-home-cache-cleanup: true @@ -54,7 +54,7 @@ jobs: # Wait for IDEA to be started - name: Health Check - uses: jtalk/url-health-check-action@v3 + uses: jtalk/url-health-check-action@v4 with: url: http://127.0.0.1:8082 max-attempts: 15 diff --git a/.readme/mark-dir.png b/.readme/mark-dir.png new file mode 100644 index 0000000..d4a5d84 Binary files /dev/null and b/.readme/mark-dir.png differ diff --git a/CHANGELOG.md b/CHANGELOG.md index aa8d066..c75c388 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ## [Unreleased] +- Updated http libs +- bug fixes + ## [0.9.1] - 2023-12-12 - Updated with latest build template diff --git a/README.md b/README.md index 465036d..84b4d14 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,11 @@ Powered by [AEM Groovy Console](https://github.com/orbinson/aem-groovy-console). ![Screenshot](.readme/code-completion.png) ![Screenshot](.readme/code-completion-1.png) - + +### Groovy Scripts Support +To run .groovy files as AEM scripts mark folder as "AEM Scripts Source Root", all groovy files in marked folder will be treated as AEM Scripts. +![Screenshot](.readme/mark-dir.png) + ## Usage Example https://youtu.be/1iL1Qhcp_x0 @@ -36,4 +40,4 @@ https://youtu.be/1iL1Qhcp_x0 ## How to get? You can download it from [JetBrains Marketplace](https://plugins.jetbrains.com/plugin/19633-aem-groovy-console) or directly in -your Intellij Idea by searching for "AEM Groovy Console". \ No newline at end of file +your Intellij Idea by searching for "AEM Groovy Console". diff --git a/gradle.properties b/gradle.properties index 4e70096..4981a37 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginGroup = com.github.bobi.aemgroovyconsoleplugin pluginName = aem-groovyconsole-plugin pluginRepositoryUrl = https://github.com/bobi/aem-groovyconsole-plugin # SemVer format -> https://semver.org -pluginVersion = 0.9.1 +pluginVersion = 0.9.2 # Supported build number ranges and IntelliJ Platform versions -> https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html pluginSinceBuild = 231 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a73de41..99eec69 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,13 +3,13 @@ annotations = "24.1.0" groovyConsole = "19.0.4" aecu = "6.5.0" -apacheHttpClient = "5.3" -jjwt = "0.12.3" +okhttp = "4.12.0" +jjwt = "0.12.5" # plugins -kotlin = "1.9.21" +kotlin = "1.9.22" changelog = "2.2.0" -gradleIntelliJPlugin = "1.16.1" +gradleIntelliJPlugin = "1.17.0" qodana = "0.1.13" kover = "0.7.5" @@ -18,13 +18,14 @@ annotations = { group = "org.jetbrains", name = "annotations", version.ref = "an groovyConsoleApi = { group = "be.orbinson.aem", name = "aem-groovy-console-api", version.ref = "groovyConsole" } groovyConsoleBundle = { group = "be.orbinson.aem", name = "aem-groovy-console-bundle", version.ref = "groovyConsole" } aecuApi = { group = "de.valtech.aecu", name = "aecu.api", version.ref = "aecu" } -apacheHttpClient = { group = "org.apache.httpcomponents.client5", name = "httpclient5", version.ref = "apacheHttpClient" } +okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } +okhttpLogging = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } jjwtApi = { group = "io.jsonwebtoken", name = "jjwt-api", version.ref = "jjwt" } jjwtImpl = { group = "io.jsonwebtoken", name = "jjwt-impl", version.ref = "jjwt" } jjwtGson = { group = "io.jsonwebtoken", name = "jjwt-gson", version.ref = "jjwt" } [bundles] -groovyConsole = ["groovyConsoleApi", "groovyConsoleBundle", "aecuApi", "apacheHttpClient", "jjwtApi", "jjwtImpl", "jjwtGson"] +groovyConsole = ["groovyConsoleApi", "groovyConsoleBundle", "aecuApi", "okhttp", "okhttpLogging", "jjwtApi", "jjwtImpl", "jjwtGson"] [plugins] changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } diff --git a/settings.gradle.kts b/settings.gradle.kts index d1cea7f..2dd521f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" } rootProject.name = "aem-groovyconsole-plugin" diff --git a/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/dsl/AemScriptExtensionClassFinder.kt b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/dsl/AemScriptExtensionClassFinder.kt index 3f04113..c5bcb6b 100644 --- a/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/dsl/AemScriptExtensionClassFinder.kt +++ b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/dsl/AemScriptExtensionClassFinder.kt @@ -3,8 +3,8 @@ package com.github.bobi.aemgroovyconsoleplugin.dsl import be.orbinson.aem.groovy.console.builders.NodeBuilder import be.orbinson.aem.groovy.console.builders.PageBuilder import be.orbinson.aem.groovy.console.table.Table +import com.github.bobi.aemgroovyconsoleplugin.utils.isInternal import com.intellij.openapi.application.PathManager -import com.intellij.openapi.application.ex.ApplicationManagerEx import com.intellij.openapi.project.Project import com.intellij.openapi.util.text.StringUtil import com.intellij.openapi.vfs.JarFileSystem @@ -57,7 +57,7 @@ class AemScriptExtensionClassFinder(project: Project) : NonClasspathClassFinder( val searchScope = NonClasspathDirectoriesScope.compose(roots) private fun buildClassesRoots(): List { - val isInternal = java.lang.Boolean.getBoolean(ApplicationManagerEx.IS_INTERNAL_PROPERTY) + val isInternal = isInternal() return jarForClasses.mapNotNullTo(LinkedHashSet()) { clazz -> val jarForClass = PathManager.getJarForClass(clazz) diff --git a/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/AdobeIMSTokenProvider.kt b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/AdobeIMSTokenProvider.kt index adaa1b4..8ba7de7 100644 --- a/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/AdobeIMSTokenProvider.kt +++ b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/AdobeIMSTokenProvider.kt @@ -5,25 +5,24 @@ import com.github.bobi.aemgroovyconsoleplugin.services.model.AccessToken import com.github.bobi.aemgroovyconsoleplugin.services.model.AemCertificateToken import com.github.bobi.aemgroovyconsoleplugin.services.model.AemDevToken import com.github.bobi.aemgroovyconsoleplugin.services.model.AuthType +import com.github.bobi.aemgroovyconsoleplugin.utils.invokeAndWaitIfNeeded import com.google.gson.Gson import com.google.gson.GsonBuilder -import com.intellij.openapi.Disposable +import com.google.gson.reflect.TypeToken +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.application.runWriteAction import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import io.jsonwebtoken.Jwts -import org.apache.hc.client5.http.classic.methods.HttpPost -import org.apache.hc.client5.http.entity.UrlEncodedFormEntity -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient -import org.apache.hc.client5.http.impl.classic.HttpClients -import org.apache.hc.core5.http.HttpException -import org.apache.hc.core5.http.HttpStatus -import org.apache.hc.core5.http.io.entity.EntityUtils -import org.apache.hc.core5.http.message.BasicNameValuePair -import org.apache.hc.core5.net.URIBuilder +import okhttp3.FormBody +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request import org.bouncycastle.openssl.PEMKeyPair import org.bouncycastle.openssl.PEMParser import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter +import java.io.IOException import java.io.StringReader import java.security.KeyFactory import java.security.interfaces.RSAPrivateKey @@ -40,11 +39,11 @@ import kotlin.time.toJavaDuration * Date/Time: 09.07.2023 14:07 */ @Service(Service.Level.PROJECT) -class AdobeIMSTokenProvider : Disposable { +class AdobeIMSTokenProvider { private val gson: Gson = GsonBuilder().create() - private val httpClient: CloseableHttpClient by lazy { - return@lazy HttpClients.createDefault() + private val httpClient: OkHttpClient by lazy { + return@lazy OkHttpClient() } fun getAccessToken(config: AemServerHttpConfig): String { @@ -60,7 +59,7 @@ class AdobeIMSTokenProvider : Disposable { return fetchAccessToken(config).accessToken } else { try { - val token = PasswordsService.getAccessToken(config.id) + val token = invokeAndWaitIfNeeded { runReadAction { PasswordsService.getAccessToken(config.id) } } if (token !== null) { val accessToken = verifyAccessToken(token) @@ -71,7 +70,14 @@ class AdobeIMSTokenProvider : Disposable { } catch (e: Throwable) { val accessToken = fetchAccessToken(config) - PasswordsService.setAccessToken(config.id, gson.toJson(accessToken)) + invokeAndWaitIfNeeded { + runWriteAction { + PasswordsService.setAccessToken( + config.id, + gson.toJson(accessToken) + ) + } + } return accessToken.accessToken } @@ -82,12 +88,14 @@ class AdobeIMSTokenProvider : Disposable { val accessToken = gson.fromJson(token, AccessToken::class.java) val splitToken = accessToken.accessToken.split(Regex("\\.")) - val unsignedToken = splitToken[0] + "." + splitToken[1] + "." - val jwt = Jwts.parser().build().parseUnsecuredClaims(unsignedToken) + val payload = gson.fromJson( + Base64.getDecoder().decode(splitToken[1]).decodeToString(), + object : TypeToken>() {} + ) - val createdAt = jwt.payload["created_at", String::class.java].toLong() - val expiresIn = jwt.payload["expires_in", String::class.java].toLong() + val createdAt = payload["created_at"]?.toString()?.toLong() ?: 0 + val expiresIn = payload["expires_in"]?.toString()?.toLong() ?: 0 val expiresAt = Instant.ofEpochMilli(createdAt + expiresIn).minus(1.hours.toJavaDuration()) @@ -115,29 +123,29 @@ class AdobeIMSTokenProvider : Disposable { val jwtToken = jwtBuilder.signWith(readPrivateKey(integration.privateKey), Jwts.SIG.RS256).compact() - val uri = URIBuilder().apply { - scheme = "https" - host = integration.imsEndpoint - path = "/ims/exchange/jwt" - }.build() - - val request = HttpPost(uri).apply { - entity = UrlEncodedFormEntity( - listOf( - BasicNameValuePair("client_id", integration.technicalAccount.clientId), - BasicNameValuePair("client_secret", integration.technicalAccount.clientSecret), - BasicNameValuePair("jwt_token", jwtToken) - ) - ) - } - - val token = httpClient.execute(request) { response -> - if (HttpStatus.SC_OK == response.code) { - val token = EntityUtils.toString(response.entity) - - return@execute gson.fromJson(token, AccessToken::class.java) + val uri = HttpUrl.Builder() + .scheme("https") + .host(integration.imsEndpoint) + .encodedPath("/ims/exchange/jwt") + .build() + + val formBody = FormBody.Builder() + .add("client_id", integration.technicalAccount.clientId) + .add("client_secret", integration.technicalAccount.clientSecret) + .add("jwt_token", jwtToken) + .build() + + val request: Request = Request.Builder() + .url(uri) + .post(formBody) + .build() + + val token = httpClient.newCall(request).execute().use { response -> + val body = response.body + if (response.isSuccessful && body != null) { + return@use gson.fromJson(body.string(), AccessToken::class.java) } else { - throw HttpException("${response.code} ${response.reasonPhrase}") + throw IOException("${response.code} ${response.message}") } } @@ -152,10 +160,6 @@ class AdobeIMSTokenProvider : Disposable { return gson.fromJson(token, AemCertificateToken::class.java) } - override fun dispose() { - httpClient.close() - } - companion object { fun getInstance(project: Project): AdobeIMSTokenProvider = project.service() diff --git a/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/GroovyConsoleHttpService.kt b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/GroovyConsoleHttpService.kt index a564d72..b73424f 100644 --- a/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/GroovyConsoleHttpService.kt +++ b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/services/http/GroovyConsoleHttpService.kt @@ -5,65 +5,52 @@ import com.github.bobi.aemgroovyconsoleplugin.services.http.model.GroovyConsoleO import com.github.bobi.aemgroovyconsoleplugin.services.http.model.GroovyConsoleTable import com.github.bobi.aemgroovyconsoleplugin.services.model.AemServerConfig import com.github.bobi.aemgroovyconsoleplugin.services.model.AuthType +import com.github.bobi.aemgroovyconsoleplugin.utils.isInternal import com.google.gson.Gson import com.google.gson.GsonBuilder -import com.intellij.openapi.Disposable import com.intellij.openapi.components.Service import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.project.Project import io.ktor.utils.io.core.* -import org.apache.hc.client5.http.classic.methods.HttpPost -import org.apache.hc.client5.http.config.ConnectionConfig -import org.apache.hc.client5.http.config.RequestConfig -import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient -import org.apache.hc.client5.http.impl.classic.HttpClients -import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder -import org.apache.hc.client5.http.utils.Base64 -import org.apache.hc.core5.http.ClassicHttpResponse -import org.apache.hc.core5.http.Header -import org.apache.hc.core5.http.HttpException -import org.apache.hc.core5.http.HttpStatus -import org.apache.hc.core5.http.message.BasicHeader -import org.apache.hc.core5.net.URIBuilder -import org.apache.hc.core5.util.Timeout +import okhttp3.FormBody +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import java.io.IOException import java.io.InputStreamReader import java.io.StringReader -import java.net.URI import java.time.Duration +import java.util.* import kotlin.io.use import kotlin.text.toByteArray + /** * User: Andrey Bardashevsky * Date/Time: 02.08.2022 16:06 */ @Service(Service.Level.PROJECT) -class GroovyConsoleHttpService(project: Project) : Disposable { +class GroovyConsoleHttpService(project: Project) { private val imsTokenProvider = AdobeIMSTokenProvider.getInstance(project) + private val httpClient: OkHttpClient by lazy { + val timeout = Duration.ofMinutes(10) - private val httpClient: CloseableHttpClient by lazy { - val timeout = Timeout.of(Duration.ofMinutes(10)) - - return@lazy HttpClients.custom() - .setConnectionManager( - PoolingHttpClientConnectionManagerBuilder.create() - .setDefaultConnectionConfig( - ConnectionConfig.custom() - .setConnectTimeout(timeout) - .setSocketTimeout(timeout) - .build() - ) - .build() - ) - .setDefaultRequestConfig( - RequestConfig.custom() - .setConnectionRequestTimeout(timeout) - .build() - ) - .build() + val builder = OkHttpClient.Builder() + + if (isInternal()) { + builder.addInterceptor(HttpLoggingInterceptor { println(it) }.setLevel(HttpLoggingInterceptor.Level.BODY)) + } + + builder.callTimeout(timeout) + builder.writeTimeout(timeout) + builder.connectTimeout(timeout) + builder.readTimeout(timeout) + + return@lazy builder.build() } private val gson: Gson = GsonBuilder().create() @@ -84,28 +71,30 @@ class GroovyConsoleHttpService(project: Project) : Disposable { ) private fun execute(config: AemServerHttpConfig, script: ByteArray, action: Action): GroovyConsoleOutput { - val uri = URIBuilder().apply { - val configHostUri = URI.create(config.url) + val uri = config.url.toHttpUrl().newBuilder().encodedPath(action.path).build() - scheme = configHostUri.scheme - host = configHostUri.host - port = configHostUri.port + val authorizationHeader = getAuthorizationHeader(config) - path = action.path - }.build() + val formBody = FormBody.Builder() + .add("script", script.decodeToString()) + .build() - val request = HttpPost(uri).apply { - addHeader(getAuthorizationHeader(config)) + val request: Request = Request.Builder() + .url(uri) + .addHeader(authorizationHeader.first, authorizationHeader.second) + .post(formBody) + .build() - entity = MultipartEntityBuilder.create().addBinaryBody("script", script).build() + return httpClient.newCall(request).execute().use { response -> + return@use handleResponse(response) } - - return httpClient.execute(request, ::handleResponse) } - private fun handleResponse(response: ClassicHttpResponse): GroovyConsoleOutput { - if (HttpStatus.SC_OK == response.code) { - val output = InputStreamReader(response.entity.content).use { + private fun handleResponse(response: Response): GroovyConsoleOutput { + val body = response.body + + if (response.isSuccessful && body != null) { + val output = InputStreamReader(body.byteStream()).use { return@use gson.fromJson(it, Output::class.java) } @@ -132,21 +121,20 @@ class GroovyConsoleHttpService(project: Project) : Disposable { table = outputTable?.table ) } else { - throw HttpException("${response.code} ${response.reasonPhrase}") + throw IOException("${response.code} ${response.message}") } } - private fun getAuthorizationHeader(config: AemServerHttpConfig): Header { + private fun getAuthorizationHeader(config: AemServerHttpConfig): Pair { val value = when (config.authType) { - AuthType.BASIC -> "Basic " + String(Base64.encodeBase64("${config.credentials.user}:${config.credentials.password}".toByteArray())) + AuthType.BASIC -> "Basic " + String( + Base64.getEncoder().encode("${config.credentials.user}:${config.credentials.password}".toByteArray()) + ) + else -> "Bearer " + imsTokenProvider.getAccessToken(config) } - return BasicHeader("Authorization", value) - } - - override fun dispose() { - httpClient.close() + return Pair("Authorization", value) } companion object { diff --git a/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/utils/ApplicationActions.kt b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/utils/ApplicationActions.kt new file mode 100644 index 0000000..7489b68 --- /dev/null +++ b/src/main/kotlin/com/github/bobi/aemgroovyconsoleplugin/utils/ApplicationActions.kt @@ -0,0 +1,21 @@ +package com.github.bobi.aemgroovyconsoleplugin.utils + +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.ModalityState +import com.intellij.openapi.application.ex.ApplicationManagerEx + +fun invokeAndWaitIfNeeded(modalityState: ModalityState? = null, runnable: () -> T): T { + val app = ApplicationManager.getApplication() + if (app.isDispatchThread) { + return runnable() + } else { + var resultRef: T? = null + app.invokeAndWait({ resultRef = runnable() }, modalityState ?: ModalityState.defaultModalityState()) + @Suppress("UNCHECKED_CAST") + return resultRef as T + } +} + +fun isInternal(): Boolean { + return java.lang.Boolean.getBoolean(ApplicationManagerEx.IS_INTERNAL_PROPERTY) +}