Skip to content

Commit 79ec2cc

Browse files
committed
Issue mozilla-mobile#1325: (lib-fetch-httpurlconnection): A concept-fetch implementation using HttpURLConnection.
1 parent d11a203 commit 79ec2cc

File tree

9 files changed

+269
-30
lines changed

9 files changed

+269
-30
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,14 @@ _Supporting components with generic helper code._
162162

163163
* 🔴 [**Dataprotect**](components/lib/dataprotect/README.md) - A component using AndroidKeyStore to protect user data.
164164

165+
*[**Fetch-HttpURLConnection**](components/lib/fetch-httpurlconnection/README.md) - A [concept-fetch](concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
166+
165167
*[**JEXL**](components/lib/jexl/README.md) - Javascript Expression Language: Context-based expression parser and evaluator.
166168

167169
## Tooling
168170

171+
* 🔵 [**Fetch-Tests**](components/tooling/fetch-tests/README.md) - A generic test suite for components that implement [concept-fetch](concept/fetch/README.md).
172+
169173
* 🔵 [**Lint**](components/tooling/lint/README.md) - Custom Lint rules for the components repository.
170174

171175
# Sample apps
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# [Android Components](../../../README.md) > Libraries > Fetch-HttpURLConnection
2+
3+
A [concept-fetch](../../concept/fetch/README.md) implementation using [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html).
4+
5+
This implementation of `concept-fetch` uses [HttpURLConnection](https://developer.android.com/reference/java/net/HttpURLConnection.html) from the standard library of the Android System. Therefore this component has no third-party dependencies and is smaller than other implementations. It's intended use is for apps that have strict APK size constraints.
6+
7+
## Usage
8+
9+
### Setting up the dependency
10+
11+
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
12+
13+
```Groovy
14+
implementation "org.mozilla.components:lib-fetch-httpurlconnection:{latest-version}"
15+
```
16+
17+
### Performing requests
18+
19+
See the [concept-fetch documentation](../../concept/fetch/README.md) for generic examples of using the API of components implementing `concept-fetch`.
20+
21+
## License
22+
23+
This Source Code Form is subject to the terms of the Mozilla Public
24+
License, v. 2.0. If a copy of the MPL was not distributed with this
25+
file, You can obtain one at http://mozilla.org/MPL/2.0/
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
apply plugin: 'com.android.library'
6+
apply plugin: 'kotlin-android'
7+
8+
android {
9+
compileSdkVersion Config.compileSdkVersion
10+
11+
defaultConfig {
12+
minSdkVersion Config.minSdkVersion
13+
targetSdkVersion Config.targetSdkVersion
14+
}
15+
16+
buildTypes {
17+
release {
18+
minifyEnabled false
19+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
20+
}
21+
}
22+
}
23+
24+
dependencies {
25+
implementation Deps.kotlin_stdlib
26+
implementation Deps.kotlin_coroutines
27+
28+
implementation project(':concept-fetch')
29+
30+
testImplementation Deps.testing_junit
31+
testImplementation Deps.testing_robolectric
32+
testImplementation Deps.testing_mockito
33+
34+
testImplementation project(':tooling-fetch-tests')
35+
}
36+
37+
apply from: '../../../publish.gradle'
38+
ext.configurePublish(Config.componentsGroupId, archivesBaseName, gradle.componentDescriptions[archivesBaseName])
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- This Source Code Form is subject to the terms of the Mozilla Public
2+
- License, v. 2.0. If a copy of the MPL was not distributed with this
3+
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
4+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
5+
package="mozilla.components.lib.fetch.httpurlconnection" />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.lib.fetch.httpurlconnection
6+
7+
import mozilla.components.concept.fetch.Client
8+
import mozilla.components.concept.fetch.Headers
9+
import mozilla.components.concept.fetch.MutableHeaders
10+
import mozilla.components.concept.fetch.Request
11+
import mozilla.components.concept.fetch.Response
12+
import java.io.FileNotFoundException
13+
import java.io.IOException
14+
import java.io.InputStream
15+
import java.net.HttpURLConnection
16+
import java.net.URL
17+
import java.util.zip.GZIPInputStream
18+
19+
/**
20+
* [HttpURLConnection] implementation of [Client].
21+
*/
22+
class HttpURLConnectionClient : Client() {
23+
@Throws(IOException::class)
24+
override fun fetch(request: Request): Response {
25+
val connection = (URL(request.url).openConnection() as HttpURLConnection)
26+
27+
connection.setupWith(request)
28+
connection.addHeadersFrom(request, defaultHeaders = defaultHeaders)
29+
connection.addBodyFrom(request)
30+
31+
return connection.toResponse()
32+
}
33+
}
34+
35+
private fun HttpURLConnection.addBodyFrom(request: Request) {
36+
if (request.body == null) {
37+
return
38+
}
39+
40+
request.body?.let { body ->
41+
doOutput = true
42+
43+
body.useStream { inStream ->
44+
outputStream.use { outStream ->
45+
inStream
46+
.buffered()
47+
.copyTo(outStream)
48+
outStream.flush()
49+
}
50+
}
51+
}
52+
}
53+
54+
private fun HttpURLConnection.setupWith(request: Request) {
55+
requestMethod = request.method.name
56+
instanceFollowRedirects = request.redirect == Request.Redirect.FOLLOW
57+
58+
request.connectTimeout?.let { (timeout, unit) ->
59+
connectTimeout = unit.toMillis(timeout).toInt()
60+
}
61+
62+
request.readTimeout?.let { (timeout, unit) ->
63+
readTimeout = unit.toMillis(timeout).toInt()
64+
}
65+
}
66+
67+
private fun HttpURLConnection.addHeadersFrom(request: Request, defaultHeaders: Headers) {
68+
defaultHeaders.filter { header ->
69+
request.headers?.contains(header.name) != true
70+
}.forEach { header ->
71+
setRequestProperty(header.name, header.value)
72+
}
73+
74+
request.headers?.forEach { header ->
75+
addRequestProperty(header.name, header.value)
76+
}
77+
}
78+
79+
private fun HttpURLConnection.toResponse(): Response {
80+
return Response(
81+
url.toString(),
82+
responseCode,
83+
translateHeaders(this),
84+
createBody(this)
85+
)
86+
}
87+
88+
private fun translateHeaders(connection: HttpURLConnection): Headers {
89+
val headers = MutableHeaders()
90+
91+
var index = 0
92+
93+
while (connection.getHeaderField(index) != null) {
94+
val name = connection.getHeaderFieldKey(index)
95+
if (name == null) {
96+
index++
97+
continue
98+
}
99+
100+
val value = connection.getHeaderField(index)
101+
102+
headers.append(name, value)
103+
104+
index++
105+
}
106+
107+
return headers
108+
}
109+
110+
private fun createBody(connection: HttpURLConnection): Response.Body {
111+
val gzipped = connection.contentEncoding == "gzip"
112+
113+
withFileNotFoundExceptionIgnored {
114+
return HttpUrlConnectionBody(
115+
connection,
116+
connection.inputStream,
117+
gzipped
118+
)
119+
}
120+
121+
withFileNotFoundExceptionIgnored {
122+
return HttpUrlConnectionBody(
123+
connection,
124+
connection.errorStream,
125+
gzipped
126+
)
127+
}
128+
129+
return EmptyBody()
130+
}
131+
132+
private class EmptyBody : Response.Body("".byteInputStream())
133+
134+
private class HttpUrlConnectionBody(
135+
private val connection: HttpURLConnection,
136+
stream: InputStream,
137+
gzipped: Boolean
138+
) : Response.Body(if (gzipped) GZIPInputStream(stream) else stream) {
139+
override fun close() {
140+
super.close()
141+
142+
connection.disconnect()
143+
}
144+
}
145+
146+
private inline fun withFileNotFoundExceptionIgnored(block: () -> Unit) {
147+
try {
148+
block()
149+
} catch (e: FileNotFoundException) {
150+
// Ignore
151+
}
152+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.lib.fetch.httpurlconnection
6+
7+
import mozilla.components.concept.fetch.Client
8+
import org.junit.Assert.assertTrue
9+
import org.junit.Test
10+
11+
class HttpUrlConnectionFetchTestCases : mozilla.components.tooling.fetch.tests.FetchTestCases() {
12+
override fun createNewClient(): Client = HttpURLConnectionClient()
13+
14+
// Inherits test methods from generic test suite base class
15+
16+
@Test
17+
fun `Client instance`() {
18+
// We need at least one test case defined here so that this is recognized as test class.
19+
assertTrue(createNewClient() is HttpURLConnectionClient)
20+
}
21+
}

components/tooling/fetch-tests/src/main/java/mozilla/components/tooling/fetch/tests/FetchTestCases.kt

Lines changed: 2 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ import org.junit.Assert.fail
1919
import org.junit.Test
2020
import java.io.File
2121
import java.io.IOException
22-
import java.net.ServerSocket
23-
import java.net.Socket
2422
import java.net.SocketTimeoutException
2523
import java.util.UUID
2624
import java.util.concurrent.TimeUnit
@@ -232,7 +230,7 @@ abstract class FetchTestCases {
232230
withServerResponding(
233231
MockResponse()
234232
.setBody("Yep!")
235-
.setBodyDelay(1, TimeUnit.SECONDS)
233+
.setBodyDelay(10, TimeUnit.SECONDS)
236234
) { client ->
237235
val response = client.fetch(
238236
Request(
@@ -243,32 +241,6 @@ abstract class FetchTestCases {
243241
}
244242
}
245243

246-
@Test(expected = SocketTimeoutException::class)
247-
fun `GET (?) with connect timeout`() {
248-
var serverSocket: ServerSocket? = null
249-
var socket: Socket? = null
250-
251-
try {
252-
// Create a local server that only accepts one connection, then connect a socket to it so that it cannot
253-
// accept any more.
254-
serverSocket = ServerSocket(0, 1)
255-
socket = Socket().apply { connect(serverSocket.localSocketAddress) }
256-
257-
val client = createNewClient()
258-
val response = client.fetch(
259-
Request(
260-
url = "http://127.0.0.1:${serverSocket.localPort}",
261-
connectTimeout = Pair(1, TimeUnit.SECONDS)
262-
)
263-
)
264-
265-
fail("Expected connect timeout, but got response: ${response.status}")
266-
} finally {
267-
try { socket?.close() } catch (e: IOException) {}
268-
try { serverSocket?.close() } catch (e: IOException) {}
269-
}
270-
}
271-
272244
@Test
273245
fun `PUT (201) file upload`() {
274246
val file = File.createTempFile(UUID.randomUUID().toString(), UUID.randomUUID().toString())
@@ -402,7 +374,7 @@ abstract class FetchTestCases {
402374
server.start()
403375
server.block(createNewClient())
404376
} finally {
405-
server.shutdown()
377+
try { server.shutdown() } catch (e: IOException) {}
406378
}
407379
}
408380

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ setupProject(':support-utils', 'components/support/utils', 'A collection of gene
105105

106106
setupProject(':lib-crash', 'components/lib/crash', 'A generic crash reporter library that can report crashes to multiple services.')
107107
setupProject(':lib-dataprotect', 'components/lib/dataprotect', 'A component using AndroidKeyStore to protect user data.')
108+
setupProject(':lib-fetch-httpurlconnection', 'components/lib/fetch-httpurlconnection', 'An implementation of lib-fetch based on HttpUrlConnection')
108109
setupProject(':lib-jexl', 'components/lib/jexl', 'Javascript Expression Language: Powerful context-based expression parser and evaluator.')
109110

110111
////////////////////////////////////////////////////////////////////////////////////////////////////

0 commit comments

Comments
 (0)