Skip to content
This repository was archived by the owner on Nov 1, 2022. It is now read-only.

Commit cf26709

Browse files
committed
Issue #1012: Add new concept-fetch component.
The `concept-fetch` component contains interfaces for defining an abstract HTTP client for fetching resources.
1 parent e5e499c commit cf26709

File tree

15 files changed

+1236
-0
lines changed

15 files changed

+1236
-0
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ _API contracts and abstraction layers for browser components._
8383

8484
*[**Engine**](components/concept/engine/README.md) - Abstraction layer that allows hiding the actual browser engine implementation.
8585

86+
*[**Fetch**](components/concept/fetch/README.md) - An abstract definition of an HTTP client for fetching resources.
87+
8688
* 🔴 [**Storage**](components/concept/storage/README.md) - Abstract definition of a browser storage component.
8789

8890
* 🔴 [**Tabstray**](components/concept/tabstray/README.md) - Abstract definition of a tabs tray component.

components/concept/fetch/README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# [Android Components](../../../README.md) > Concept > Fetch
2+
3+
The `concept-fetch` component contains interfaces for defining an abstract HTTP client for fetching resources.
4+
5+
The primary use of this component is to hide the actual implementation of the HTTP client from components required to make HTTP requests. This allows apps to configure a single app-wide used client without the components enforcing a particular dependency.
6+
7+
The API and name of the component is inspired by the [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API).
8+
9+
## Usage
10+
11+
### Setting up the dependency
12+
13+
Use Gradle to download the library from [maven.mozilla.org](https://maven.mozilla.org/) ([Setup repository](../../../README.md#maven-repository)):
14+
15+
```Groovy
16+
implementation "org.mozilla.components:concept-fetch:{latest-version}"
17+
```
18+
19+
### Performing requests
20+
21+
#### Get a URL
22+
23+
```Kotlin
24+
val request = Request(url)
25+
val response = client.fetch(request)
26+
val body = response.string()
27+
```
28+
29+
A `Response` may hold references to other resources (e.g. streams). Therefore it's important to always close the `Response` object or its `Body`. This can be done by either consuming the content of the `Body` with one of the available methods or by using Kotlin's extension methods for using `Closeable` implementations (e.g. `use()`):
30+
31+
```Kotlin
32+
client.fetch(Request(url)).use { response ->
33+
val body = response.body.string()
34+
}
35+
```
36+
37+
#### Post to a URL
38+
39+
```Kotlin
40+
val request = Request(
41+
url = "...",
42+
method = Request.Method.POST,
43+
body = Request.Body.fromStream(stream))
44+
45+
client.fetch(request).use { response ->
46+
if (response.success) {
47+
// ...
48+
}
49+
}
50+
```
51+
52+
#### Github API example
53+
54+
```Kotlin
55+
val request = Request(
56+
url = "https://api.github.com/repos/mozilla-mobile/android-components/issues",
57+
headers = MutableHeaders(
58+
"User-Agent" to "AwesomeBrowser/1.0",
59+
"Accept" to "application/json; q=0.5",
60+
"Accept" to "application/vnd.github.v3+json"))
61+
62+
client.fetch(request).use { response ->
63+
val server = response.headers.get('Server')
64+
val result = response.body.string()
65+
}
66+
```
67+
68+
#### Posting a file
69+
70+
```Kotlin
71+
val file = File("README.md")
72+
73+
val request = Request(
74+
url = "https://api.github.com/markdown/raw",
75+
headers = MutableHeaders(
76+
"Content-Type", "text/x-markdown; charset=utf-8"
77+
),
78+
body = Request.Body.fromFile(file))
79+
80+
client.fetch(request).use { response ->
81+
if (request.success) {
82+
// Upload was successful!
83+
}
84+
}
85+
86+
```
87+
88+
#### Asynchronous requests
89+
90+
Client implementations are synchronous. For asynchronous requests it's recommended to wrap a client in a Coroutine with a scope the calling code is in control of:
91+
92+
```Kotlin
93+
val deferredResponse = async { client.fetch(request) }
94+
val body = deferredResponse.await().body.string()
95+
```
96+
97+
## License
98+
99+
This Source Code Form is subject to the terms of the Mozilla Public
100+
License, v. 2.0. If a copy of the MPL was not distributed with this
101+
file, You can obtain one at http://mozilla.org/MPL/2.0/
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
buildConfigField("String", "LIBRARY_VERSION", "\"" + Config.componentsVersion + "\"")
16+
}
17+
18+
buildTypes {
19+
release {
20+
minifyEnabled false
21+
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
22+
}
23+
}
24+
}
25+
26+
dependencies {
27+
implementation Deps.kotlin_stdlib
28+
implementation Deps.kotlin_coroutines
29+
30+
testImplementation Deps.testing_junit
31+
testImplementation Deps.testing_robolectric
32+
testImplementation Deps.testing_mockito
33+
testImplementation Deps.testing_mockwebserver
34+
35+
testImplementation project(':support-test')
36+
}
37+
38+
apply from: '../../../publish.gradle'
39+
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.concept.fetch" />
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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.concept.fetch
6+
7+
import java.io.IOException
8+
9+
/**
10+
* A generic [Client] for fetching resources via HTTP/s.
11+
*
12+
* Abstract base class / interface for clients implementing the `concept-fetch` component.
13+
*
14+
* The [Request]/[Response] API is inspired by the Web Fetch API:
15+
* https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
16+
*/
17+
abstract class Client {
18+
/**
19+
* Starts the process of fetching a resource from the network as described by the [Request] object.
20+
*
21+
* A [Response] may keep references to open streams. Therefore it's important to always close the [Response] or
22+
* its [Response.Body].
23+
*
24+
* Use the `use()` extension method when performing multiple operations on the [Response] object:
25+
*
26+
* ```Kotlin
27+
* client.fetch(request).use { response ->
28+
* // Use response. Resources will get released automatically at the end of the block.
29+
* }
30+
* ```
31+
*
32+
* Alternatively you can use multiple `use*()` methods on the [Response.Body] object.
33+
*
34+
* @param request The request to be executed by this [Client].
35+
* @return The [Response] returned by the server.
36+
* @throws IOException if the request could not be executed due to cancellation, a connectivity problem or a
37+
* timeout.
38+
*/
39+
@Throws(IOException::class)
40+
abstract fun fetch(request: Request): Response
41+
42+
/**
43+
* List of default headers that should be added to every request unless overridden by the headers in the request.
44+
*/
45+
protected val defaultHeaders: Headers = MutableHeaders(
46+
// Unfortunately some implementations will always send a not removable Accept header. Let's override it with
47+
// a header that accepts everything.
48+
"Accept" to "*/*",
49+
50+
// We expect all clients to implement gzip decoding transparently.
51+
"Accept-Encoding" to "gzip",
52+
53+
// Default User Agent. Clients are expected to append their own tokens if needed.
54+
"User-Agent" to "MozacFetch/${BuildConfig.LIBRARY_VERSION}",
55+
56+
// We expect all clients to support and use keep-alive by default.
57+
"Connection" to "keep-alive"
58+
)
59+
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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.concept.fetch
6+
7+
import java.lang.IllegalArgumentException
8+
9+
/**
10+
* A collection of HTTP [Headers] (immutable) of a [Request] or [Response].
11+
*/
12+
interface Headers : Iterable<Header> {
13+
/**
14+
* Returns the number of headers (key / value combinations).
15+
*/
16+
val size: Int
17+
18+
/**
19+
* Gets the [Header] at the specified [index].
20+
*/
21+
operator fun get(index: Int): Header
22+
23+
/**
24+
* Returns the last values corresponding to the specified header field name. Or null if the header does not exist.
25+
*/
26+
operator fun get(name: String): String?
27+
28+
/**
29+
* Returns the list of values corresponding to the specified header field name.
30+
*/
31+
fun getAll(name: String): List<String>
32+
33+
/**
34+
* Sets the [Header] at the specified [index].
35+
*/
36+
operator fun set(index: Int, header: Header)
37+
38+
/**
39+
* Returns true if a [Header] with the given [name] exists.
40+
*/
41+
operator fun contains(name: String): Boolean
42+
}
43+
44+
/**
45+
* Represents a [Header] containing of a [name] and [value].
46+
*/
47+
data class Header(
48+
val name: String,
49+
val value: String
50+
) {
51+
init {
52+
if (name.isEmpty()) {
53+
throw IllegalArgumentException("Header name cannot be empty")
54+
}
55+
}
56+
}
57+
58+
/**
59+
* A collection of HTTP [Headers] (mutable) of a [Request] or [Response].
60+
*/
61+
class MutableHeaders(
62+
vararg pairs: Pair<String, String>
63+
) : Headers, MutableIterable<Header> {
64+
private val headers: MutableList<Header> = pairs.map {
65+
(name, value) -> Header(name, value)
66+
}.toMutableList()
67+
68+
/**
69+
* Gets the [Header] at the specified [index].
70+
*/
71+
override fun get(index: Int): Header = headers[index]
72+
73+
/**
74+
* Returns the last value corresponding to the specified header field name. Or null if the header does not exist.
75+
*/
76+
override fun get(name: String) = headers.lastOrNull { header -> header.name == name }?.value
77+
78+
/**
79+
* Returns the list of values corresponding to the specified header field name.
80+
*/
81+
override fun getAll(name: String): List<String> = headers
82+
.filter { header -> header.name == name }
83+
.map { header -> header.value }
84+
85+
/**
86+
* Sets the [Header] at the specified [index].
87+
*/
88+
override fun set(index: Int, header: Header) {
89+
headers[index] = header
90+
}
91+
92+
/**
93+
* Returns an iterator over the headers that supports removing elements during iteration.
94+
*/
95+
override fun iterator(): MutableIterator<Header> = headers.iterator()
96+
97+
/**
98+
* Returns true if a [Header] with the given [name] exists.
99+
*/
100+
override operator fun contains(name: String): Boolean = headers.firstOrNull { it.name == name } != null
101+
102+
/**
103+
* Returns the number of headers (key / value combinations).
104+
*/
105+
override val size: Int
106+
get() = headers.size
107+
108+
/**
109+
* Append a header without removing the headers already present.
110+
*/
111+
fun append(name: String, value: String): MutableHeaders {
112+
headers.add(Header(name, value))
113+
return this
114+
}
115+
116+
/**
117+
* Set the only occurrence of the header; potentially overriding an already existing header.
118+
*/
119+
fun set(name: String, value: String): MutableHeaders {
120+
headers.forEachIndexed { index, current ->
121+
if (current.name == name) {
122+
headers[index] = Header(name, value)
123+
return this
124+
}
125+
}
126+
127+
return append(name, value)
128+
}
129+
}

0 commit comments

Comments
 (0)