diff --git a/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt b/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt index 0788f7708..3fc4b9839 100644 --- a/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt +++ b/common-api/src/main/kotlin/com/itangcent/http/ApacheHttpClient.kt @@ -52,11 +52,8 @@ open class ApacheHttpClient : HttpClient { private val httpClient: org.apache.http.client.HttpClient - constructor() { - val basicCookieStore = BasicCookieStore() - this.apacheCookieStore = ApacheCookieStore(basicCookieStore) - this.httpClientContext!!.cookieStore = basicCookieStore - this.httpClient = HttpClients.custom() + constructor() : this( + HttpClients.custom() .setConnectionManager(PoolingHttpClientConnectionManager().also { it.maxTotal = 50 it.defaultMaxPerRoute = 20 @@ -73,10 +70,8 @@ open class ApacheHttpClient : HttpClient { .setSocketTimeout(30 * 1000) .setCookieSpec(CookieSpecs.STANDARD).build() ) - .setSSLHostnameVerifier(NOOP_HOST_NAME_VERIFIER) - .setSSLSocketFactory(SSLSF) .build() - } + ) constructor(httpClient: org.apache.http.client.HttpClient) { val basicCookieStore = BasicCookieStore() @@ -133,7 +128,7 @@ open class ApacheHttpClient : HttpClient { if (param.type() == "file") { val filePath = param.value() if (filePath.isNullOrBlank()) { - continue + throw FileNotFoundException("file not found") } val file = File(filePath) if (!file.exists() || !file.isFile) { @@ -187,13 +182,7 @@ open class ApacheHttpClient : HttpClient { } @ScriptTypeName("request") -class ApacheHttpRequest : AbstractHttpRequest { - - private val apacheHttpClient: ApacheHttpClient - - constructor(apacheHttpClient: ApacheHttpClient) : super() { - this.apacheHttpClient = apacheHttpClient - } +class ApacheHttpRequest(private val apacheHttpClient: ApacheHttpClient) : AbstractHttpRequest() { /** * Executes HTTP request using the [apacheHttpClient]. @@ -214,13 +203,7 @@ fun HttpRequest.contentType(contentType: ContentType): HttpRequest { * The implement of [CookieStore] by [org.apache.http.client.CookieStore]. */ @ScriptTypeName("cookieStore") -class ApacheCookieStore : CookieStore { - - private var cookieStore: org.apache.http.client.CookieStore - - constructor(cookieStore: org.apache.http.client.CookieStore) { - this.cookieStore = cookieStore - } +class ApacheCookieStore(private var cookieStore: org.apache.http.client.CookieStore) : CookieStore { /** * Adds an [Cookie], replacing any existing equivalent cookies. @@ -281,7 +264,7 @@ class ApacheHttpResponse( * * @return the status of the response, or {@code null} if not yet set */ - override fun code(): Int? { + override fun code(): Int { val statusLine = response.statusLine return statusLine.statusCode } @@ -314,9 +297,11 @@ class ApacheHttpResponse( } /** - * Cache the bytes message of this response. + * the bytes message of this response. */ - private var bytes: ByteArray? = null + private val bodyBytes: ByteArray by lazy { + response.entity.toByteArray() + } /** * Obtains the bytes message of this response. @@ -324,17 +309,8 @@ class ApacheHttpResponse( * @return the response bytes, or * {@code null} if there is none */ - override fun bytes(): ByteArray? { - if (bytes == null) { - synchronized(this) - { - if (bytes == null) { - val entity = response.entity - bytes = entity.toByteArray() - } - } - } - return bytes!! + override fun bytes(): ByteArray { + return bodyBytes } /** @@ -353,17 +329,12 @@ class ApacheHttpResponse( * The implement of [Cookie] by [org.apache.http.cookie.Cookie]. */ @ScriptTypeName("cookie") -class ApacheCookie : Cookie { - private val cookie: org.apache.http.cookie.Cookie +class ApacheCookie(private val cookie: org.apache.http.cookie.Cookie) : Cookie { fun getWrapper(): org.apache.http.cookie.Cookie { return cookie } - constructor(cookie: org.apache.http.cookie.Cookie) { - this.cookie = cookie - } - override fun getName(): String? { return cookie.name } @@ -404,7 +375,7 @@ class ApacheCookie : Cookie { return cookie.isSecure } - override fun getVersion(): Int? { + override fun getVersion(): Int { return cookie.version } diff --git a/common-api/src/main/kotlin/com/itangcent/http/HttpClient.kt b/common-api/src/main/kotlin/com/itangcent/http/HttpClient.kt index aad57690f..f8d75dc93 100644 --- a/common-api/src/main/kotlin/com/itangcent/http/HttpClient.kt +++ b/common-api/src/main/kotlin/com/itangcent/http/HttpClient.kt @@ -3,9 +3,15 @@ package com.itangcent.http import com.itangcent.annotation.script.ScriptTypeName import com.itangcent.common.constant.HttpMethod +/** + * Defines an interface for an HTTP client capable of creating various types of HTTP requests. + */ @ScriptTypeName("httpClient") interface HttpClient { + /** + * Returns a CookieStore to manage cookies for HTTP transactions. + */ fun cookieStore(): CookieStore /** @@ -124,6 +130,5 @@ interface HttpClient { fun head(url: String): HttpRequest { return request().method(HttpMethod.HEAD).url(url) } - } diff --git a/common-api/src/main/kotlin/com/itangcent/http/HttpRequest.kt b/common-api/src/main/kotlin/com/itangcent/http/HttpRequest.kt index d3bc85e74..6156cb791 100644 --- a/common-api/src/main/kotlin/com/itangcent/http/HttpRequest.kt +++ b/common-api/src/main/kotlin/com/itangcent/http/HttpRequest.kt @@ -82,14 +82,20 @@ interface Cookie { * {@code null} if no such comment has been defined. * Compatible only.Obsolete. * @return comment + * + * @deprecated it is only supported by Apache HttpClient */ + @Deprecated("Obsolete") fun getComment(): String? /** * If a user agent (web browser) presents this cookie to a user, the * cookie's purpose will be described by the information at this URL. * Compatible only.Obsolete. + * + * @deprecated it is only supported by Apache HttpClient */ + @Deprecated("Obsolete") fun getCommentURL(): String? /** @@ -129,7 +135,10 @@ interface Cookie { /** * Get the Port attribute. It restricts the ports to which a cookie * may be returned in a Cookie request header. + * + * @deprecated it is only supported by Apache HttpClient */ + @Deprecated("Obsolete") fun getPorts(): IntArray? /** @@ -146,10 +155,17 @@ interface Cookie { * Compatible only.Obsolete. * * @return the version of the cookie. + * @deprecated it is only supported by Apache HttpClient */ + @Deprecated("Obsolete") fun getVersion(): Int? } +fun Cookie.isExpired(): Boolean { + val expiryDate = this.getExpiryDate() + return expiryDate != null && expiryDate < System.currentTimeMillis() +} + @ScriptTypeName("cookie") interface MutableCookie : Cookie { @@ -157,8 +173,16 @@ interface MutableCookie : Cookie { fun setValue(value: String?) + /** + * @deprecated it is only supported by Apache HttpClient + */ + @Deprecated("Obsolete") fun setComment(comment: String?) + /** + * @deprecated it is only supported by Apache HttpClient + */ + @Deprecated("Obsolete") fun setCommentURL(commentURL: String?) /** @@ -193,7 +217,10 @@ interface MutableCookie : Cookie { * Sets the Port attribute. It restricts the ports to which a cookie * may be returned in a Cookie request header. * Compatible only.Obsolete. + * + * @deprecated it is only supported by Apache HttpClient */ + @Deprecated("Obsolete") fun setPorts(ports: IntArray?) /** @@ -218,7 +245,10 @@ interface MutableCookie : Cookie { * @param version the version of the cookie. * * @see Cookie.getVersion + * + * @deprecated it is only supported by Apache HttpClient */ + @Deprecated("Obsolete") fun setVersion(version: Int?) } @@ -1239,5 +1269,4 @@ class BasicHttpParam : HttpParam { fun setType(type: String?) { this.type = type } - } \ No newline at end of file diff --git a/common-api/src/test/kotlin/com/itangcent/http/ApacheHttpClientTest.kt b/common-api/src/test/kotlin/com/itangcent/http/ApacheHttpClientTest.kt index 634014b48..f527a7ef0 100644 --- a/common-api/src/test/kotlin/com/itangcent/http/ApacheHttpClientTest.kt +++ b/common-api/src/test/kotlin/com/itangcent/http/ApacheHttpClientTest.kt @@ -350,309 +350,309 @@ class ApacheHttpClientTest { //skip test if connect timed out } } - - open class AbstractCallTest { - - protected lateinit var httpClient: org.apache.http.client.HttpClient - protected lateinit var httpResponse: HttpResponse - protected lateinit var httpEntity: HttpEntity - - protected var responseCode: Int = 200 - protected lateinit var responseBody: String - protected lateinit var responseHeaders: Array> - protected lateinit var responseCharset: Charset - - protected lateinit var httpUriRequest: HttpUriRequest - protected var closed: Boolean = false - - @BeforeEach - fun setUp() { - //by default - responseCode = 200 - responseBody = "{}" - responseHeaders = arrayOf() - responseCharset = Charsets.UTF_8 - closed = false - - httpClient = mock() - httpResponse = mock(extraInterfaces = arrayOf(Closeable::class)) - httpEntity = mock() - - httpClient.stub { - this.on(httpClient.execute(any(), any())) - .doAnswer { - httpUriRequest = it.getArgument(0) - httpResponse - } - } - (httpResponse as Closeable).stub { - this.on((httpResponse as Closeable).close()) - .doAnswer { - closed = true - } - } - httpResponse.stub { - this.on(httpResponse.statusLine) - .doAnswer { BasicStatusLine(HttpVersion.HTTP_1_0, responseCode, "") } - this.on(httpResponse.entity) - .thenReturn(httpEntity) - this.on(httpResponse.allHeaders) - .doAnswer { - responseHeaders.mapToTypedArray { - org.apache.http.message.BasicHeader( - it.first, - it.second - ) - } +} + +open class AbstractCallTest { + + protected lateinit var httpClient: org.apache.http.client.HttpClient + protected lateinit var httpResponse: HttpResponse + protected lateinit var httpEntity: HttpEntity + + protected var responseCode: Int = 200 + protected lateinit var responseBody: String + protected lateinit var responseHeaders: Array> + protected lateinit var responseCharset: Charset + + protected lateinit var httpUriRequest: HttpUriRequest + protected var closed: Boolean = false + + @BeforeEach + fun setUp() { + //by default + responseCode = 200 + responseBody = "{}" + responseHeaders = arrayOf() + responseCharset = Charsets.UTF_8 + closed = false + + httpClient = mock() + httpResponse = mock(extraInterfaces = arrayOf(Closeable::class)) + httpEntity = mock() + + httpClient.stub { + this.on(httpClient.execute(any(), any())) + .doAnswer { + httpUriRequest = it.getArgument(0) + httpResponse + } + } + (httpResponse as Closeable).stub { + this.on((httpResponse as Closeable).close()) + .doAnswer { + closed = true + } + } + httpResponse.stub { + this.on(httpResponse.statusLine) + .doAnswer { BasicStatusLine(HttpVersion.HTTP_1_0, responseCode, "") } + this.on(httpResponse.entity) + .thenReturn(httpEntity) + this.on(httpResponse.allHeaders) + .doAnswer { + responseHeaders.mapToTypedArray { + org.apache.http.message.BasicHeader( + it.first, + it.second + ) } - } - httpEntity.stub { - this.on(httpEntity.content) - .doAnswer { responseBody.byteInputStream(responseCharset) } - } + } + } + httpEntity.stub { + this.on(httpEntity.content) + .doAnswer { responseBody.byteInputStream(responseCharset) } } } +} - open class CallTest : AbstractCallTest() { +open class CallTest : AbstractCallTest() { - @Test - fun testCallPostJson() { - responseCode = 200 - responseBody = "ok" - responseHeaders = arrayOf( - "Content-type" to "application/json;charset=UTF-8", - "x-token" to "123", "x-token" to "987", - "Content-Disposition" to "attachment; filename=\"test.json\"" - ) + @Test + fun testCallPostJson() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type" to "application/json;charset=UTF-8", + "x-token" to "123", "x-token" to "987", + "Content-Disposition" to "attachment; filename=\"test.json\"" + ) + + val httpClient = ApacheHttpClient(this.httpClient) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .param("hello", "hello") + .contentType("application/json") + .body("hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertTrue(httpUriRequest is HttpEntityEnclosingRequest) + val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) + assertTrue(httpUriRequest.entity is StringEntity) + + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertEquals("test.json", httpResponse.getHeaderFileName()) + httpResponse.close() + assertTrue(closed) + } - val httpClient = ApacheHttpClient(this.httpClient) - val httpRequest = httpClient - .post("https://www.apache.org/licenses/LICENSE-2.0") - .param("hello", "hello") - .contentType("application/json") - .body("hello") - val httpResponse = httpRequest - .call() - assertSame(httpRequest, httpResponse.request()) - - assertTrue(httpUriRequest is HttpEntityEnclosingRequest) - val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) - assertTrue(httpUriRequest.entity is StringEntity) - - assertEquals(200, httpResponse.code()) - assertEquals("ok", httpResponse.string()) - assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) - - assertEquals(true, httpResponse.containsHeader("Content-type")) - assertEquals(false, httpResponse.containsHeader("y-token")) - assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) - assertEquals("123", httpResponse.firstHeader("x-token")) - assertEquals("987", httpResponse.lastHeader("x-token")) - assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) - assertEquals("test.json", httpResponse.getHeaderFileName()) - httpResponse.close() - assertTrue(closed) - } + @Test + fun testCallPostFormData() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type" to "application/json;charset=UTF-8", + "x-token" to "123", "x-token" to "987", + "Content-Disposition" to "attachment; filename=\"\"" + ) + + val httpClient = ApacheHttpClient(this.httpClient) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.MULTIPART_FORM_DATA) + .param("hello", "hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertTrue(httpUriRequest is HttpEntityEnclosingRequest) + val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) + assertEquals("org.apache.http.entity.mime.MultipartFormEntity", httpUriRequest.entity::class.qualifiedName) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertNull(httpResponse.getHeaderFileName()) + httpResponse.close() + assertTrue(closed) + } - @Test - fun testCallPostFormData() { - responseCode = 200 - responseBody = "ok" - responseHeaders = arrayOf( - "Content-type" to "application/json;charset=UTF-8", - "x-token" to "123", "x-token" to "987", - "Content-Disposition" to "attachment; filename=\"\"" - ) + @Test + fun testCallPostUrlencoded() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type" to "application/json;charset=UTF-8", + "x-token" to "123", "x-token" to "987" + ) + + val httpClient = ApacheHttpClient(this.httpClient) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.APPLICATION_FORM_URLENCODED) + .param("hello", "hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertTrue(httpUriRequest is HttpEntityEnclosingRequest) + val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) + assertTrue(httpUriRequest.entity is UrlEncodedFormEntity) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertNull(httpResponse.getHeaderFileName()) + httpResponse.close() + assertTrue(closed) + } + + @Test + fun testCallPostBodyOverForm() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type" to "application/json", + "x-token" to "123", "x-token" to "987", + "Content-Disposition" to "attachment; filename=\"test.json\"" + ) + + val httpClient = ApacheHttpClient(this.httpClient) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.MULTIPART_FORM_DATA) + .param("hello", "hello") + .body("hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertTrue(httpUriRequest is HttpEntityEnclosingRequest) + val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) + assertTrue(httpUriRequest.entity is StringEntity) + + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertEquals("test.json", httpResponse.getHeaderFileName()) + httpResponse.close() + assertTrue(closed) + } + + @Test + fun testUrlWithOutQuestionMark() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf("Content-type" to "application/json;charset=UTF-8") + + val httpClient = ApacheHttpClient(this.httpClient) + val httpRequest = httpClient + .get("https://www.apache.org/licenses/LICENSE-2.0") + .query("x", "1") + .query("y", "2") + .contentType("application/json") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertFalse(httpUriRequest is HttpEntityEnclosingRequest) + assertEquals("https://www.apache.org/licenses/LICENSE-2.0?x=1&y=2", httpUriRequest.uri.toString()) + httpResponse.close() + assertTrue(closed) + } - val httpClient = ApacheHttpClient(this.httpClient) - val httpRequest = httpClient + @Test + fun testUrlWithQuestionMark() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf("Content-type" to "application/json;charset=UTF-8") + + val httpClient = ApacheHttpClient(this.httpClient) + val httpRequest = httpClient + .get("https://www.apache.org/licenses/LICENSE-2.0?x=1") + .query("y", "2") + .contentType("application/json") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertFalse(httpUriRequest is HttpEntityEnclosingRequest) + assertEquals("https://www.apache.org/licenses/LICENSE-2.0?x=1&y=2", httpUriRequest.uri.toString()) + httpResponse.close() + assertTrue(closed) + } +} + +class PostFileTest : AbstractCallTest() { + + @JvmField + @TempDir + var tempDir: Path? = null + + @Test + fun testCallPostFileFormData() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf("Content-type" to "application/json;charset=UTF-8") + + assertThrows { + ApacheHttpClient(this.httpClient) .post("https://www.apache.org/licenses/LICENSE-2.0") .contentType(ContentType.MULTIPART_FORM_DATA) .param("hello", "hello") - val httpResponse = httpRequest + .fileParam("file", "${tempDir}/a.txt") .call() - assertSame(httpRequest, httpResponse.request()) - assertEquals(200, httpResponse.code()) - assertEquals("ok", httpResponse.string()) - assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) - - assertTrue(httpUriRequest is HttpEntityEnclosingRequest) - val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) - assertEquals("org.apache.http.entity.mime.MultipartFormEntity", httpUriRequest.entity::class.qualifiedName) - - assertEquals(true, httpResponse.containsHeader("Content-type")) - assertEquals(false, httpResponse.containsHeader("y-token")) - assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) - assertEquals("123", httpResponse.firstHeader("x-token")) - assertEquals("987", httpResponse.lastHeader("x-token")) - assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) - assertNull(httpResponse.getHeaderFileName()) - httpResponse.close() - assertTrue(closed) } - @Test - fun testCallPostUrlencoded() { - responseCode = 200 - responseBody = "ok" - responseHeaders = arrayOf( - "Content-type" to "application/json;charset=UTF-8", - "x-token" to "123", "x-token" to "987" - ) - - val httpClient = ApacheHttpClient(this.httpClient) - val httpRequest = httpClient + FileUtils.forceMkdir(File("${tempDir}/a")) + assertThrows { + ApacheHttpClient(this.httpClient) .post("https://www.apache.org/licenses/LICENSE-2.0") - .contentType(ContentType.APPLICATION_FORM_URLENCODED) + .contentType(ContentType.MULTIPART_FORM_DATA) .param("hello", "hello") - val httpResponse = httpRequest + .fileParam("file", "${tempDir}/a") .call() - assertSame(httpRequest, httpResponse.request()) - assertEquals(200, httpResponse.code()) - assertEquals("ok", httpResponse.string()) - assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) - - assertTrue(httpUriRequest is HttpEntityEnclosingRequest) - val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) - assertTrue(httpUriRequest.entity is UrlEncodedFormEntity) - - assertEquals(true, httpResponse.containsHeader("Content-type")) - assertEquals(false, httpResponse.containsHeader("y-token")) - assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) - assertEquals("123", httpResponse.firstHeader("x-token")) - assertEquals("987", httpResponse.lastHeader("x-token")) - assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) - assertNull(httpResponse.getHeaderFileName()) - httpResponse.close() - assertTrue(closed) } - @Test - fun testCallPostBodyOverForm() { - responseCode = 200 - responseBody = "ok" - responseHeaders = arrayOf( - "Content-type" to "application/json", - "x-token" to "123", "x-token" to "987", - "Content-Disposition" to "attachment; filename=\"test.json\"" - ) - - val httpClient = ApacheHttpClient(this.httpClient) - val httpRequest = httpClient + val txtFile = File("${tempDir}/a/a.txt") + FileUtils.forceMkdirParent(txtFile) + FileUtils.write(txtFile, "abc") + assertThrows { + ApacheHttpClient(this.httpClient) .post("https://www.apache.org/licenses/LICENSE-2.0") .contentType(ContentType.MULTIPART_FORM_DATA) .param("hello", "hello") - .body("hello") - val httpResponse = httpRequest + .fileParam("file", "${tempDir}/a/a.txt") + .fileParam("file", null) .call() - assertSame(httpRequest, httpResponse.request()) - - assertTrue(httpUriRequest is HttpEntityEnclosingRequest) - val httpUriRequest = (this.httpUriRequest as HttpEntityEnclosingRequest) - assertTrue(httpUriRequest.entity is StringEntity) - - assertEquals(200, httpResponse.code()) - assertEquals("ok", httpResponse.string()) - assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) - - assertEquals(true, httpResponse.containsHeader("Content-type")) - assertEquals(false, httpResponse.containsHeader("y-token")) - assertEquals("application/json", httpResponse.contentType()) - assertEquals("123", httpResponse.firstHeader("x-token")) - assertEquals("987", httpResponse.lastHeader("x-token")) - assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) - assertEquals("test.json", httpResponse.getHeaderFileName()) - httpResponse.close() - assertTrue(closed) - } - - @Test - fun testUrlWithOutQuestionMark() { - responseCode = 200 - responseBody = "ok" - responseHeaders = arrayOf("Content-type" to "application/json;charset=UTF-8") - - val httpClient = ApacheHttpClient(this.httpClient) - val httpRequest = httpClient - .get("https://www.apache.org/licenses/LICENSE-2.0") - .query("x", "1") - .query("y", "2") - .contentType("application/json") - val httpResponse = httpRequest - .call() - assertSame(httpRequest, httpResponse.request()) - - assertFalse(httpUriRequest is HttpEntityEnclosingRequest) - assertEquals("https://www.apache.org/licenses/LICENSE-2.0?x=1&y=2", httpUriRequest.uri.toString()) - httpResponse.close() - assertTrue(closed) - } - - @Test - fun testUrlWithQuestionMark() { - responseCode = 200 - responseBody = "ok" - responseHeaders = arrayOf("Content-type" to "application/json;charset=UTF-8") - - val httpClient = ApacheHttpClient(this.httpClient) - val httpRequest = httpClient - .get("https://www.apache.org/licenses/LICENSE-2.0?x=1") - .query("y", "2") - .contentType("application/json") - val httpResponse = httpRequest - .call() - assertSame(httpRequest, httpResponse.request()) - - assertFalse(httpUriRequest is HttpEntityEnclosingRequest) - assertEquals("https://www.apache.org/licenses/LICENSE-2.0?x=1&y=2", httpUriRequest.uri.toString()) - httpResponse.close() - assertTrue(closed) - } - } - - class PostFileTest : AbstractCallTest() { - - @JvmField - @TempDir - var tempDir: Path? = null - - @Test - fun testCallPostFileFormData() { - responseCode = 200 - responseBody = "ok" - responseHeaders = arrayOf("Content-type" to "application/json;charset=UTF-8") - - assertThrows { - ApacheHttpClient(this.httpClient) - .post("https://www.apache.org/licenses/LICENSE-2.0") - .contentType(ContentType.MULTIPART_FORM_DATA) - .param("hello", "hello") - .fileParam("file", "${tempDir}/a.txt") - .call() - } - - FileUtils.forceMkdir(File("${tempDir}/a")) - assertThrows { - ApacheHttpClient(this.httpClient) - .post("https://www.apache.org/licenses/LICENSE-2.0") - .contentType(ContentType.MULTIPART_FORM_DATA) - .param("hello", "hello") - .fileParam("file", "${tempDir}/a") - .call() - } - - val txtFile = File("${tempDir}/a/a.txt") - FileUtils.forceMkdirParent(txtFile) - FileUtils.write(txtFile, "abc") - assertDoesNotThrow { - ApacheHttpClient(this.httpClient) - .post("https://www.apache.org/licenses/LICENSE-2.0") - .contentType(ContentType.MULTIPART_FORM_DATA) - .param("hello", "hello") - .fileParam("file", "${tempDir}/a/a.txt") - .fileParam("file", null) - .call() - } } } } \ No newline at end of file diff --git a/idea-plugin/build.gradle.kts b/idea-plugin/build.gradle.kts index b99dfe1f9..21bc1d88b 100644 --- a/idea-plugin/build.gradle.kts +++ b/idea-plugin/build.gradle.kts @@ -78,6 +78,10 @@ dependencies { // https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc implementation("org.xerial:sqlite-jdbc:3.34.0") + // https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + // https://search.maven.org/artifact/org.mockito.kotlin/mockito-kotlin/3.2.0/jar testImplementation("org.mockito.kotlin:mockito-kotlin:3.2.0") diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiCallAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiCallAction.kt index 47b8ddea3..f7dd68dc1 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiCallAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiCallAction.kt @@ -12,8 +12,6 @@ import com.itangcent.intellij.extend.guice.singleton import com.itangcent.intellij.extend.guice.with import com.itangcent.intellij.file.DefaultLocalFileRepository import com.itangcent.intellij.file.LocalFileRepository -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.suv.http.HttpClientProvider class ApiCallAction : ApiExportAction("Call Api") { @@ -31,8 +29,6 @@ class ApiCallAction : ApiExportAction("Call Api") { builder.bind(ClassExporter::class) { it.with(CachedRequestClassExporter::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } - builder.bind(ApiCaller::class) { it.singleton() } } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiDashBoardAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiDashBoardAction.kt index b8cb7aa7d..5d6b31818 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiDashBoardAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/ApiDashBoardAction.kt @@ -19,8 +19,6 @@ import com.itangcent.intellij.extend.guice.singleton import com.itangcent.intellij.extend.guice.with import com.itangcent.intellij.file.DefaultLocalFileRepository import com.itangcent.intellij.file.LocalFileRepository -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.suv.http.HttpClientProvider class ApiDashBoardAction : ApiExportAction("ApiDashBoard") { @@ -38,7 +36,6 @@ class ApiDashBoardAction : ApiExportAction("ApiDashBoard") { builder.bindInstance(ExportDoc::class, ExportDoc.of("request")) builder.bind(ClassExporter::class) { it.with(CachedRequestClassExporter::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(RequestBuilderListener::class) { it.with(CompositeRequestBuilderListener::class).singleton() } builder.bind(ActiveWindowProvider::class) { it.with(SimpleActiveWindowProvider::class) } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/BasicAnAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/BasicAnAction.kt index 318e06957..58a37be2a 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/BasicAnAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/BasicAnAction.kt @@ -43,8 +43,6 @@ abstract class BasicAnAction : KotlinAnAction { builder.bind(Logger::class, "delegate.logger") { it.with(ConsoleRunnerLogger::class).singleton() } builder.bind(ResourceResolver::class) { it.with(CachedResourceResolver::class).singleton() } - DisableDocSupport.bind(builder) - afterBuildActionContext(event, builder) } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt index bbd895c52..18bbce914 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/PostmanExportAction.kt @@ -14,8 +14,6 @@ import com.itangcent.intellij.extend.guice.singleton import com.itangcent.intellij.extend.guice.with import com.itangcent.intellij.file.DefaultLocalFileRepository import com.itangcent.intellij.file.LocalFileRepository -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.suv.http.HttpClientProvider class PostmanExportAction : ApiExportAction("Export Postman") { @@ -27,7 +25,6 @@ class PostmanExportAction : ApiExportAction("Export Postman") { builder.bind(FormatFolderHelper::class) { it.with(PostmanFormatFolderHelper::class).singleton() } builder.bind(PostmanApiHelper::class) { it.with(PostmanCachedApiHelper::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(ClassExporter::class) { it.with(CompositeClassExporter::class).singleton() } builder.bindInstance(ExportChannel::class, ExportChannel.of("postman")) diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt index da7e6a7da..91d408e9e 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiDashBoardAction.kt @@ -15,8 +15,6 @@ import com.itangcent.intellij.extend.guice.singleton import com.itangcent.intellij.extend.guice.with import com.itangcent.intellij.file.DefaultLocalFileRepository import com.itangcent.intellij.file.LocalFileRepository -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.suv.http.HttpClientProvider class YapiDashBoardAction : ApiExportAction("YapiDashBoard") { @@ -28,8 +26,7 @@ class YapiDashBoardAction : ApiExportAction("YapiDashBoard") { builder.bind(YapiDashBoard::class) { it.singleton() } builder.bind(YapiApiDashBoardExporter::class) { it.singleton() } - builder.bind(YapiApiHelper::class) { it.with(YapiCachedApiHelper::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } + builder.bind(YapiApiHelper::class) { it.with(CachedYapiApiHelper::class).singleton() } //allow cache api builder.bind(ClassExporter::class, "delegate_classExporter") { diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt index 266a2e318..c0eca13b3 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/actions/YapiExportAction.kt @@ -13,8 +13,6 @@ import com.itangcent.intellij.extend.guice.with import com.itangcent.intellij.file.DefaultLocalFileRepository import com.itangcent.intellij.file.LocalFileRepository import com.itangcent.intellij.jvm.PsiClassHelper -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.suv.http.HttpClientProvider class YapiExportAction : ApiExportAction("Export Yapi") { @@ -23,10 +21,9 @@ class YapiExportAction : ApiExportAction("Export Yapi") { builder.bind(LocalFileRepository::class) { it.with(DefaultLocalFileRepository::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(LinkResolver::class) { it.with(YapiLinkResolver::class).singleton() } - builder.bind(YapiApiHelper::class) { it.with(YapiCachedApiHelper::class).singleton() } + builder.bind(YapiApiHelper::class) { it.with(CachedYapiApiHelper::class).singleton() } builder.bind(ClassExporter::class) { it.with(CompositeClassExporter::class).singleton() } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt index 3058f8891..4b7d21d1b 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/suv/SuvApiExporter.kt @@ -51,8 +51,6 @@ import com.itangcent.intellij.jvm.PsiClassHelper import com.itangcent.intellij.logger.Logger import com.itangcent.intellij.tip.TipsHelper import com.itangcent.intellij.util.UIUtils -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.suv.http.HttpClientProvider import java.util.* import kotlin.reflect.KClass import kotlin.reflect.full.createInstance @@ -364,7 +362,6 @@ open class SuvApiExporter { builder.bind(LocalFileRepository::class) { it.with(DefaultLocalFileRepository::class).singleton() } builder.bind(PostmanApiHelper::class) { it.with(PostmanCachedApiHelper::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(FormatFolderHelper::class) { it.with(PostmanFormatFolderHelper::class).singleton() } @@ -407,9 +404,8 @@ open class SuvApiExporter { builder.bind(LocalFileRepository::class) { it.with(DefaultLocalFileRepository::class).singleton() } - builder.bind(YapiApiHelper::class) { it.with(YapiCachedApiHelper::class).singleton() } + builder.bind(YapiApiHelper::class) { it.with(CachedYapiApiHelper::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(LinkResolver::class) { it.with(YapiLinkResolver::class).singleton() } builder.bind(ClassExporter::class) { it.with(CompositeClassExporter::class).singleton() } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiCachedApiHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/CachedYapiApiHelper.kt similarity index 97% rename from idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiCachedApiHelper.kt rename to idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/CachedYapiApiHelper.kt index 1a9647c7d..f5e57f95c 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/YapiCachedApiHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/api/export/yapi/CachedYapiApiHelper.kt @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap * cache: * projectToken -> projectId */ -open class YapiCachedApiHelper : DefaultYapiApiHelper() { +open class CachedYapiApiHelper : DefaultYapiApiHelper() { @Inject private val localFileRepository: LocalFileRepository? = null diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/configurable/AbstractEasyApiConfigurable.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/configurable/AbstractEasyApiConfigurable.kt index 0828b112b..8d03adcb9 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/configurable/AbstractEasyApiConfigurable.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/configurable/AbstractEasyApiConfigurable.kt @@ -5,9 +5,7 @@ import com.intellij.openapi.options.SearchableConfigurable import com.intellij.openapi.project.Project import com.itangcent.common.kit.toJson import com.itangcent.common.logger.Log -import com.itangcent.idea.plugin.api.export.core.EasyApiConfigProvider import com.itangcent.idea.plugin.settings.SettingBinder -import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.extend.guice.singleton import com.itangcent.intellij.extend.guice.with @@ -16,8 +14,6 @@ import com.itangcent.intellij.file.DefaultLocalFileRepository import com.itangcent.intellij.file.LocalFileRepository import com.itangcent.intellij.logger.Logger import com.itangcent.intellij.logger.SystemLogger -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.suv.http.HttpClientProvider import javax.swing.JComponent abstract class AbstractEasyApiConfigurable(private var myProject: Project?) : SearchableConfigurable { @@ -58,7 +54,6 @@ abstract class AbstractEasyApiConfigurable(private var myProject: Project?) : Se builder.bindInstance("plugin.name", "easy_api") myProject?.let { builder.bindInstance(Project::class, it) } builder.bind(Logger::class) { it.with(SystemLogger::class).singleton() } - builder.bind(HttpClientProvider::class) { it.with(ConfigurableHttpClientProvider::class).singleton() } builder.bind(LocalFileRepository::class) { it.with(DefaultLocalFileRepository::class).singleton() } afterBuildActionContext(builder) diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form index 65ceacfb6..cdab274e4 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.form @@ -3,7 +3,7 @@ - + @@ -1407,7 +1407,7 @@ - + @@ -1418,7 +1418,7 @@ - + @@ -1452,7 +1452,7 @@ - + @@ -1470,9 +1470,9 @@ - + - + @@ -1496,6 +1496,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt index 91344e7ce..578f802b3 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/dialog/EasyApiSettingGUI.kt @@ -29,8 +29,6 @@ import com.itangcent.idea.utils.isDoubleClick import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.extend.rx.ThrottleHelper import com.itangcent.intellij.logger.Logger -import com.itangcent.suv.http.ConfigurableHttpClientProvider -import com.itangcent.common.utils.ResourceUtils import java.awt.event.MouseAdapter import java.awt.event.MouseEvent import java.awt.event.MouseListener @@ -165,6 +163,10 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { private var recommendedCheckBox: JCheckBox? = null + private var unsafeSslCheckBox: JCheckBox? = null + + private var httpClientComboBox: JComboBox? = null + private var httpTimeOutTextField: JTextField? = null private var trustHostsTextArea: JTextArea? = null @@ -253,6 +255,9 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { markdownFormatTypeComboBox!!.model = DefaultComboBoxModel(MarkdownFormatType.values().mapToTypedArray { it.name }) + httpClientComboBox!!.model = + DefaultComboBoxModel(HttpClientType.values().mapToTypedArray { it.value }) + //endregion general----------------------------------------------------- } @@ -300,6 +305,8 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { this.trustHostsTextArea!!.text = settings.trustHosts.joinToString(separator = "\n") this.maxDeepTextField!!.text = settings.inferMaxDeep.toString() + this.unsafeSslCheckBox!!.isSelected = settings.unsafeSsl + this.httpClientComboBox!!.selectedItem = settings.httpClient this.httpTimeOutTextField!!.text = settings.httpTimeOut.toString() refresh() @@ -606,8 +613,9 @@ class EasyApiSettingGUI : AbstractEasyApiSettingGUI() { settings.yapiExportMode = yapiExportModeComboBox!!.selectedItem as? String ?: YapiExportMode.ALWAYS_UPDATE.name settings.yapiReqBodyJson5 = yapiReqBodyJson5CheckBox!!.isSelected settings.yapiResBodyJson5 = yapiResBodyJson5CheckBox!!.isSelected - settings.httpTimeOut = - httpTimeOutTextField!!.text.toIntOrNull() ?: ConfigurableHttpClientProvider.defaultHttpTimeOut + settings.unsafeSsl = unsafeSslCheckBox!!.isSelected + settings.httpClient = httpClientComboBox!!.selectedItem as? String ?: HttpClientType.APACHE.value + settings.httpTimeOut = httpTimeOutTextField!!.text.toIntOrNull() ?: 10 settings.useRecommendConfig = recommendedCheckBox!!.isSelected settings.logLevel = (logLevelComboBox!!.selected() ?: CommonSettingsHelper.CoarseLogLevel.LOW).getLevel() diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/HttpClientType.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/HttpClientType.kt new file mode 100644 index 000000000..3215f9eec --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/HttpClientType.kt @@ -0,0 +1,12 @@ +package com.itangcent.idea.plugin.settings + +/** + * @author tangcent + * @date 2024/04/28 + */ +enum class HttpClientType( + val value: String +) { + APACHE("Apache"), + OKHTTP("Okhttp"); +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt index ae00b7f4e..7ff7671b5 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/Settings.kt @@ -82,6 +82,10 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { override var trustHosts: Array = DEFAULT_TRUST_HOSTS + override var unsafeSsl: Boolean = false + + override var httpClient: String = "Apache" + //endregion /** @@ -156,6 +160,8 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { if (yapiReqBodyJson5 != other.yapiReqBodyJson5) return false if (yapiResBodyJson5 != other.yapiResBodyJson5) return false if (httpTimeOut != other.httpTimeOut) return false + if (unsafeSsl != other.unsafeSsl) return false + if (httpClient != other.httpClient) return false if (!trustHosts.contentEquals(other.trustHosts)) return false if (useRecommendConfig != other.useRecommendConfig) return false if (recommendConfigs != other.recommendConfigs) return false @@ -201,6 +207,8 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { result = 31 * result + yapiReqBodyJson5.hashCode() result = 31 * result + yapiResBodyJson5.hashCode() result = 31 * result + httpTimeOut + result = 31 * result + unsafeSsl.hashCode() + result = 31 * result + httpClient.hashCode() result = 31 * result + trustHosts.contentHashCode() result = 31 * result + useRecommendConfig.hashCode() result = 31 * result + recommendConfigs.hashCode() @@ -215,7 +223,31 @@ class Settings : ProjectSettingsSupport, ApplicationSettingsSupport { } override fun toString(): String { - return "Settings(methodDocEnable=$methodDocEnable, genericEnable=$genericEnable, feignEnable=$feignEnable, jaxrsEnable=$jaxrsEnable, actuatorEnable=$actuatorEnable, pullNewestDataBefore=$pullNewestDataBefore, postmanToken=$postmanToken, postmanWorkspace=$postmanWorkspace, postmanExportMode=$postmanExportMode, postmanCollections=$postmanCollections, wrapCollection=$wrapCollection, autoMergeScript=$autoMergeScript, postmanJson5FormatType='$postmanJson5FormatType', queryExpanded=$queryExpanded, formExpanded=$formExpanded, readGetter=$readGetter, readSetter=$readSetter, inferEnable=$inferEnable, inferMaxDeep=$inferMaxDeep, selectedOnly=$selectedOnly, yapiServer=$yapiServer, yapiTokens=$yapiTokens, enableUrlTemplating=$enableUrlTemplating, switchNotice=$switchNotice, loginMode=$loginMode, yapiExportMode=$yapiExportMode, yapiReqBodyJson5=$yapiReqBodyJson5, yapiResBodyJson5=$yapiResBodyJson5, httpTimeOut=$httpTimeOut, trustHosts=${trustHosts.contentToString()}, useRecommendConfig=$useRecommendConfig, recommendConfigs='$recommendConfigs', logLevel=$logLevel, logCharset='$logCharset', outputDemo=$outputDemo, outputCharset='$outputCharset', markdownFormatType='$markdownFormatType', builtInConfig=$builtInConfig), remoteConfig=$remoteConfig)" + return "Settings(methodDocEnable=$methodDocEnable, genericEnable=$genericEnable, " + + "feignEnable=$feignEnable, " + + "jaxrsEnable=$jaxrsEnable, actuatorEnable=$actuatorEnable, " + + "pullNewestDataBefore=$pullNewestDataBefore, " + + "postmanToken=$postmanToken, postmanWorkspace=$postmanWorkspace, " + + "postmanExportMode=$postmanExportMode, " + + "postmanCollections=$postmanCollections, postmanBuildExample=$postmanBuildExample, " + + "wrapCollection=$wrapCollection, autoMergeScript=$autoMergeScript, " + + "postmanJson5FormatType='$postmanJson5FormatType', " + + "queryExpanded=$queryExpanded, formExpanded=$formExpanded, " + + "readGetter=$readGetter, readSetter=$readSetter, " + + "inferEnable=$inferEnable, inferMaxDeep=$inferMaxDeep, " + + "selectedOnly=$selectedOnly, yapiServer=$yapiServer, " + + "apiTokens=$yapiTokens, enableUrlTemplating=$enableUrlTemplating, " + + "switchNotice=$switchNotice, loginMode=$loginMode, " + + "yapiExportMode='$yapiExportMode', yapiReqBodyJson5=$yapiReqBodyJson5, " + + "yapiResBodyJson5=$yapiResBodyJson5, " + + "httpTimeOut=$httpTimeOut, unsafeSsl=$unsafeSsl, " + + "httpClient='$httpClient', trustHosts=${trustHosts.contentToString()}," + + "useRecommendConfig=$useRecommendConfig, " + + "recommendConfigs='$recommendConfigs', logLevel=$logLevel, logCharset='$logCharset', " + + "outputDemo=$outputDemo, " + + "outputCharset='$outputCharset', markdownFormatType='$markdownFormatType', " + + "builtInConfig=$builtInConfig, " + + "remoteConfig=${remoteConfig.contentToString()})" } companion object { diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelper.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelperImpl.kt similarity index 73% rename from idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelper.kt rename to idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelperImpl.kt index bfa17711c..9fde3f508 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelper.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/helper/HttpSettingsHelperImpl.kt @@ -1,5 +1,6 @@ package com.itangcent.idea.plugin.settings.helper +import com.google.inject.ImplementedBy import com.google.inject.Inject import com.google.inject.Singleton import com.intellij.openapi.ui.Messages @@ -10,8 +11,18 @@ import com.itangcent.idea.swing.MessagesHelper import java.net.URL import java.util.concurrent.TimeUnit +@ImplementedBy(HttpSettingsHelperImpl::class) +interface HttpSettingsHelper { + fun checkTrustUrl(url: String, dumb: Boolean = true): Boolean + fun checkTrustHost(host: String, dumb: Boolean = true): Boolean + fun addTrustHost(host: String) + fun resolveHost(url: String): String + fun httpTimeOut(timeUnit: TimeUnit): Int + fun unsafeSsl(): Boolean +} + @Singleton -class HttpSettingsHelper { +class HttpSettingsHelperImpl : HttpSettingsHelper { @Inject private lateinit var settingBinder: SettingBinder @@ -21,7 +32,7 @@ class HttpSettingsHelper { //region trustHosts---------------------------------------------------- - fun checkTrustUrl(url: String, dumb: Boolean = true): Boolean { + override fun checkTrustUrl(url: String, dumb: Boolean): Boolean { val trustHosts = settingBinder.read().trustHosts var ret: Boolean? = null for (trustHost in trustHosts) { @@ -39,7 +50,7 @@ class HttpSettingsHelper { return checkTrustHost(resolveHost(url), dumb) } - fun checkTrustHost(host: String, dumb: Boolean = true): Boolean { + override fun checkTrustHost(host: String, dumb: Boolean): Boolean { val settings = settingBinder.read() val trustHosts = settings.trustHosts @@ -48,12 +59,15 @@ class HttpSettingsHelper { return false } if (settings.yapiServer == host - || trustHosts.contains(host)) { + || trustHosts.contains(host) + ) { return true } if (!dumb) { - val trustRet = messagesHelper.showYesNoDialog("Do you trust [$host]?", - "Trust Host", Messages.getQuestionIcon()) + val trustRet = messagesHelper.showYesNoDialog( + "Do you trust [$host]?", + "Trust Host", Messages.getQuestionIcon() + ) return if (trustRet == Messages.YES) { addTrustHost(host) @@ -66,7 +80,7 @@ class HttpSettingsHelper { return false } - fun addTrustHost(host: String) { + override fun addTrustHost(host: String) { settingBinder.update { if (!trustHosts.contains(host)) { trustHosts += host @@ -74,7 +88,7 @@ class HttpSettingsHelper { } } - fun resolveHost(url: String): String { + override fun resolveHost(url: String): String { try { val settings = settingBinder.read() @@ -96,7 +110,7 @@ class HttpSettingsHelper { //endregion trustHosts---------------------------------------------------- - fun httpTimeOut(timeUnit: TimeUnit): Int { + override fun httpTimeOut(timeUnit: TimeUnit): Int { //unit of httpTimeOut is second if (timeUnit == TimeUnit.SECONDS) { return settingBinder.read().httpTimeOut @@ -104,12 +118,18 @@ class HttpSettingsHelper { return timeUnit.convert(settingBinder.read().httpTimeOut.toLong(), TimeUnit.SECONDS).toInt() } + override fun unsafeSsl(): Boolean { + return settingBinder.read().unsafeSsl + } + companion object { val HOST_RESOLVERS: Array<(String) -> String?> = arrayOf({ if (it.startsWith("https://raw.githubusercontent.com")) { val url = if (it.endsWith("/")) it else "$it/" - return@arrayOf RegexUtils.extract("https://raw.githubusercontent.com/(.*?)/.*", - url, "https://raw.githubusercontent.com/$1") + return@arrayOf RegexUtils.extract( + "https://raw.githubusercontent.com/(.*?)/.*", + url, "https://raw.githubusercontent.com/$1" + ) } return@arrayOf null }) diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt index fc572d4c3..e2b4b9f59 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/plugin/settings/xml/ApplicationSettings.kt @@ -38,6 +38,9 @@ interface ApplicationSettingsSupport { //unit:s var httpTimeOut: Int var trustHosts: Array + var unsafeSsl: Boolean + var httpClient: String + //enable to use recommend config var useRecommendConfig: Boolean @@ -77,7 +80,6 @@ interface ApplicationSettingsSupport { newSetting.yapiExportMode = this.yapiExportMode newSetting.yapiReqBodyJson5 = this.yapiReqBodyJson5 newSetting.yapiResBodyJson5 = this.yapiResBodyJson5 - newSetting.httpTimeOut = this.httpTimeOut newSetting.useRecommendConfig = this.useRecommendConfig newSetting.recommendConfigs = this.recommendConfigs newSetting.logLevel = this.logLevel @@ -86,6 +88,9 @@ interface ApplicationSettingsSupport { newSetting.outputCharset = this.outputCharset newSetting.markdownFormatType = this.markdownFormatType newSetting.builtInConfig = this.builtInConfig + newSetting.httpTimeOut = this.httpTimeOut + newSetting.unsafeSsl = this.unsafeSsl + newSetting.httpClient = this.httpClient newSetting.trustHosts = this.trustHosts newSetting.remoteConfig = this.remoteConfig } @@ -158,6 +163,10 @@ class ApplicationSettings : ApplicationSettingsSupport { override var trustHosts: Array = Settings.DEFAULT_TRUST_HOSTS + override var unsafeSsl: Boolean = false + + override var httpClient: String = "Apache" + //endregion //enable to use recommend config diff --git a/idea-plugin/src/main/kotlin/com/itangcent/idea/psi/DisableDocSupport.kt b/idea-plugin/src/main/kotlin/com/itangcent/idea/psi/DisableDocSupport.kt index dfd7cdc00..b80f137b9 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/idea/psi/DisableDocSupport.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/idea/psi/DisableDocSupport.kt @@ -2,6 +2,7 @@ package com.itangcent.idea.psi import com.google.inject.matcher.Matchers import com.itangcent.common.logger.Log +import com.itangcent.common.spi.SetupAble import com.itangcent.common.utils.toBool import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.context.ActionContext @@ -13,17 +14,19 @@ import org.aopalliance.intercept.MethodInvocation * The DisableDocSupport object is responsible for binding the EmptyInterceptor to the ActionContextBuilder. * It provides a way to disable the plugin from reading documentation. */ -object DisableDocSupport { +class DisableDocSupport : SetupAble { /* * Binds the EmptyInterceptor to the ActionContextBuilder, enabling the plugin to intercept method invocations. * @param builder The ActionContextBuilder to bind the interceptor to. */ - fun bind(builder: ActionContext.ActionContextBuilder) { - builder.bindInterceptor( - Matchers.subclassesOf(DocHelper::class.java), - Matchers.any(), - EmptyInterceptor() - ) + override fun init() { + ActionContext.addDefaultInject { builder -> + builder.bindInterceptor( + Matchers.subclassesOf(DocHelper::class.java), + Matchers.any(), + EmptyInterceptor + ) + } } } @@ -31,20 +34,21 @@ object DisableDocSupport { * The EmptyInterceptor class is an interceptor used to disable documentation support. * Use 'doc.source.disable' configuration property to determine if documentation is enabled or disabled. */ -class EmptyInterceptor : MethodInterceptor { - - companion object : Log() +object EmptyInterceptor : MethodInterceptor, Log() { - private val disableDoc by lazy { - val disable = ActionContext.getContext() - ?.instance(ConfigReader::class) - ?.first("doc.source.disable") - ?.toBool(false) ?: false - if (disable) { - LOG.info("disable doc") + private val disableDoc: Boolean + get() { + return ActionContext.getContext()?.cacheOrCompute("disableDoc") { + val disable = ActionContext.getContext() + ?.instance(ConfigReader::class) + ?.first("doc.source.disable") + ?.toBool(false) ?: false + if (disable) { + LOG.info("disable doc") + } + disable + } ?: false } - disable - } override fun invoke(invocation: MethodInvocation): Any? { if (disableDoc) { diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/AbstractHttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/AbstractHttpClientProvider.kt index 938e35bc1..bacb9702a 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/AbstractHttpClientProvider.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/AbstractHttpClientProvider.kt @@ -2,22 +2,28 @@ package com.itangcent.suv.http import com.itangcent.http.HttpClient - +/** + * Abstract base class for creating and providing an HttpClient instance. + * This class implements the HttpClientProvider interface and uses a singleton pattern to + * ensure that a single, shared instance of HttpClient is provided across the entire application. + */ abstract class AbstractHttpClientProvider : HttpClientProvider { - protected var httpClientInstance: HttpClient? = null - - override fun getHttpClient(): HttpClient { - if (httpClientInstance == null) { - synchronized(this) - { - if (httpClientInstance == null) { - httpClientInstance = buildHttpClient() - } - } - } - return httpClientInstance!! + /** + * The HttpClient instance that will be provided by this provider. + * This instance is created lazily and is shared across the entire application. + */ + protected val httpClientInstance: HttpClient by lazy { + buildHttpClient() } + /** + * Retrieve the shared instance of HttpClient. + */ + override fun getHttpClient(): HttpClient = httpClientInstance + + /** + * Create and configure an instance of HttpClient. + */ abstract fun buildHttpClient(): HttpClient } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ApacheHttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ApacheHttpClientProvider.kt new file mode 100644 index 000000000..6e342cb5d --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ApacheHttpClientProvider.kt @@ -0,0 +1,61 @@ +package com.itangcent.suv.http + +import com.google.inject.Inject +import com.itangcent.http.ApacheHttpClient +import com.itangcent.http.HttpClient +import com.itangcent.http.NOOP_HOST_NAME_VERIFIER +import com.itangcent.http.SSLSF +import com.itangcent.idea.plugin.condition.ConditionOnSetting +import com.itangcent.idea.plugin.settings.helper.HttpSettingsHelper +import org.apache.http.client.config.CookieSpecs +import org.apache.http.client.config.RequestConfig +import org.apache.http.config.SocketConfig +import org.apache.http.impl.client.HttpClients +import org.apache.http.impl.conn.PoolingHttpClientConnectionManager +import java.util.concurrent.TimeUnit + +/** + * An implementation of [HttpClient] using Apache HttpClient. + * Provides an Apache HttpClient based on configuration settings. + * + * @author tangcent + * @date 2024/05/08 + */ +@ConditionOnSetting("httpClient", havingValue = "Apache") +open class ApacheHttpClientProvider : AbstractHttpClientProvider() { + + @Inject + protected lateinit var httpSettingsHelper: HttpSettingsHelper + + override fun buildHttpClient(): HttpClient { + var httpClientBuilder = HttpClients.custom() + + // Initialize HttpClient with timeout settings. + val timeOutInMills = httpSettingsHelper.httpTimeOut(TimeUnit.MILLISECONDS) + httpClientBuilder = httpClientBuilder + .setConnectionManager(PoolingHttpClientConnectionManager().also { + it.maxTotal = 50 + it.defaultMaxPerRoute = 20 + }) + .setDefaultSocketConfig( + SocketConfig.custom() + .setSoTimeout(timeOutInMills) + .build() + ) + .setDefaultRequestConfig( + RequestConfig.custom() + .setConnectTimeout(timeOutInMills) + .setConnectionRequestTimeout(timeOutInMills) + .setSocketTimeout(timeOutInMills) + .setCookieSpec(CookieSpecs.STANDARD).build() + ) + + // If unsafe SSL is allowed, configure HttpClient to trust all certificates. + if (httpSettingsHelper.unsafeSsl()) { + httpClientBuilder = httpClientBuilder.setSSLHostnameVerifier(NOOP_HOST_NAME_VERIFIER) + .setSSLSocketFactory(SSLSF) + } + + return ApacheHttpClient(httpClientBuilder.build()) + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/DefaultHttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/DefaultHttpClientProvider.kt index e513c9d2c..74ccebe36 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/DefaultHttpClientProvider.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/DefaultHttpClientProvider.kt @@ -1,14 +1,25 @@ package com.itangcent.suv.http import com.google.inject.Singleton -import com.itangcent.http.ApacheHttpClient import com.itangcent.http.HttpClient +import com.itangcent.intellij.context.ActionContext +import com.itangcent.spi.SpiCompositeLoader +/** + * The default implementation of the [HttpClientProvider] interface + * which automatically loads an implementation of the HttpClientProvider interface using the service provider interface (SPI) mechanism. + */ @Singleton -class DefaultHttpClientProvider : AbstractHttpClientProvider() { +open class DefaultHttpClientProvider : AbstractHttpClientProvider() { + + private val httpClientProvider: HttpClientProvider by lazy { + val actionContext = ActionContext.getContext()!! + SpiCompositeLoader.load(actionContext).firstOrNull() + ?: actionContext.instance(ApacheHttpClientProvider::class) + } override fun buildHttpClient(): HttpClient { - return ApacheHttpClient() + return httpClientProvider.getHttpClient() } } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt index b0d8d93f3..4ea6ac7cb 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientProvider.kt @@ -4,8 +4,20 @@ import com.google.inject.ImplementedBy import com.itangcent.http.HttpClient +/** + * Defines an interface for obtaining an instance of HttpClient. + * + * It is the responsibility of each implementation to determine whether to return + * the same instance of HttpClient consistently or to create a new instance for each request. + * + * @author tangcent + * @date 2024/05/08 + */ @ImplementedBy(DefaultHttpClientProvider::class) interface HttpClientProvider { + /** + * Retrieve an instance of HttpClient, either as a singleton or as a new instance per call. + */ fun getHttpClient(): HttpClient } diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientScriptInterceptor.kt similarity index 68% rename from idea-plugin/src/main/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProvider.kt rename to idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientScriptInterceptor.kt index 94eb2e1f5..fa686a0b6 100644 --- a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProvider.kt +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/HttpClientScriptInterceptor.kt @@ -1,112 +1,96 @@ package com.itangcent.suv.http -import com.google.inject.Inject -import com.google.inject.Singleton +import com.google.inject.matcher.Matchers import com.itangcent.annotation.script.ScriptIgnore import com.itangcent.annotation.script.ScriptTypeName +import com.itangcent.common.spi.SetupAble import com.itangcent.http.* import com.itangcent.idea.plugin.api.export.core.ClassExportRuleKeys import com.itangcent.idea.plugin.rule.SuvRuleContext import com.itangcent.idea.plugin.settings.helper.HttpSettingsHelper -import com.itangcent.intellij.config.ConfigReader import com.itangcent.intellij.config.rule.RuleComputer +import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.logger.Logger -import org.apache.http.client.config.CookieSpecs -import org.apache.http.client.config.RequestConfig -import org.apache.http.config.SocketConfig -import org.apache.http.impl.client.HttpClients -import org.apache.http.impl.conn.PoolingHttpClientConnectionManager +import org.aopalliance.intercept.MethodInterceptor +import org.aopalliance.intercept.MethodInvocation import java.io.ByteArrayInputStream import java.io.InputStream import java.nio.charset.Charset -import java.util.concurrent.TimeUnit /** - * An implementation of the HttpClientProvider interface - * that provides a configurable HttpClient implementation. + * A support class for integrating [HttpClientProvider]s with scripting functionalities. + * This class setups method interceptors on HTTP client providers to augment them with additional + * scripting capabilities, monitoring, and conditional execution of HTTP requests based on script annotations. + * It is responsible for setting up interception of HTTP client methods to apply custom logic before + * or after HTTP requests are executed. + * + * @author tangcent + * @date 2024/05/08 */ -@Singleton -class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { - - @Inject(optional = true) - protected val httpSettingsHelper: HttpSettingsHelper? = null - - @Inject(optional = true) - protected val configReader: ConfigReader? = null - - @Inject(optional = true) - protected val ruleComputer: RuleComputer? = null - - @Inject - protected lateinit var logger: Logger - +class HttpClientScriptInterceptorSupport : SetupAble { /** - * Builds an instance of HttpClient using configuration options such as timeouts and SSL settings. - * - * @return An instance of HttpClient. + * Initializes and adds default injections to intercept [HttpClientProvider] methods, + * applying custom interceptor logic defined in [HttpClientScriptInterceptor]. */ - override fun buildHttpClient(): HttpClient { - val httpClientBuilder = HttpClients.custom() - - val config = readHttpConfig() - - httpClientBuilder - .setConnectionManager(PoolingHttpClientConnectionManager().also { - it.maxTotal = 50 - it.defaultMaxPerRoute = 20 - }) - .setDefaultSocketConfig( - SocketConfig.custom() - .setSoTimeout(config.timeOut) - .build() - ) - .setDefaultRequestConfig( - RequestConfig.custom() - .setConnectTimeout(config.timeOut) - .setConnectionRequestTimeout(config.timeOut) - .setSocketTimeout(config.timeOut) - .setCookieSpec(CookieSpecs.STANDARD).build() + override fun init() { + ActionContext.addDefaultInject { builder -> + builder.bindInterceptor( + Matchers.subclassesOf(HttpClientProvider::class.java), + Matchers.any(), + HttpClientScriptInterceptor ) - .setSSLHostnameVerifier(NOOP_HOST_NAME_VERIFIER) - .setSSLSocketFactory(SSLSF) - - return HttpClientWrapper(ApacheHttpClient(httpClientBuilder.build())) + } } +} - private fun readHttpConfig(): HttpConfig { - val httpConfig = HttpConfig() +/** + * A method interceptor for [HttpClientProvider] which wraps returned HttpClient instances in a custom wrapper + * to augment them with additional functional behaviors. + */ +object HttpClientScriptInterceptor : MethodInterceptor { - httpSettingsHelper?.let { - httpConfig.timeOut = it.httpTimeOut(TimeUnit.MILLISECONDS) - } + private val logger: Logger = ActionContext.local() + private val ruleComputer: RuleComputer = ActionContext.local() + private val httpSettingsHelper: HttpSettingsHelper = ActionContext.local() - if (configReader != null) { - try { - configReader.first("http.timeOut")?.toLong() - ?.let { httpConfig.timeOut = TimeUnit.SECONDS.toMillis(it).toInt() } - } catch (e: NumberFormatException) { - logger.warn("http.timeOut must be a number") - } + /** + * Intercepts method invocations on [HttpClientProvider]s, wrapping returned HttpClient objects + * if they are not already wrapped. + */ + override fun invoke(invocation: MethodInvocation): Any { + if (invocation.method.returnType == HttpClient::class.java) { + return (invocation.proceed() as HttpClient).wrap() } + return invocation.proceed() + } - return httpConfig + /** + * Ensures that an HttpClient instance is wrapped with [HttpClientWrapper] to apply custom behaviors. + * + * @return A wrapped HttpClient instance. + */ + private fun HttpClient.wrap(): HttpClient { + if (this is HttpClientWrapper) { + return this + } + return HttpClientWrapper(this) } /** * A wrapper class that implements the HttpClient interface and delegates to a wrapped HttpClient instance. */ @ScriptTypeName("httpClient") - private inner class HttpClientWrapper(private val httpClient: HttpClient) : HttpClient { + internal class HttpClientWrapper(val delegate: HttpClient) : HttpClient { override fun cookieStore(): CookieStore { - return httpClient.cookieStore() + return delegate.cookieStore() } /** * Wraps the request in a custom HttpRequestWrapper implementation and delegates to the wrapped HttpClient instance. */ override fun request(): HttpRequest { - return HttpRequestWrapper(httpClient.request()) + return HttpRequestWrapper(delegate.request()) } } @@ -114,7 +98,7 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { * A wrapper class that implements the HttpRequest interface and delegates to a wrapped HttpRequest instance. */ @ScriptTypeName("request") - private inner class HttpRequestWrapper(private val httpRequest: HttpRequest) : HttpRequest by httpRequest { + private class HttpRequestWrapper(private val httpRequest: HttpRequest) : HttpRequest by httpRequest { /** * Set the HTTP method to request @@ -235,9 +219,7 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { */ override fun call(): HttpResponse { val url = url() ?: throw IllegalArgumentException("url not be set") - if (httpSettingsHelper != null - && !httpSettingsHelper.checkTrustUrl(url, false) - ) { + if (!httpSettingsHelper.checkTrustUrl(url, false)) { logger.warn("[access forbidden] call:$url") return EmptyHttpResponse(this) } @@ -245,7 +227,7 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { while (true) { val suvRuleContext = SuvRuleContext() suvRuleContext.setExt("request", this) - ruleComputer!!.computer(ClassExportRuleKeys.HTTP_CLIENT_BEFORE_CALL, suvRuleContext, null) + ruleComputer.computer(ClassExportRuleKeys.HTTP_CLIENT_BEFORE_CALL, suvRuleContext, null) val response = DiscardAbleHttpResponse(httpRequest.call()) suvRuleContext.setExt("response", response) ruleComputer.computer(ClassExportRuleKeys.HTTP_CLIENT_AFTER_CALL, suvRuleContext, null) @@ -262,7 +244,7 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { /** * An implementation of the HttpResponse interface that returns empty or null values for all methods. */ - class EmptyHttpResponse(private val request: HttpRequest) : HttpResponse { + private class EmptyHttpResponse(private val request: HttpRequest) : HttpResponse { override fun code(): Int { return 404 } @@ -322,7 +304,7 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { * discard() method that can be used to discard the current response and recall the request. */ @ScriptTypeName("response") - class DiscardAbleHttpResponse(httpResponse: HttpResponse) : HttpResponse by httpResponse { + private class DiscardAbleHttpResponse(httpResponse: HttpResponse) : HttpResponse by httpResponse { private var discarded = false @@ -338,17 +320,4 @@ class ConfigurableHttpClientProvider : AbstractHttpClientProvider() { return this.discarded } } - - /** - * A data class that holds configuration settings for the HTTP client. - */ - class HttpConfig { - - //default 10s - var timeOut: Int = defaultHttpTimeOut - } - - companion object { - const val defaultHttpTimeOut: Int = 10 - } } \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/OkHttpClient.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/OkHttpClient.kt new file mode 100644 index 000000000..7ac80d3b7 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/OkHttpClient.kt @@ -0,0 +1,330 @@ +package com.itangcent.suv.http + +import com.itangcent.annotation.script.ScriptTypeName +import com.itangcent.common.utils.GsonUtils +import com.itangcent.http.* +import com.itangcent.http.Cookie +import okhttp3.* +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody +import java.io.File +import java.io.FileNotFoundException +import java.util.concurrent.TimeUnit + +/** + * Provides a concrete implementation of HttpClient using OkHttpClient from the OkHttp library. + * + * @author tangcent + * @date 2024/04/27 + */ +@ScriptTypeName("httpClient") +class OkHttpClient : HttpClient { + + private val cookieStore: OkHttpCookieStore = OkHttpCookieStore() + + private val client: okhttp3.OkHttpClient + + constructor(clientBuilder: okhttp3.OkHttpClient.Builder) { + this.client = clientBuilder.cookieJar(cookieStore).build() + } + + constructor() : this( + okhttp3.OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + ) + + override fun cookieStore(): CookieStore { + return cookieStore + } + + override fun request(): HttpRequest { + return OkHttpRequest(this) + } + + /** + * Handles the execution of OkHttpRequest and constructs an OkHttpResponse. + */ + fun call(request: OkHttpRequest): HttpResponse { + val builder = Request.Builder() + + // Handle URL and query parameters + val httpUrlBuilder = + request.url()?.toHttpUrlOrNull()?.newBuilder() ?: throw IllegalArgumentException("Invalid URL") + request.querys()?.forEach { query -> + val name = query.name() + assert(name != null) { "Query parameter must have a name" } + httpUrlBuilder.addQueryParameter(name!!, query.value()) + } + builder.url(httpUrlBuilder.build()) + + // Handle headers + request.headers()?.forEach { header -> + val name = header.name() + assert(name != null) { "Header must have a name" } + builder.addHeader(name!!, header.value() ?: "") + } + + // Handle request body + val requestBody = buildRequestBody(request) + + // Set request method and body + builder.method(request.method(), requestBody) + + val call = client.newCall(builder.build()) + val response = call.execute() + + return OkHttpResponse(request, response) + } + + private fun buildRequestBody(request: OkHttpRequest): RequestBody? { + if (request.method().equals("GET", ignoreCase = true)) return null + + if (request.contentType()?.startsWith("application/x-www-form-urlencoded") == true) { + val formBodyBuilder = FormBody.Builder() + request.params()?.forEach { param -> + formBodyBuilder.add(param.name() ?: "", param.value() ?: "") + } + return formBodyBuilder.build() + } + + if (request.contentType()?.startsWith("multipart/form-data") == true) { + val builder = MultipartBody.Builder().setType(MultipartBody.FORM) + request.params()?.forEach { param -> + if (param.type() == "file") { + if (param.value() == null) throw FileNotFoundException("file not found") + val file = File(param.value()!!) + if (!file.exists() || !file.isFile) { + throw FileNotFoundException("file ${file.absolutePath} not exists") + } + builder.addFormDataPart( + param.name() ?: "", + file.name, + file.asRequestBody(contentType = "application/octet-stream".toMediaType()) + ) + } else { + builder.addFormDataPart(param.name() ?: "", param.value() ?: "") + } + } + return builder.build() + } + + val body = request.body() ?: return null + return (when (body) { + is String -> body + else -> GsonUtils.toJson(body) + }).toRequestBody((request.contentType() ?: "application/json; charset=utf-8").toMediaType()) + } +} + +/** + * Implementation of CookieJar interface to support cookie management in OkHttp. + */ +class OkHttpCookieStore : CookieJar, CookieStore { + private val cookieStore = mutableMapOf>() + + override fun saveFromResponse(url: HttpUrl, cookies: List) { + cookieStore[url.host] = cookies.filter { !it.isExpired() } + } + + override fun loadForRequest(url: HttpUrl): List { + val cookies = cookieStore[url.host] ?: return emptyList() + return cookies.asSequence() + .filter { !it.isExpired() } + .filter { it.matches(url) } + .toList() + } + + override fun addCookie(cookie: Cookie?) { + if (cookie == null || cookie.isExpired()) { + return + } + val domain = cookie.getDomain() ?: return + val existingCookies = cookieStore.getOrDefault(domain, emptyList()) + cookieStore[domain] = existingCookies.toMutableList() + cookie.asOkHttpCookie() + + } + + override fun addCookies(cookies: Array?) { + cookies?.forEach { addCookie(it) } + } + + override fun cookies(): List { + return cookieStore.values.asSequence() + .flatten() + .filter { !it.isExpired() } + .map { OkHttpCookie(it) } + .toList() + } + + override fun clear() { + cookieStore.clear() + } + + override fun newCookie(): MutableCookie { + return BasicCookie() + } +} + +private fun okhttp3.Cookie.isExpired(): Boolean { + return expiresAt < System.currentTimeMillis() +} + +/** + * A simple wrapper around the okhttp3.Cookie to adapt it to the custom [Cookie] interface. + */ +@ScriptTypeName("cookie") +class OkHttpCookie(private val cookie: okhttp3.Cookie) : Cookie { + + fun getWrapper(): okhttp3.Cookie { + return cookie + } + + override fun getName(): String { + return cookie.name + } + + override fun getValue(): String { + return cookie.value + } + + override fun getDomain(): String { + return cookie.domain + } + + override fun getPath(): String { + return cookie.path + } + + override fun getExpiryDate(): Long { + return cookie.expiresAt + } + + override fun isPersistent(): Boolean { + return cookie.persistent + } + + override fun isSecure(): Boolean { + return cookie.secure + } + + override fun getComment(): String? { + // OkHttp's Cookie class does not support comments; return null or an empty string if needed. + return null + } + + override fun getCommentURL(): String? { + // OkHttp's Cookie class does not support comment URLs; return null. + return null + } + + override fun getPorts(): IntArray? { + // OkHttp's Cookie class does not support ports; return null. + return null + } + + override fun getVersion(): Int { + // OkHttp's Cookie class does not explicitly handle version; typically version 1 (Netscape spec) is assumed. + return 1 + } + + override fun toString(): String { + return cookie.toString() + } +} + +// Converts a generic Cookie instance into an okhttp3.Cookie. +fun Cookie.asOkHttpCookie(): okhttp3.Cookie { + if (this is OkHttpCookie) { + return this.getWrapper() + } + + // Build a new OkHttp Cookie from generic Cookie interface + return okhttp3.Cookie.Builder().apply { + name(this@asOkHttpCookie.getName() ?: throw IllegalArgumentException("Cookie name cannot be null")) + value(this@asOkHttpCookie.getValue() ?: throw IllegalArgumentException("Cookie value cannot be null")) + domain(this@asOkHttpCookie.getDomain() ?: throw IllegalArgumentException("Cookie domain cannot be null")) + path(this@asOkHttpCookie.getPath() ?: "/") // Default to root if path is not specified + + if (this@asOkHttpCookie.getExpiryDate() != null) { + expiresAt(this@asOkHttpCookie.getExpiryDate()!!) + } else { + // If no expiry is set, use a far future date to mimic a non-expiring cookie + expiresAt(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365)) // Plus one year + } + + if (this@asOkHttpCookie.isSecure()) { + secure() + } + }.build() +} + +/** + * Represents an HTTP request specific to OkHttp implementation. + * Handles the delegation of the call to the OkHttpClient instance. + */ +@ScriptTypeName("request") +class OkHttpRequest(private val client: OkHttpClient) : AbstractHttpRequest() { + override fun call(): HttpResponse { + return client.call(this) + } +} + +/** + * Represents an HTTP response from OkHttp. + */ +@ScriptTypeName("response") +class OkHttpResponse( + private val request: HttpRequest, + private val response: Response +) : AbstractHttpResponse(), AutoCloseable { + + /** + * Obtains the status code of the HTTP response. + * + * @return the HTTP status code + */ + override fun code(): Int { + return response.code + } + + /** + * Obtains all headers of the HTTP response. + * + * @return a list of headers (name-value pairs) + */ + override fun headers(): List { + return response.headers.names() + .flatMap { name -> response.headers(name).map { value -> BasicHttpHeader(name, value) } } + } + + /** + * the bytes message of this response. + */ + private val bodyBytes: ByteArray? by lazy { + response.body?.bytes() + } + + /** + * Obtains the byte array of the response body if available. + * + * @return the byte array of the response body, or null if no body is available + */ + override fun bytes(): ByteArray? { + return bodyBytes + } + + override fun request(): HttpRequest { + return request + } + + /** + * Closes the response to free resources. + */ + override fun close() { + response.close() + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/kotlin/com/itangcent/suv/http/OkHttpClientProvider.kt b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/OkHttpClientProvider.kt new file mode 100644 index 000000000..946495487 --- /dev/null +++ b/idea-plugin/src/main/kotlin/com/itangcent/suv/http/OkHttpClientProvider.kt @@ -0,0 +1,56 @@ +package com.itangcent.suv.http + +import com.google.inject.Inject +import com.itangcent.http.HttpClient +import com.itangcent.idea.plugin.condition.ConditionOnSetting +import com.itangcent.idea.plugin.settings.helper.HttpSettingsHelper +import org.apache.http.ssl.SSLContexts +import java.util.concurrent.TimeUnit + +/** + * An implementation of [HttpClient] using OkHttpClient from the OkHttp library. + * Provides an OkHttpClient based on configuration settings. + * + * @author tangcent + * @date 2024/05/08 + */ +@ConditionOnSetting("httpClient", havingValue = "Okhttp") +open class OkHttpClientProvider : AbstractHttpClientProvider() { + + @Inject + protected lateinit var httpSettingsHelper: HttpSettingsHelper + + override fun buildHttpClient(): HttpClient { + // Initialize OkHttpClient with timeout settings. + val timeOutInMills = httpSettingsHelper.httpTimeOut(TimeUnit.MILLISECONDS).toLong() + val builder = okhttp3.OkHttpClient.Builder() + .connectTimeout(timeOutInMills, TimeUnit.MILLISECONDS) + .readTimeout(timeOutInMills, TimeUnit.MILLISECONDS) + .writeTimeout(timeOutInMills, TimeUnit.MILLISECONDS) + + // If unsafe SSL is allowed, configure OkHttpClient to trust all certificates. + if (httpSettingsHelper.unsafeSsl()) { + builder.hostnameVerifier { _, _ -> true } + val trustAllCert = object : javax.net.ssl.X509TrustManager { + override fun checkClientTrusted( + chain: Array?, + authType: String? + ) { + } + + override fun checkServerTrusted( + chain: Array?, + authType: String? + ) { + } + + override fun getAcceptedIssuers(): Array { + return arrayOf() + } + } + builder.sslSocketFactory(SSLContexts.createSystemDefault().socketFactory, trustAllCert) + } + + return OkHttpClient(builder) + } +} \ No newline at end of file diff --git a/idea-plugin/src/main/resources/META-INF/services/com.itangcent.common.spi.SetupAble b/idea-plugin/src/main/resources/META-INF/services/com.itangcent.common.spi.SetupAble index 24f67fce5..0528c2049 100644 --- a/idea-plugin/src/main/resources/META-INF/services/com.itangcent.common.spi.SetupAble +++ b/idea-plugin/src/main/resources/META-INF/services/com.itangcent.common.spi.SetupAble @@ -1,3 +1,5 @@ com.itangcent.intellij.spi.IdeaAutoInject com.itangcent.intellij.tip.OnlyOnceInContextTipSetup com.itangcent.intellij.jvm.kotlin.KotlinAutoInject +com.itangcent.idea.psi.DisableDocSupport +com.itangcent.suv.http.HttpClientScriptInterceptorSupport \ No newline at end of file diff --git a/idea-plugin/src/main/resources/META-INF/services/com.itangcent.suv.http.HttpClientProvider b/idea-plugin/src/main/resources/META-INF/services/com.itangcent.suv.http.HttpClientProvider new file mode 100644 index 000000000..d8f217867 --- /dev/null +++ b/idea-plugin/src/main/resources/META-INF/services/com.itangcent.suv.http.HttpClientProvider @@ -0,0 +1,2 @@ +com.itangcent.suv.http.ApacheHttpClientProvider +com.itangcent.suv.http.OkHttpClientProvider \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/config/CachedResourceResolverTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/config/CachedResourceResolverTest.kt index 5dc227f0e..fe060a1bd 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/config/CachedResourceResolverTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/config/CachedResourceResolverTest.kt @@ -6,6 +6,7 @@ import com.itangcent.common.utils.readString import com.itangcent.idea.plugin.settings.SettingBinder import com.itangcent.idea.plugin.settings.Settings import com.itangcent.idea.plugin.settings.helper.HttpSettingsHelper +import com.itangcent.idea.plugin.settings.helper.HttpSettingsHelperImpl import com.itangcent.idea.swing.MessagesHelper import com.itangcent.intellij.config.resource.ResourceResolver import com.itangcent.intellij.context.ActionContext @@ -34,7 +35,7 @@ internal class CachedResourceResolverTest : AdvancedContextTest() { @Inject private lateinit var httpSettingsHelper: HttpSettingsHelper - private val delegateHttpSettingsHelper: HttpSettingsHelper = HttpSettingsHelper() + private val delegateHttpSettingsHelper: HttpSettingsHelper = HttpSettingsHelperImpl() private val settings: Settings = Settings() diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/ETHUtils.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/ETHUtils.kt new file mode 100644 index 000000000..f40a24f26 --- /dev/null +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/ETHUtils.kt @@ -0,0 +1,86 @@ +package com.itangcent.idea.plugin.settings + +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.memberProperties +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull + +/** + * @author tangcent + * @date 2024/05/11 + */ +object ETHUtils { + + fun testCETH(original: T, copy: T.() -> T) { + assertNotNull(original, "original should not be null") + original!! + // Create a copy using the data class generated `copy` method + val copied = original.copy() + assertEquals(original, copied) + assertEquals(original.hashCode(), copied.hashCode()) + assertEquals(original.toString(), copied.toString()) + + // Use reflection to fetch all properties + Settings::class.memberProperties + .filterIsInstance>() + .forEach { property -> + val newCopied = original.copy() + + val backup = property.getter.call(newCopied) + val propClass = property.returnType.classifier as? KClass<*> + val updateValue = backup?.backup() ?: propClass?.fake() + + property.setter.call(newCopied, updateValue) + // Check that the modified object does not equal the copied object + assertNotEquals( + original, + newCopied, + "[${original!!::class}] Change Property: ${property.name} from $backup to $updateValue}" + ) + assertNotEquals( + original.hashCode(), newCopied.hashCode(), + "[${original!!::class}] Change Property: ${property.name} from $backup to $updateValue}" + ) + assertNotEquals( + original.toString(), newCopied.toString(), + "[${original!!::class}] Change Property: ${property.name} from $backup to $updateValue}" + ) + + // Restore original property to continue clean tests + property.setter.call(original, backup) + } + } +} + +fun Any.backup(): Any { + return when (this) { + is Array<*> -> { + if (this.size > 0) { + val newArray = this.copyOf(size + 1) as Array + newArray[newArray.lastIndex] = this.first()!!.backup() + newArray + } else { + // get the component type of the array + val componentType = this::class.java.componentType.kotlin + val fake = componentType.fake() + (this.copyOf(1) as Array).apply { this[0] = fake } + } + } + + is Boolean -> !this + is String -> "$this modify" + is Int -> this + 1 + else -> throw IllegalArgumentException("Unsupported type: ${this::class.simpleName}") + } +} + +fun KClass<*>.fake(): Any { + return when (this) { + Boolean::class -> false + String::class -> "fake" + Int::class -> 0 + else -> throw IllegalArgumentException("Unsupported type: ${this.simpleName}") + } +} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/SettingsTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/SettingsTest.kt index 322afdbd0..ecbd213ee 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/SettingsTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/plugin/settings/SettingsTest.kt @@ -1,8 +1,6 @@ package com.itangcent.idea.plugin.settings import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotEquals /** * Test case of [Settings] @@ -10,15 +8,11 @@ import kotlin.test.assertNotEquals internal class SettingsTest { @Test - fun testCEH() { - val settings = Settings() - settings.pullNewestDataBefore = true - settings.yapiServer = "http://127.0.0.1" + fun testCETH() { + val original = Settings() + original.pullNewestDataBefore = true + original.yapiServer = "http://127.0.0.1" - val copySettings = settings.copy() - assertEquals(settings, copySettings) - assertEquals(settings.hashCode(), copySettings.hashCode()) - settings.pullNewestDataBefore = false - assertNotEquals(settings.hashCode(), copySettings.hashCode()) + ETHUtils.testCETH(original) { copy() } } } \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/idea/psi/DisableDocSupportTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/idea/psi/DisableDocSupportTest.kt index d3895f028..9734ec9a9 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/idea/psi/DisableDocSupportTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/idea/psi/DisableDocSupportTest.kt @@ -2,6 +2,7 @@ package com.itangcent.idea.psi import com.google.inject.Inject import com.intellij.psi.PsiClass +import com.itangcent.common.spi.Setup import com.itangcent.intellij.context.ActionContext import com.itangcent.intellij.jvm.DocHelper import com.itangcent.testFramework.PluginContextLightCodeInsightFixtureTestCase @@ -19,11 +20,13 @@ abstract class DisableDocSupportTest : PluginContextLightCodeInsightFixtureTestC @Inject protected lateinit var docHelper: DocHelper + override fun beforeBind() { + super.beforeBind() + Setup.setup(DisableDocSupport::class) + } + override fun bind(builder: ActionContext.ActionContextBuilder) { super.bind(builder) - - DisableDocSupport.bind(builder) - userInfoPsiClass = loadClass("model/UserInfo.java")!! userCtrlPsiClass = loadClass("api/UserCtrl.java")!! } diff --git a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProviderTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProviderTest.kt deleted file mode 100644 index 4eb5aba13..000000000 --- a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/ConfigurableHttpClientProviderTest.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.itangcent.suv.http - -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue - -/** - * Test case of [ConfigurableHttpClientProvider] - */ -internal class ConfigurableHttpClientProviderTest : HttpClientProviderTest() { - - override val httpClientProviderClass get() = ConfigurableHttpClientProvider::class - - override fun customConfig(): String { - return "http.call.before=groovy:logger.info(\"call:\"+request.url())\nhttp.call.after=groovy:logger.info(\"response:\"+response.string())\nhttp.timeOut=3" - } - - @Test - fun `test buildHttpClient`() { - // Build an instance of HttpClient using the provider. - val httpClient = httpClientProvider.getHttpClient() - - // Assert that the HttpClient instance is not null. - Assertions.assertNotNull(httpClient) - - // Assert that the HttpClient instance is an instance of HttpClientWrapper. - Assertions.assertEquals( - "com.itangcent.suv.http.ConfigurableHttpClientProvider.HttpClientWrapper", - httpClient::class.qualifiedName - ) - } - - @Test - fun `test callForbiddenRequest`() { - - // Build an instance of HttpClient using the provider. - val httpClient = httpClientProvider.getHttpClient() - - // Create an instance of HttpRequest using the HttpClient instance. - val httpRequest = httpClient.request() - .method("GET") - .url("http://forbidden.com") - - // Send the request and receive the response. - val emptyHttpResponse = httpRequest.call() - - // Assert that the response code is 404. - assertEquals(404, emptyHttpResponse.code()) - - // Assert that the response headers are null. - assertNull(emptyHttpResponse.headers()) - - // Assert that the response headers for a specific name are null. - assertNull(emptyHttpResponse.headers("Content-Type")) - - // Assert that the response string is null. - assertNull(emptyHttpResponse.string()) - - // Assert that the response string with a specific charset is null. - assertNull(emptyHttpResponse.string(Charsets.UTF_8)) - - // Assert that the response stream is empty. - assertTrue(emptyHttpResponse.stream().readBytes().isEmpty()) - - // Assert that the response content type is null. - assertNull(emptyHttpResponse.contentType()) - - // Assert that the response bytes are null. - assertNull(emptyHttpResponse.bytes()) - - // Assert that the response does not contain a specific header. - Assertions.assertFalse(emptyHttpResponse.containsHeader("Content-Type")) - - // Assert that the first header for a specific name is null. - assertNull(emptyHttpResponse.firstHeader("Content-Type")) - - // Assert that the last header for a specific name is null. - assertNull(emptyHttpResponse.lastHeader("Content-Type")) - - // Assert that the response request is the same as the sample request. - assertEquals(httpRequest, emptyHttpResponse.request()) - } -} - - -internal class NonConfigConfigurableHttpClientProviderTest : HttpClientProviderTest() { - - override val httpClientProviderClass get() = ConfigurableHttpClientProvider::class - - @Test - fun `test buildHttpClient`() { - // Build an instance of HttpClient using the provider. - val httpClient = httpClientProvider.getHttpClient() - - // Assert that the HttpClient instance is not null. - Assertions.assertNotNull(httpClient) - } -} - -internal class IllegalConfigConfigurableHttpClientProviderTest : HttpClientProviderTest() { - - override val httpClientProviderClass get() = ConfigurableHttpClientProvider::class - - override fun customConfig(): String { - return "http.timeOut=illegal" - } - - @Test - fun `test buildHttpClient`() { - // Build an instance of HttpClient using the provider. - val httpClient = httpClientProvider.getHttpClient() - - // Assert that the HttpClient instance is not null. - Assertions.assertNotNull(httpClient) - } -} - - diff --git a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt index 9f2a5cf3f..99f5870e2 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/DefaultHttpClientProviderTest.kt @@ -1,9 +1,232 @@ package com.itangcent.suv.http +import com.itangcent.common.utils.DateUtils +import com.itangcent.http.ApacheCookie +import com.itangcent.http.ApacheHttpClient +import com.itangcent.http.asApacheCookie +import com.itangcent.idea.plugin.settings.HttpClientType +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertNull +import kotlin.test.assertTrue + /** * Test case of [DefaultHttpClientProvider] */ -internal class DefaultHttpClientProviderTest : HttpClientProviderTest() { +internal abstract class DefaultHttpClientProviderTest : HttpClientProviderTest() { override val httpClientProviderClass get() = DefaultHttpClientProvider::class -} \ No newline at end of file + + override fun customConfig(): String { + return "http.call.before=groovy:logger.info(\"call:\"+request.url())\nhttp.call.after=groovy:logger.info(\"response:\"+response.string())\nhttp.timeOut=3" + } + + @Test + fun `test buildHttpClient`() { + // Build an instance of HttpClient using the provider. + val httpClient = httpClientProvider.getHttpClient() + + // Assert that the HttpClient instance is not null. + Assertions.assertNotNull(httpClient) + + // Assert that the HttpClient instance is an instance of HttpClientWrapper. + Assertions.assertEquals( + "com.itangcent.suv.http.HttpClientScriptInterceptor.HttpClientWrapper", + httpClient::class.qualifiedName + ) + } + + @Test + fun `test callForbiddenRequest`() { + + // Build an instance of HttpClient using the provider. + val httpClient = httpClientProvider.getHttpClient() + + // Create an instance of HttpRequest using the HttpClient instance. + val httpRequest = httpClient.request() + .method("GET") + .url("http://forbidden.com") + + // Send the request and receive the response. + httpRequest.call().use { emptyHttpResponse -> + // Assert that the response code is 404. + assertEquals(404, emptyHttpResponse.code()) + + // Assert that the response headers are null. + assertNull(emptyHttpResponse.headers()) + + // Assert that the response headers for a specific name are null. + assertNull(emptyHttpResponse.headers("Content-Type")) + + // Assert that the response string is null. + assertNull(emptyHttpResponse.string()) + + // Assert that the response string with a specific charset is null. + assertNull(emptyHttpResponse.string(Charsets.UTF_8)) + + // Assert that the response stream is empty. + assertTrue(emptyHttpResponse.stream().readBytes().isEmpty()) + + // Assert that the response content type is null. + assertNull(emptyHttpResponse.contentType()) + + // Assert that the response bytes are null. + assertNull(emptyHttpResponse.bytes()) + + // Assert that the response does not contain a specific header. + Assertions.assertFalse(emptyHttpResponse.containsHeader("Content-Type")) + + // Assert that the first header for a specific name is null. + assertNull(emptyHttpResponse.firstHeader("Content-Type")) + + // Assert that the last header for a specific name is null. + assertNull(emptyHttpResponse.lastHeader("Content-Type")) + + // Assert that the response request is the same as the sample request. + assertEquals(httpRequest, emptyHttpResponse.request()) + + } + } +} + +internal class ApacheHttpClientProviderTest : DefaultHttpClientProviderTest() { + override fun setUp() { + settings.httpClient = HttpClientType.APACHE.value + } + + @Test + fun `the httpClient should be ApacheHttpClient`() { + val httpClient = httpClientProvider.getHttpClient() + assertTrue(httpClient is HttpClientScriptInterceptor.HttpClientWrapper) + assertTrue(httpClient.delegate is ApacheHttpClient) + } + + @Test + fun testApacheCookies() { + val httpClient = httpClientProvider.getHttpClient() + val cookieStore = httpClient.cookieStore() + cookieStore.clear() + + val token = cookieStore.newCookie() + token.setName("token") + token.setValue("111111") + token.setExpiryDate(DateUtils.parse("2021-01-01").time) + token.setDomain("github.com") + token.setPorts(intArrayOf(9999)) + token.setComment("for auth") + token.setCommentURL("http://www.apache.org/licenses/LICENSE-2.0") + token.setSecure(false) + token.setPath("/") + token.setVersion(100) + token.setExpiryDate(DateUtils.parse("2099-01-01").time) + + var apacheCookie = token.asApacheCookie() + + val packageApacheCookie = ApacheCookie(apacheCookie) + assertEquals("token", packageApacheCookie.getName()) + assertEquals("111111", packageApacheCookie.getValue()) + assertEquals("github.com", packageApacheCookie.getDomain()) + assertEquals("for auth", packageApacheCookie.getComment()) + assertEquals("http://www.apache.org/licenses/LICENSE-2.0", packageApacheCookie.getCommentURL()) + assertEquals("/", packageApacheCookie.getPath()) + assertEquals(100, packageApacheCookie.getVersion()) + assertContentEquals(intArrayOf(9999), packageApacheCookie.getPorts()) + assertEquals(DateUtils.parse("2099-01-01").time, packageApacheCookie.getExpiryDate()) + assertTrue(packageApacheCookie.isPersistent()) + + token.setPorts(null) + apacheCookie = token.asApacheCookie() + assertNull(apacheCookie.commentURL) + assertTrue(apacheCookie.isPersistent) + } +} + +internal class UnsafeSslApacheHttpClientProviderTest : DefaultHttpClientProviderTest() { + override fun setUp() { + settings.httpClient = HttpClientType.APACHE.value + settings.unsafeSsl = true + } + + @Test + fun `the httpClient should be ApacheHttpClient`() { + val httpClient = httpClientProvider.getHttpClient() + assertTrue(httpClient is HttpClientScriptInterceptor.HttpClientWrapper) + assertTrue(httpClient.delegate is ApacheHttpClient) + } +} + +internal class OkHttpClientProviderTest : DefaultHttpClientProviderTest() { + override fun setUp() { + settings.httpClient = HttpClientType.OKHTTP.value + } + + @Test + fun `the httpClient should be OkHttpClient`() { + val httpClient = httpClientProvider.getHttpClient() + assertTrue(httpClient is HttpClientScriptInterceptor.HttpClientWrapper) + assertTrue(httpClient.delegate is OkHttpClient) + } +} + +internal class UnsafeSslOkHttpClientProviderTest : DefaultHttpClientProviderTest() { + override fun setUp() { + settings.httpClient = HttpClientType.OKHTTP.value + settings.unsafeSsl = true + } + + @Test + fun `the httpClient should be OkHttpClient`() { + val httpClient = httpClientProvider.getHttpClient() + assertTrue(httpClient is HttpClientScriptInterceptor.HttpClientWrapper) + assertTrue(httpClient.delegate is OkHttpClient) + } +} + +internal class IllegalHttpClientProviderTest : DefaultHttpClientProviderTest() { + override fun setUp() { + settings.httpClient = "fake" + } + + @Test + fun `assert the httpClient should default to Apache`() { + val httpClient = httpClientProvider.getHttpClient() + assertTrue(httpClient is HttpClientScriptInterceptor.HttpClientWrapper) + assertTrue(httpClient.delegate is ApacheHttpClient) + } +} + +internal class NonConfigConfigurableHttpClientProviderTest : HttpClientProviderTest() { + + override val httpClientProviderClass get() = DefaultHttpClientProvider::class + + @Test + fun `test buildHttpClient`() { + // Build an instance of HttpClient using the provider. + val httpClient = httpClientProvider.getHttpClient() + + // Assert that the HttpClient instance is not null. + Assertions.assertNotNull(httpClient) + } +} + +internal class IllegalConfigConfigurableHttpClientProviderTest : HttpClientProviderTest() { + + override val httpClientProviderClass get() = DefaultHttpClientProvider::class + + override fun customConfig(): String { + return "http.timeOut=illegal" + } + + @Test + fun `test buildHttpClient`() { + // Build an instance of HttpClient using the provider. + val httpClient = httpClientProvider.getHttpClient() + + // Assert that the HttpClient instance is not null. + Assertions.assertNotNull(httpClient) + } +} + + diff --git a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt index 3494442b4..bd3da9ed2 100644 --- a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt +++ b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientProviderTest.kt @@ -32,11 +32,13 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { abstract val httpClientProviderClass: KClass + protected val settings = Settings() + override fun bind(builder: ActionContext.ActionContextBuilder) { super.bind(builder) builder.bind(HttpClientProvider::class) { it.with(httpClientProviderClass) } builder.bind(SettingBinder::class) { - it.toInstance(SettingBinderAdaptor(Settings().also { settings -> + it.toInstance(SettingBinderAdaptor(settings.also { settings -> settings.trustHosts = arrayOf( "https://jsonplaceholder.typicode.com", "!http://forbidden.com" @@ -56,16 +58,17 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { .url("https://jsonplaceholder.typicode.com/todos/1") // Send the request and receive the response. - val httpResponse = httpRequest.call() + httpRequest.call().use { httpResponse -> - // Assert that the response is not null. - assertNotNull(httpResponse) + // Assert that the response is not null. + assertNotNull(httpResponse) - // Assert that the response has a status code of 200. - assertEquals(200, httpResponse.code()) + // Assert that the response has a status code of 200. + assertEquals(200, httpResponse.code()) - // Assert that the response has a non-empty entity. - assertNotNull(httpResponse.bytes()) + // Assert that the response has a non-empty entity. + assertNotNull(httpResponse.bytes()) + } } @Test @@ -81,19 +84,20 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { .body("Hello, world!") // Send the request and receive the response. - val httpResponse = httpRequest.call() + httpRequest.call().use { httpResponse -> - // Assert that the response is not null. - assertNotNull(httpResponse) + // Assert that the response is not null. + assertNotNull(httpResponse) - // Assert that the response has a status code of 201. - assertEquals(201, httpResponse.code()) + // Assert that the response has a status code of 201. + assertEquals(201, httpResponse.code()) - // Assert that the response has a non-empty entity. - assertNotNull(httpResponse.bytes()) + // Assert that the response has a non-empty entity. + assertNotNull(httpResponse.bytes()) - // Assert that the response entity contains the expected text. - assertTrue(httpResponse.string().notNullOrBlank()) + // Assert that the response entity contains the expected text. + assertTrue(httpResponse.string().notNullOrBlank()) + } } @Test @@ -109,19 +113,20 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { .body("Hello, world!") // Send the request and receive the response. - val httpResponse = httpRequest.call() + httpRequest.call().use { httpResponse -> - // Assert that the response is not null. - assertNotNull(httpResponse) + // Assert that the response is not null. + assertNotNull(httpResponse) - // Assert that the response has a status code of 200. - assertEquals(200, httpResponse.code()) + // Assert that the response has a status code of 200. + assertEquals(200, httpResponse.code()) - // Assert that the response has a non-empty entity. - assertNotNull(httpResponse.bytes()) + // Assert that the response has a non-empty entity. + assertNotNull(httpResponse.bytes()) - // Assert that the response entity contains the expected text. - assertTrue(httpResponse.string().notNullOrBlank()) + // Assert that the response entity contains the expected text. + assertTrue(httpResponse.string().notNullOrBlank()) + } } @Test @@ -135,19 +140,20 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { .url("https://jsonplaceholder.typicode.com/posts/1") // Send the request and receive the response. - val httpResponse = httpRequest.call() + httpRequest.call().use { httpResponse -> - // Assert that the response is not null. - assertNotNull(httpResponse) + // Assert that the response is not null. + assertNotNull(httpResponse) - // Assert that the response has a status code of 200. - assertEquals(200, httpResponse.code()) + // Assert that the response has a status code of 200. + assertEquals(200, httpResponse.code()) - // Assert that the response has a non-empty entity. - assertNotNull(httpResponse.bytes()) + // Assert that the response has a non-empty entity. + assertNotNull(httpResponse.bytes()) - // Assert that the response entity contains the expected text. - assertEquals("{}", httpResponse.string()) + // Assert that the response entity contains the expected text. + assertEquals("{}", httpResponse.string()) + } } @Test @@ -354,6 +360,7 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { fun testCookies() { val httpClient = httpClientProvider.getHttpClient() val cookieStore = httpClient.cookieStore() + cookieStore.clear() assertTrue(cookieStore.cookies().isEmpty()) val token = cookieStore.newCookie() @@ -361,12 +368,8 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { token.setValue("111111") token.setExpiryDate(DateUtils.parse("2021-01-01").time) token.setDomain("github.com") - token.setPorts(intArrayOf(9999)) - token.setComment("for auth") - token.setCommentURL("http://www.apache.org/licenses/LICENSE-2.0") token.setSecure(false) token.setPath("/") - token.setVersion(100) assertTrue(token.isPersistent()) //add cookie which is expired @@ -382,10 +385,7 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { assertEquals("token", it.getName()) assertEquals("111111", it.getValue()) assertEquals("github.com", it.getDomain()) - assertEquals("for auth", it.getComment()) - assertEquals("http://www.apache.org/licenses/LICENSE-2.0", it.getCommentURL()) assertEquals("/", it.getPath()) - assertEquals(100, it.getVersion()) assertEquals(false, it.isSecure()) assertEquals(DateUtils.parse("2099-01-01").time, it.getExpiryDate()) @@ -393,10 +393,7 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { assertEquals("token", fromJson.getName()) assertEquals("111111", fromJson.getValue()) assertEquals("github.com", fromJson.getDomain()) - assertEquals("for auth", fromJson.getComment()) - assertEquals("http://www.apache.org/licenses/LICENSE-2.0", fromJson.getCommentURL()) assertEquals("/", fromJson.getPath()) - assertEquals(100, fromJson.getVersion()) assertEquals(false, fromJson.isSecure()) assertEquals(DateUtils.parse("2099-01-01").time, fromJson.getExpiryDate()) @@ -405,10 +402,7 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { assertEquals("token", mutable.getName()) assertEquals("111111", mutable.getValue()) assertEquals("github.com", mutable.getDomain()) - assertEquals("for auth", mutable.getComment()) - assertEquals("http://www.apache.org/licenses/LICENSE-2.0", mutable.getCommentURL()) assertEquals("/", mutable.getPath()) - assertEquals(100, mutable.getVersion()) assertEquals(false, mutable.isSecure()) assertEquals(DateUtils.parse("2099-01-01").time, mutable.getExpiryDate()) @@ -422,17 +416,5 @@ internal abstract class HttpClientProviderTest : AdvancedContextTest() { assertTrue(cookieStore.cookies().isEmpty()) cookieStore.addCookies(cookies.toTypedArray()) assertEquals(1, cookies.size) - - token.setPorts(null) - val apacheCookie = token.asApacheCookie() - assertNull(apacheCookie.commentURL) - assertTrue(apacheCookie.isPersistent) - - val packageApacheCookie = ApacheCookie(apacheCookie) - assertEquals("token", packageApacheCookie.getName()) - assertEquals("111111", packageApacheCookie.getValue()) - assertEquals("github.com", packageApacheCookie.getDomain()) - assertEquals("for auth", packageApacheCookie.getComment()) - assertTrue(packageApacheCookie.isPersistent()) } } \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientScriptInterceptorTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientScriptInterceptorTest.kt new file mode 100644 index 000000000..9874aff5b --- /dev/null +++ b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/HttpClientScriptInterceptorTest.kt @@ -0,0 +1,54 @@ +package com.itangcent.suv.http + +import com.itangcent.http.HttpClient +import com.itangcent.intellij.context.ActionContext +import com.itangcent.intellij.extend.guice.with +import com.itangcent.mock.AdvancedContextTest +import org.mockito.kotlin.mock +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull + +/** + * Test for [HttpClientScriptInterceptor] + * + * @author tangcent + * @date 2024/05/11 + */ +class HttpClientScriptInterceptorTest : AdvancedContextTest() { + + override fun bind(builder: ActionContext.ActionContextBuilder) { + super.bind(builder) + builder.bind(HttpClientProvider::class) { it.with(HttpClientProviderImpl::class) } + } + + open class HttpClientProviderImpl : HttpClientProvider { + override fun getHttpClient(): HttpClient = mock() + + // keep it open for test + open fun otherMethod(): String = "otherMethod" + } + + @Test + fun testGetHttpClient() { + val httpClientProvider = actionContext.instance(HttpClientProvider::class) + assertNotNull(httpClientProvider) + assertIs(httpClientProvider) + + val httpClient = httpClientProvider.getHttpClient() + assertNotNull(httpClient) + assertIs(httpClient) + } + + @Test + fun testOtherMethod() { + val httpClientProvider = actionContext.instance(HttpClientProvider::class) + assertNotNull(httpClientProvider) + assertIs(httpClientProvider) + + val res = httpClientProvider.otherMethod() + assertNotNull(res) + assertEquals("otherMethod", res) + } +} \ No newline at end of file diff --git a/idea-plugin/src/test/kotlin/com/itangcent/suv/http/OkHttpClientTest.kt b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/OkHttpClientTest.kt new file mode 100644 index 000000000..671cc53c8 --- /dev/null +++ b/idea-plugin/src/test/kotlin/com/itangcent/suv/http/OkHttpClientTest.kt @@ -0,0 +1,628 @@ +package com.itangcent.suv.http + +import com.itangcent.common.utils.DateUtils +import com.itangcent.common.utils.FileUtils +import com.itangcent.common.utils.readString +import com.itangcent.http.* +import okhttp3.Headers +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Response +import okhttp3.ResponseBody +import org.apache.http.HttpEntityEnclosingRequest +import org.apache.http.conn.ConnectTimeoutException +import org.apache.http.entity.ContentType +import org.junit.jupiter.api.Assertions.assertArrayEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.assertDoesNotThrow +import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.io.TempDir +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import java.io.File +import java.io.FileNotFoundException +import java.nio.charset.Charset +import java.nio.file.Path +import java.util.concurrent.TimeUnit +import kotlin.reflect.KClass +import kotlin.test.* + +/** + * Test case of [OkHttpClient] + * + * @author tangcent + * @date 2024/05/09 + */ +class OkHttpClientTest { + + @Test + fun testMethods() { + val httpClient: HttpClient = OkHttpClient() + + //GET + httpClient.get().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("GET", it.method()) + } + httpClient.get("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("GET", it.method()) + } + + //POST + httpClient.post().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("POST", it.method()) + } + httpClient.post("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("POST", it.method()) + } + + //PUT + httpClient.put().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("PUT", it.method()) + } + httpClient.put("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("PUT", it.method()) + } + + //DELETE + httpClient.delete().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("DELETE", it.method()) + } + httpClient.delete("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("DELETE", it.method()) + } + + //OPTIONS + httpClient.options().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("OPTIONS", it.method()) + } + httpClient.options("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("OPTIONS", it.method()) + } + + //TRACE + httpClient.trace().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("TRACE", it.method()) + } + httpClient.trace("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("TRACE", it.method()) + } + + //PATCH + httpClient.patch().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("PATCH", it.method()) + } + httpClient.patch("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("PATCH", it.method()) + } + + //HEAD + httpClient.head().url("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("HEAD", it.method()) + } + httpClient.head("https://github.com/tangcent/easy-yapi/pulls").let { + assertEquals("https://github.com/tangcent/easy-yapi/pulls", it.url()) + assertEquals("HEAD", it.method()) + } + + } + + @Test + fun testHeaders() { + val httpClient: HttpClient = OkHttpClient() + val request = httpClient.request() + + assertFalse(request.containsHeader("x-token")) + assertNull(request.headers("x-token")) + assertNull(request.firstHeader("x-token")) + assertNull(request.lastHeader("x-token")) + + assertDoesNotThrow { request.removeHeaders("x-token") } + assertDoesNotThrow { request.removeHeader("x-token", "222222") } + + assertFalse(request.containsHeader("x-token")) + assertNull(request.headers("x-token")) + assertNull(request.firstHeader("x-token")) + assertNull(request.lastHeader("x-token")) + + request.header("x-token", "111111") + assertTrue(request.containsHeader("x-token")) + assertArrayEquals(arrayOf("111111"), request.headers("x-token")) + assertEquals("111111", request.firstHeader("x-token")) + assertEquals("111111", request.lastHeader("x-token")) + + request.header(BasicHttpHeader("x-token", null)) + request.header(BasicHttpHeader("x-token", "222222")) + assertTrue(request.containsHeader("x-token")) + assertArrayEquals(arrayOf("111111", "222222"), request.headers("x-token")) + assertEquals("111111", request.firstHeader("x-token")) + assertEquals("222222", request.lastHeader("x-token")) + + request.removeHeader("x-token", "222222") + assertTrue(request.containsHeader("x-token")) + assertArrayEquals(arrayOf("111111"), request.headers("x-token")) + assertEquals("111111", request.firstHeader("x-token")) + assertEquals("111111", request.lastHeader("x-token")) + + request.removeHeaders("x-token") + assertFalse(request.containsHeader("x-token")) + } + + @Test + fun testQuery() { + val httpClient: HttpClient = OkHttpClient() + val request = httpClient.request() + assertNull(request.querys()) + request.query("q", "test") + assertNotNull(request.querys()) + } + + @Test + fun testBody() { + val httpClient: HttpClient = OkHttpClient() + val request = httpClient.request() + assertNull(request.body()) + request.body("body") + assertEquals("body", request.body()) + request.body(1) + assertEquals(1, request.body()) + } + + @Test + fun testContentType() { + val httpClient: HttpClient = OkHttpClient() + val request = httpClient.request() + assertNull(request.contentType()) + request.contentType("application/json") + assertEquals("application/json", request.contentType()) + assertEquals("application/json", request.firstHeader("content-type")) + request.contentType(ContentType.IMAGE_PNG) + assertEquals("image/png", request.contentType()) + assertEquals("image/png", request.firstHeader("content-type")) + } + + @Test + fun testParams() { + val httpClient: HttpClient = OkHttpClient() + val request = httpClient.request() + assertFalse(request.containsParam("auth")) + assertNull(request.params("auth")) + assertNull(request.firstParam("auth")) + assertNull(request.lastParam("auth")) + + request.param("auth", "111111") + assertTrue(request.containsParam("auth")) + assertArrayEquals(arrayOf("111111"), request.paramValues("auth")) + assertEquals("111111", request.firstParamValue("auth")) + assertEquals("111111", request.lastParamValue("auth")) + request.firstParam("auth")?.let { + assertEquals("auth", it.name()) + assertEquals("111111", it.value()) + assertEquals("text", it.type()) + } + + request.param("token", "xxxxx") + request.param("auth", null) + request.fileParam("auth", "222222") + assertTrue(request.containsParam("auth")) + assertArrayEquals(arrayOf("111111", "222222"), request.paramValues("auth")) + assertEquals("111111", request.firstParamValue("auth")) + assertEquals("222222", request.lastParamValue("auth")) + request.lastParam("auth")?.let { + assertEquals("auth", it.name()) + assertEquals("222222", it.value()) + assertEquals("file", it.type()) + } + } + + @Test + fun testCookies() { + val httpClient: HttpClient = OkHttpClient() + val cookieStore = httpClient.cookieStore() + assertTrue(cookieStore.cookies().isEmpty()) + + val token = cookieStore.newCookie() + token.setName("token") + token.setValue("111111") + token.setExpiryDate(DateUtils.parse("2021-01-01").time) + token.setDomain("github.com") + token.setSecure(false) + token.setPath("/") + assertTrue(token.isPersistent()) + + //add cookie which is expired + cookieStore.addCookie(token) + assertTrue(cookieStore.cookies().isEmpty()) + + token.setExpiryDate(DateUtils.parse("2099-01-01").time) + cookieStore.addCookie(token) + + val cookies = cookieStore.cookies() + assertEquals(1, cookies.size) + cookies.first().let { + assertEquals("token", it.getName()) + assertEquals("111111", it.getValue()) + assertEquals("github.com", it.getDomain()) + assertEquals("/", it.getPath()) + assertEquals(false, it.isSecure()) + assertEquals(DateUtils.parse("2099-01-01").time, it.getExpiryDate()) + + val fromJson = BasicCookie.fromJson(it.json()) + assertEquals("token", fromJson.getName()) + assertEquals("111111", fromJson.getValue()) + assertEquals("github.com", fromJson.getDomain()) + assertEquals("/", fromJson.getPath()) + assertEquals(false, fromJson.isSecure()) + assertEquals(DateUtils.parse("2099-01-01").time, fromJson.getExpiryDate()) + + val mutable = it.mutable() + assertSame(mutable, mutable.mutable()) + assertEquals("token", mutable.getName()) + assertEquals("111111", mutable.getValue()) + assertEquals("github.com", mutable.getDomain()) + assertEquals("/", mutable.getPath()) + assertEquals(false, mutable.isSecure()) + assertEquals(DateUtils.parse("2099-01-01").time, mutable.getExpiryDate()) + + val str = it.toString() + assertTrue(str.contains("token")) + assertTrue(str.contains("111111")) + assertTrue(str.contains("github.com")) + } + + cookieStore.clear() + assertTrue(cookieStore.cookies().isEmpty()) + cookieStore.addCookies(cookies.toTypedArray()) + assertEquals(1, cookies.size) + } + + @Test + fun testCall() { + try { + val httpClient = OkHttpClient( + okhttp3.OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + ) + val httpResponse = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .param("hello", "hello") + .body("hello") + .call() + if (500 == httpResponse.code()) { + assertTrue(httpResponse.string()!!.contains("Internal Server Error")) + } + } catch (e: ConnectTimeoutException) { + //skip test if connect timed out + } + } +} + +open class AbstractCallTest { + + protected lateinit var call: okhttp3.Call + protected lateinit var httpClient: okhttp3.OkHttpClient + protected lateinit var httpClientBuilder: okhttp3.OkHttpClient.Builder + protected lateinit var httpResponseBody: ResponseBody + + protected var responseCode: Int = 200 + protected lateinit var responseBody: String + protected lateinit var responseHeaders: Array + protected lateinit var responseCharset: Charset + + protected lateinit var httpRequest: okhttp3.Request + + @BeforeEach + fun setUp() { + //by default + responseCode = 200 + responseBody = "{}" + responseHeaders = arrayOf() + responseCharset = Charsets.UTF_8 + + httpClientBuilder = mock() + httpClient = mock() + call = mock() + httpResponseBody = mock() + + httpClientBuilder.stub { + this.on(httpClientBuilder.cookieJar(any())) + .doAnswer { + httpClientBuilder + } + this.on(httpClientBuilder.build()) + .doAnswer { + httpClient + } + } + httpClient.stub { + this.on(httpClient.newCall(any())) + .doAnswer { + httpRequest = it.getArgument(0) + call + } + } + call.stub { + this.on(call.execute()) + .doAnswer { + Response.Builder() + .request(okhttp3.Request.Builder().url("https://www.apache.org/licenses/LICENSE-2.0").build()) + .protocol(okhttp3.Protocol.HTTP_1_1) + .code(responseCode) + .message("ok") + .headers(Headers.headersOf(*responseHeaders)) + .body(httpResponseBody) + .build() + } + } + httpResponseBody.stub { + this.on(httpResponseBody.bytes()) + .doAnswer { responseBody.encodeToByteArray() } + this.on(httpResponseBody.string()) + .doAnswer { responseBody } + this.on(httpResponseBody.byteStream()) + .doAnswer { responseBody.byteInputStream() } + this.on(httpResponseBody.charStream()) + .doAnswer { responseBody.reader() } + this.on(httpResponseBody.contentLength()) + .doAnswer { responseBody.encodeToByteArray().size.toLong() } + this.on(httpResponseBody.contentType()) + .doAnswer { + (Headers.headersOf(*responseHeaders)["Content-type"] + ?: ContentType.APPLICATION_JSON.toString()).toMediaType() + } + } + } +} + +open class CallTest : AbstractCallTest() { + + @Test + fun testCallPostJson() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type", "application/json;charset=UTF-8", + "x-token", "123", "x-token", "987", + "Content-Disposition", "attachment; filename=\"test.json\"" + ) + + val httpClient = OkHttpClient(this.httpClientBuilder) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .param("hello", "hello") + .contentType("application/json") + .body("hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertTrue("okhttp3.RequestBody" in this.httpRequest.body!!::class.toString()) + + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertEquals("test.json", httpResponse.getHeaderFileName()) + httpResponse.close() + } + + @Test + fun testCallPostFormData() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type", "application/json;charset=UTF-8", + "x-token", "123", + "x-token", "987", + "Content-Disposition", "attachment; filename=\"\"" + ) + + val httpClient = OkHttpClient(this.httpClientBuilder) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.MULTIPART_FORM_DATA) + .param("hello", "hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertEquals>(okhttp3.MultipartBody::class, this.httpRequest.body!!::class) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertNull(httpResponse.getHeaderFileName()) + httpResponse.close() + + } + + @Test + fun testCallPostUrlencoded() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type", "application/json;charset=UTF-8", + "x-token", "123", "x-token", "987" + ) + + val httpClient = OkHttpClient(this.httpClientBuilder) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.APPLICATION_FORM_URLENCODED) + .param("hello", "hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertEquals>(okhttp3.FormBody::class, this.httpRequest.body!!::class) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json;charset=UTF-8", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertNull(httpResponse.getHeaderFileName()) + httpResponse.close() + + } + + @Test + fun testCallPostBodyOverForm() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf( + "Content-type", "application/json", + "x-token", "123", "x-token", "987", + "Content-Disposition", "attachment; filename=\"test.json\"" + ) + + val httpClient = OkHttpClient(this.httpClientBuilder) + val httpRequest = httpClient + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.MULTIPART_FORM_DATA) + .param("hello", "hello") + .body("hello") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertEquals>(okhttp3.MultipartBody::class, this.httpRequest.body!!::class) + + assertEquals(200, httpResponse.code()) + assertEquals("ok", httpResponse.string()) + assertEquals("ok", httpResponse.stream().readString(Charsets.UTF_8)) + + assertEquals(true, httpResponse.containsHeader("Content-type")) + assertEquals(false, httpResponse.containsHeader("y-token")) + assertEquals("application/json", httpResponse.contentType()) + assertEquals("123", httpResponse.firstHeader("x-token")) + assertEquals("987", httpResponse.lastHeader("x-token")) + assertArrayEquals(arrayOf("123", "987"), httpResponse.headers("x-token")) + assertEquals("test.json", httpResponse.getHeaderFileName()) + httpResponse.close() + + } + + @Test + fun testUrlWithOutQuestionMark() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf("Content-type", "application/json;charset=UTF-8") + + val httpClient = OkHttpClient(this.httpClientBuilder) + val httpRequest = httpClient + .get("https://www.apache.org/licenses/LICENSE-2.0") + .query("x", "1") + .query("y", "2") + .contentType("application/json") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertFalse(this.httpRequest.body is HttpEntityEnclosingRequest) + assertEquals("https://www.apache.org/licenses/LICENSE-2.0?x=1&y=2", this.httpRequest.url.toString()) + httpResponse.close() + + } + + @Test + fun testUrlWithQuestionMark() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf("Content-type", "application/json;charset=UTF-8") + + val httpClient = OkHttpClient(this.httpClientBuilder) + val httpRequest = httpClient + .get("https://www.apache.org/licenses/LICENSE-2.0?x=1") + .query("y", "2") + .contentType("application/json") + val httpResponse = httpRequest + .call() + assertSame(httpRequest, httpResponse.request()) + + assertFalse(this.httpRequest.body is HttpEntityEnclosingRequest) + assertEquals("https://www.apache.org/licenses/LICENSE-2.0?x=1&y=2", this.httpRequest.url.toString()) + httpResponse.close() + + } +} + +class PostFileTest : AbstractCallTest() { + + @JvmField + @TempDir + var tempDir: Path? = null + + @Test + fun testCallPostFileFormData() { + responseCode = 200 + responseBody = "ok" + responseHeaders = arrayOf("Content-type", "application/json;charset=UTF-8") + + assertThrows { + OkHttpClient(this.httpClientBuilder) + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.MULTIPART_FORM_DATA) + .param("hello", "hello") + .fileParam("file", "${tempDir}/a.txt") + .call() + } + + FileUtils.forceMkdir(File("${tempDir}/a")) + assertThrows { + OkHttpClient(this.httpClientBuilder) + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.MULTIPART_FORM_DATA) + .param("hello", "hello") + .fileParam("file", "${tempDir}/a") + .call() + } + + val txtFile = File("${tempDir}/a/a.txt") + FileUtils.forceMkdirParent(txtFile) + FileUtils.write(txtFile, "abc") + assertThrows { + OkHttpClient(this.httpClientBuilder) + .post("https://www.apache.org/licenses/LICENSE-2.0") + .contentType(ContentType.MULTIPART_FORM_DATA) + .param("hello", "hello") + .fileParam("file", "${tempDir}/a/a.txt") + .fileParam("file", null) + .call() + } + } +} \ No newline at end of file diff --git a/plugin-adapter/build/kotlin/pluginadapterjar-classes.txt b/plugin-adapter/build/kotlin/pluginadapterjar-classes.txt deleted file mode 100644 index e69de29bb..000000000 diff --git a/plugin-adapter/build/libs/plugin-adapter.jar b/plugin-adapter/build/libs/plugin-adapter.jar deleted file mode 100644 index 6ffdb4af3..000000000 Binary files a/plugin-adapter/build/libs/plugin-adapter.jar and /dev/null differ diff --git a/plugin-adapter/build/tmp/jar/MANIFEST.MF b/plugin-adapter/build/tmp/jar/MANIFEST.MF deleted file mode 100644 index 59499bce4..000000000 --- a/plugin-adapter/build/tmp/jar/MANIFEST.MF +++ /dev/null @@ -1,2 +0,0 @@ -Manifest-Version: 1.0 -