Skip to content

Commit

Permalink
Merge pull request #14 from IZIVIA/fix/add-missing-headers-and-fix-po…
Browse files Browse the repository at this point in the history
…st-credentials-response
  • Loading branch information
lilgallon committed Nov 2, 2023
2 parents 493f347 + a9a5801 commit a4ea337
Show file tree
Hide file tree
Showing 18 changed files with 561 additions and 229 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -1023,6 +1023,7 @@ ij_groovy_while_on_new_line = false
ij_groovy_wrap_long_lines = false

[{*.gradle.kts,*.kt,*.kts,*.main.kts}]
ktlint_disabled_rules = no-wildcard-imports, package-name, filename
ij_kotlin_align_in_columns_case_branch = false
ij_kotlin_align_multiline_binary_operation = false
ij_kotlin_align_multiline_extends_list = false
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@

Open Charge Point Interface (OCPI) java / kotlin library

## Setup

In your `build.gradle.kts`, add:
```kts
dependencies {
implementation("com.izivia:ocpi-2-2-1:0.0.13")
implementation("com.izivia:ocpi-transport:0.0.13")

// ... other dependencies
}
```

To see all available artifacts, go to: https://central.sonatype.com/search?namespace=com.izivia&q=ocpi

## Usage

### Server (CPO or eMSP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,25 @@ import com.fasterxml.jackson.core.type.TypeReference
import com.izivia.ocpi.toolkit.modules.credentials.repositories.PlatformRepository
import com.izivia.ocpi.toolkit.modules.versions.domain.ModuleID
import com.izivia.ocpi.toolkit.transport.TransportClientBuilder
import com.izivia.ocpi.toolkit.transport.domain.HttpException
import com.izivia.ocpi.toolkit.transport.domain.HttpRequest
import com.izivia.ocpi.toolkit.transport.domain.HttpResponse
import com.izivia.ocpi.toolkit.transport.domain.HttpStatus
import com.izivia.ocpi.toolkit.transport.domain.*
import java.util.*

typealias AuthenticatedHttpRequest = HttpRequest

object Header {
const val AUTHORIZATION = "Authorization"
const val X_REQUEST_ID = "X-Request-ID"
const val X_CORRELATION_ID = "X-Correlation-ID"
const val X_TOTAL_COUNT = "X-Total-Count"
const val X_LIMIT = "X-Limit"
const val LINK = "Link"
const val CONTENT_TYPE = "Content-Type"
}

object ContentType {
const val APPLICATION_JSON = "application/json"
}

/**
* Parse body of a paginated request. The result will be stored in a SearchResult which contains all pagination
* information.
Expand All @@ -22,10 +33,10 @@ inline fun <reified T> HttpResponse.parsePaginatedBody(offset: Int): OcpiRespons
.let { parsedBody ->
OcpiResponseBody(
data = parsedBody.data?.toSearchResult(
totalCount = headers["X-Total-Count"]!!.toInt(),
limit = headers["X-Limit"]!!.toInt(),
totalCount = getHeader(Header.X_TOTAL_COUNT)!!.toInt(),
limit = getHeader(Header.X_LIMIT)!!.toInt(),
offset = offset,
nextPageUrl = headers["Link"]?.split("<")?.get(1)?.split(">")?.first()
nextPageUrl = getHeader(Header.LINK)?.split("<")?.get(1)?.split(">")?.first()
),
status_code = parsedBody.status_code,
status_message = parsedBody.status_message,
Expand Down Expand Up @@ -56,7 +67,7 @@ fun String.decodeBase64(): String = Base64.getDecoder().decode(this).decodeToStr
/**
* Creates the authorization header from the given token
*/
fun authorizationHeader(token: String): Pair<String, String> = "Authorization" to "Token ${token.encodeBase64()}"
fun authorizationHeader(token: String): Pair<String, String> = Header.AUTHORIZATION to "Token ${token.encodeBase64()}"

/**
* Creates the authorization header by taking the right token in the platform repository
Expand Down Expand Up @@ -106,27 +117,45 @@ suspend fun HttpRequest.authenticate(
fun HttpRequest.authenticate(token: String): AuthenticatedHttpRequest =
withHeaders(headers = headers.plus(authorizationHeader(token = token)))

/**
* It adds Content-Type header as "application/json" if the body is not null.
*/
private fun HttpRequest.withContentTypeHeaderIfNeeded(): HttpRequest =
withHeaders(
headers = if (body != null) {
headers.plus(Header.CONTENT_TYPE to ContentType.APPLICATION_JSON)
} else {
headers
}
)

/**
* For debugging issues, OCPI implementations are required to include unique IDs via HTTP headers in every
* request/response
* request/response.
*
* - X-Request-ID: Every request SHALL contain a unique request ID, the response to this request SHALL contain the same
* ID.
* - X-Correlation-ID: Every request/response SHALL contain a unique correlation ID, every response to this request
* SHALL contain the same ID.
*
* Moreover, for requests, Content-Type SHALL be set to application/json for any request that contains a
* message body: POST, PUT and PATCH. When no body is present, probably in a GET or DELETE, then the Content-Type
* header MAY be omitted.
*
* This method should be called when doing the first request from a client.
*
* Dev note: When the server does a request (not a response), it must keep the same X-Correlation-ID but generate a new
* X-Request-ID. So don't call this method in that case.
*/
fun HttpRequest.withDebugHeaders(): HttpRequest =
fun HttpRequest.withRequiredHeaders(
requestId: String,
correlationId: String
): HttpRequest =
withHeaders(
headers = headers
.plus("X-Request-ID" to generateUUIDv4Token())
.plus("X-Correlation-ID" to generateUUIDv4Token())
)
.plus(Header.X_REQUEST_ID to requestId)
.plus(Header.X_CORRELATION_ID to correlationId)
).withContentTypeHeaderIfNeeded()

/**
* For debugging issues, OCPI implementations are required to include unique IDs via HTTP headers in every
Expand All @@ -137,23 +166,30 @@ fun HttpRequest.withDebugHeaders(): HttpRequest =
* - X-Correlation-ID: Every request/response SHALL contain a unique correlation ID, every response to this request
* SHALL contain the same ID.
*
* Moreover, for requests, Content-Type SHALL be set to application/json for any request that contains a
* message body: POST, PUT and PATCH. When no body is present, probably in a GET or DELETE, then the Content-Type
* header MAY be omitted.
*
* This method should be called when doing the a request from a server.
*
* TODO: test debug headers with integration tests
*
* @param headers Headers of the caller. It will re-use the X-Correlation-ID header and regenerate X-Request-ID
*/
fun HttpRequest.withUpdatedDebugHeaders(headers: Map<String, String>): HttpRequest =
fun HttpRequest.withUpdatedRequiredHeaders(
headers: Map<String, String>,
generatedRequestId: String
): HttpRequest =
withHeaders(
headers = headers
.plus("X-Request-ID" to generateUUIDv4Token())
.plus(Header.X_REQUEST_ID to generatedRequestId) // it replaces existing X_REQUEST_ID header
.plus(
"X-Correlation-ID" to headers.getOrDefault(
"X-Correlation-ID",
"error - could not get X-Correlation-ID header"
)
Header.X_CORRELATION_ID to (
headers.getByNormalizedKey(Header.X_CORRELATION_ID)
?: "error - could not get ${Header.X_CORRELATION_ID} header"
)
)
)
).withContentTypeHeaderIfNeeded()

/**
* For debugging issues, OCPI implementations are required to include unique IDs via HTTP headers in every
Expand All @@ -167,24 +203,42 @@ fun HttpRequest.withUpdatedDebugHeaders(headers: Map<String, String>): HttpReque
* This method should be called when responding to a request from a client.
*/
fun HttpRequest.getDebugHeaders() = listOfNotNull(
headers["X-Request-ID"]?.let { "X-Request-ID" to it },
headers["X-Correlation-ID"]?.let { "X-Correlation-ID" to it }
getHeader(Header.X_REQUEST_ID)?.let { Header.X_REQUEST_ID to it },
getHeader(Header.X_CORRELATION_ID)?.let { Header.X_CORRELATION_ID to it }
).toMap()

/**
* Returns the value of a header by its key. The key is not case-sensitive.
*/
fun HttpRequest.getHeader(key: String): String? =
headers.getByNormalizedKey(key)

/**
* Returns the value of a header by its key. The key is not case-sensitive.
*/
fun HttpResponse.getHeader(key: String): String? =
headers.getByNormalizedKey(key)

/**
* Returns the value of a map entry by its key. The key is not case-sensitive.
*/
fun Map<String, String>.getByNormalizedKey(key: String): String? =
mapKeys { it.key.lowercase() }[key.lowercase()]

/**
* Parses authorization header from the HttpRequest
*
* @throws OcpiClientNotEnoughInformationException if the token is missing
* @throws HttpException if the authorization header is missing
*/
fun HttpRequest.parseAuthorizationHeader() = (headers["Authorization"] ?: headers["authorization"])
fun HttpRequest.parseAuthorizationHeader() = getHeader(Header.AUTHORIZATION)
?.let {
if (it.startsWith("Token ")) it
else throw OcpiClientInvalidParametersException("Unkown token format: $it")
}
?.removePrefix("Token ")
?.decodeBase64()
?: throw HttpException(HttpStatus.UNAUTHORIZED, "Authorization header missing")
?: throw HttpException(HttpStatus.UNAUTHORIZED, "${Header.AUTHORIZATION} header missing")

/**
* Throws an exception if the token is invalid. Does nothing otherwise.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ suspend fun <T> HttpRequest.httpResponse(fn: suspend () -> OcpiResponseBody<T>):
}
),
headers = getDebugHeaders()
.plus(Header.CONTENT_TYPE to ContentType.APPLICATION_JSON)
).let {
if (isPaginated) {
it.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,54 @@ import com.izivia.ocpi.toolkit.transport.domain.HttpRequest
*/
class CredentialsClient(
private val transportClient: TransportClient
): CredentialsInterface {
) : CredentialsInterface {

override suspend fun get(tokenC: String): OcpiResponseBody<Credentials> =
transportClient
.send(
HttpRequest(method = HttpMethod.GET)
.withDebugHeaders()
.withRequiredHeaders(
requestId = transportClient.generateRequestId(),
correlationId = transportClient.generateCorrelationId()
)
.authenticate(token = tokenC)
)
.parseBody()


override suspend fun post(tokenA: String, credentials: Credentials, debugHeaders: Map<String, String>): OcpiResponseBody<Credentials> =
override suspend fun post(
tokenA: String,
credentials: Credentials,
debugHeaders: Map<String, String>
): OcpiResponseBody<Credentials> =
transportClient
.send(
HttpRequest(
method = HttpMethod.POST,
body = mapper.writeValueAsString(credentials)
)
.withDebugHeaders()
.withRequiredHeaders(
requestId = transportClient.generateRequestId(),
correlationId = transportClient.generateCorrelationId()
)
.authenticate(token = tokenA)
)
.parseBody()

override suspend fun put(tokenC: String, credentials: Credentials, debugHeaders: Map<String, String>): OcpiResponseBody<Credentials> =
override suspend fun put(
tokenC: String,
credentials: Credentials,
debugHeaders: Map<String, String>
): OcpiResponseBody<Credentials> =
transportClient
.send(
HttpRequest(
method = HttpMethod.PUT,
body = mapper.writeValueAsString(credentials)
)
.withDebugHeaders()
.withRequiredHeaders(
requestId = transportClient.generateRequestId(),
correlationId = transportClient.generateCorrelationId()
)
.authenticate(token = tokenC)
)
.parseBody()
Expand All @@ -52,7 +68,10 @@ class CredentialsClient(
transportClient
.send(
HttpRequest(method = HttpMethod.DELETE)
.withDebugHeaders()
.withRequiredHeaders(
requestId = transportClient.generateRequestId(),
correlationId = transportClient.generateCorrelationId()
)
.authenticate(token = tokenC)
)
.parseBody()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,16 @@ class CredentialsServerService(
) {
val versions = transportClientBuilder
.build(credentials.url)
.send(
HttpRequest(method = HttpMethod.GET)
.withUpdatedDebugHeaders(headers = debugHeaders)
.authenticate(token = credentials.token)
)
.run {
send(
HttpRequest(method = HttpMethod.GET)
.withUpdatedRequiredHeaders(
headers = debugHeaders,
generatedRequestId = generateRequestId()
)
.authenticate(token = credentials.token)
)
}
.parseBody<OcpiResponseBody<List<Version>>>()
.let {
it.data
Expand All @@ -124,11 +129,16 @@ class CredentialsServerService(

val versionDetail = transportClientBuilder
.build(matchingVersion.url)
.send(
HttpRequest(method = HttpMethod.GET)
.withUpdatedDebugHeaders(headers = debugHeaders)
.authenticate(token = credentials.token)
)
.run {
send(
HttpRequest(method = HttpMethod.GET)
.withUpdatedRequiredHeaders(
headers = debugHeaders,
generatedRequestId = generateRequestId()
)
.authenticate(token = credentials.token)
)
}
.parseBody<OcpiResponseBody<VersionDetails>>()
.let {
it.data
Expand Down
Loading

0 comments on commit a4ea337

Please sign in to comment.