Skip to content

Commit

Permalink
ISSUE-1289 Mobile APP app grid (#1347)
Browse files Browse the repository at this point in the history
  • Loading branch information
vttranlina authored Nov 29, 2024
1 parent c11d72b commit e4cdb07
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
= Ecosystem Discovery
:navtitle: Ecosystem Discovery


Allows clients to discover Linagora ecosystem services.
An example case for the Mobile APP grid in the application.

== How to use it

Clients can access the ecosystem discovery endpoint using the following URL:

```
http://${jmapBaseServerUrl}/.well-known/linagora-ecosystem
```

The JSON object response sample:
```json
{
"linShareApiUrl": "https://linshare.linagora.com/linshare/webservice",
"linToApiUrl": "https://linto.ai/demo",
"linToApiKey": "apiKey",
"twakeApiUrl": "https://api.twake.app",
"mobileApps": {
"Twake Chat": {
"logoURL": "https://twake-chat.xyz/tild3837-6430-4761-b135-303536323633__twake-chat_1.svg",
"appId": "twake-chat"
},
"Twake Drive": {
"logoURL": "https://twake-drive.xyz/tild6334-3137-4635-b331-636632306164__drive_1.svg",
"webLink": "https://tdrive.linagora.com"
}
}
}
```

== How to configure the response JSON

Provide the key-value in the `linagora-ecosystem.properties` file in the configure path.
Use `.` for nested keys, representing hierarchical configuration.
Example: `service.database.url` translates to a JSON structure:
```json
{
"service": {
"database": {
"url": "..."
}
}
}
```
Use `_` as a substitute for spaces in keys for better readability.
Example: `key_with_space` can be interpreted as `key with space` in json key.
When `app.key_with_space.apikey=1234` is set in the properties file, it will be translated to:
```json
{
"app": {
"key with space": {
"apikey": "1234"
}
}
}
```
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/tmail-backend/configure/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ This includes:
- link:rabbitmq.adoc[Additional RabbitMQ configuration]
- xref:tmail-backend/imap-extensions/imapAuthDelegationExtension.adoc[IMAP extensions]
- xref:tmail-backend/smtp-extensions/smtpAuthDelegationExtension.adoc[SMTP extensions]
- link:ecosystem-discovery.adoc[Linagora Ecosystem discovery]
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
linShareApiUrl=https://linshare.linagora.com/linshare/webservice
linToApiUrl=https://linto.ai/demo
linToApiKey=apiKey
twakeApiUrl=https://api.twake.app
twakeApiUrl=https://api.twake.app

# mobileApps.Twake_Chat.logoURL=https://twake-chat.xyz/tild3837-6430-4761-b135-303536323633__twake-chat_1.svg
# mobileApps.Twake_Chat.appId=twake-chat
# mobileApps.Twake_Drive.logoURL=https://twake-drive.xyz/tild6334-3137-4635-b331-636632306164__drive_1.svg
# mobileApps.Twake_Drive.webLink=https://tdrive.linagora.com
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@
linShareApiUrl=https://linshare.linagora.com/linshare/webservice
linToApiUrl=https://linto.ai/demo
linToApiKey=apiKey
twakeApiUrl=https://api.twake.app
twakeApiUrl=https://api.twake.app

# mobileApps.Twake_Chat.logoURL=https://twake-chat.xyz/tild3837-6430-4761-b135-303536323633__twake-chat_1.svg
# mobileApps.Twake_Chat.appId=twake-chat
# mobileApps.Twake_Drive.logoURL=https://twake-drive.xyz/tild6334-3137-4635-b331-636632306164__drive_1.svg
# mobileApps.Twake_Drive.webLink=https://tdrive.linagora.com
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,20 @@ trait LinagoraServicesDiscoveryRoutesContract {
assertThatJson(response)
.isEqualTo(
s"""{
| "linShareApiUrl": "https://linshare.linagora.com/linshare/webservice",
| "linToApiUrl": "https://linto.ai/demo",
| "linToApiKey": "apiKey",
| "twakeApiUrl": "https://api.twake.app"
| "mobileApps": {
| "Twake Chat": {
| "logoURL": "https://twake-chat.xyz/tild3837-6430-4761-b135-303536323633__twake-chat_1.svg",
| "appId": "twake-chat"
| },
| "Twake Drive": {
| "logoURL": "https://twake-drive.xyz/tild6334-3137-4635-b331-636632306164__drive_1.svg",
| "webLink": "https://tdrive.linagora.com"
| }
| },
| "linToApiUrl": "https://linto.ai/demo",
| "twakeApiUrl": "https://api.twake.app",
| "linToApiKey": "apiKey",
| "linShareApiUrl": "https://linshare.linagora.com/linshare/webservice"
|}""".stripMargin)
}

Expand All @@ -68,10 +78,20 @@ trait LinagoraServicesDiscoveryRoutesContract {
assertThatJson(response)
.isEqualTo(
s"""{
| "linShareApiUrl": "https://linshare.linagora.com/linshare/webservice",
| "linToApiUrl": "https://linto.ai/demo",
| "linToApiKey": "apiKey",
| "twakeApiUrl": "https://api.twake.app"
| "mobileApps": {
| "Twake Chat": {
| "logoURL": "https://twake-chat.xyz/tild3837-6430-4761-b135-303536323633__twake-chat_1.svg",
| "appId": "twake-chat"
| },
| "Twake Drive": {
| "logoURL": "https://twake-drive.xyz/tild6334-3137-4635-b331-636632306164__drive_1.svg",
| "webLink": "https://tdrive.linagora.com"
| }
| },
| "linToApiUrl": "https://linto.ai/demo",
| "twakeApiUrl": "https://api.twake.app",
| "linToApiKey": "apiKey",
| "linShareApiUrl": "https://linshare.linagora.com/linshare/webservice"
|}""".stripMargin)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
linShareApiUrl=https://linshare.linagora.com/linshare/webservice
linToApiUrl=https://linto.ai/demo
linToApiKey=apiKey
twakeApiUrl=https://api.twake.app
twakeApiUrl=https://api.twake.app
mobileApps.Twake_Chat.logoURL=https://twake-chat.xyz/tild3837-6430-4761-b135-303536323633__twake-chat_1.svg
mobileApps.Twake_Chat.appId=twake-chat
mobileApps.Twake_Drive.logoURL=https://twake-drive.xyz/tild6334-3137-4635-b331-636632306164__drive_1.svg
mobileApps.Twake_Drive.webLink=https://tdrive.linagora.com
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
import io.netty.handler.codec.http.HttpResponseStatus._
import io.netty.handler.codec.http.{HttpMethod, HttpResponseStatus}
import jakarta.inject.{Inject, Named}
import org.apache.commons.lang3.StringUtils
import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
import org.apache.james.jmap.core.ProblemDetails
import org.apache.james.jmap.exceptions.UnauthorizedException
Expand All @@ -18,20 +19,27 @@ import org.apache.james.jmap.json.ResponseSerializer
import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
import org.apache.james.utils.PropertiesProvider
import org.slf4j.{Logger, LoggerFactory}
import play.api.libs.json.{JsObject, JsString, Json, OWrites, Writes}
import play.api.libs.json.{JsObject, JsString, JsValue, Json}
import reactor.core.publisher.Mono
import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}

private[discovery] object Serializers {
private implicit val serviceItemsWrites: OWrites[List[LinagoraServicesDiscoveryItem]] =
(ids: List[LinagoraServicesDiscoveryItem]) => {
ids.foldLeft(JsObject.empty)((jsObject, item) => {
jsObject.+(item.key, JsString.apply(item.value))
})
}
private implicit val responseWrites: Writes[LinagoraServicesDiscoveryConfiguration] = Json.valueWrites[LinagoraServicesDiscoveryConfiguration]
private[jmap] object ServicesDiscoveryConfigurationSerializers {
private val NESTED_DELIMITER = "\\."
private val UNDERSCORE_DELIMITER = "_"

def serialize(response: LinagoraServicesDiscoveryConfiguration): String =
Json.stringify(response.services.foldLeft(Json.obj()) { (json, service) =>
insertNestedJson(json,
service.key.split(NESTED_DELIMITER).map(_.replace(UNDERSCORE_DELIMITER, StringUtils.SPACE)).toList,
JsString(service.value))
})

def serialize(response: LinagoraServicesDiscoveryConfiguration): String = Json.stringify(Json.toJson(response))
private def insertNestedJson(base: JsObject, path: List[String], value: JsValue): JsObject =
path match {
case head :: Nil => base + (head -> value)
case head :: tail => base + (head -> insertNestedJson((base \ head).asOpt[JsObject].getOrElse(Json.obj()), tail, value))
case Nil => base
}
}

class LinagoraServicesDiscoveryModule() extends AbstractModule {
Expand Down Expand Up @@ -70,10 +78,10 @@ class LinagoraServicesDiscoveryRoutes @Inject()(val servicesDiscoveryConfigurati
.flatMap(_ => response
.status(HttpResponseStatus.OK)
.header(CONTENT_TYPE, JSON_CONTENT_TYPE)
.sendString(Mono.fromCallable(() => Serializers.serialize(servicesDiscoveryConfiguration)))
.sendString(Mono.fromCallable(() => ServicesDiscoveryConfigurationSerializers.serialize(servicesDiscoveryConfiguration)))
.`then`())
.cast(classOf[Void])
.onErrorResume(_ match {
.onErrorResume {
case e: UnauthorizedException =>
LOGGER.warn("Unauthorized", e)
respondDetails(e.addHeaders(response),
Expand All @@ -82,7 +90,7 @@ class LinagoraServicesDiscoveryRoutes @Inject()(val servicesDiscoveryConfigurati
LOGGER.error("Unexpected error upon service discovering", e)
respondDetails(response,
ProblemDetails(status = INTERNAL_SERVER_ERROR, detail = e.getMessage), INTERNAL_SERVER_ERROR)
})
}

private def respondDetails(httpServerResponse: HttpServerResponse, problemDetails: ProblemDetails, statusCode: HttpResponseStatus): Mono[Void] =
Mono.fromCallable(() => ResponseSerializer.serialize(problemDetails))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.linagora.tmail.james.jmap.json

import com.linagora.tmail.james.jmap.service.discovery.{LinagoraServicesDiscoveryConfiguration, LinagoraServicesDiscoveryItem, ServicesDiscoveryConfigurationSerializers => testee}
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import play.api.libs.json.Json

class LinagoraServicesDiscoveryConfigurationSerializeTest {

@Test
def serializeShouldHandleFlatPropertiesCorrectly(): Unit = {
val input = LinagoraServicesDiscoveryConfiguration(services = List(
LinagoraServicesDiscoveryItem("linShareApiUrl", "https://linshare.linagora.com/linshare/webservice"),
LinagoraServicesDiscoveryItem("linToApiUrl", "https://linto.ai/demo"),
LinagoraServicesDiscoveryItem("linToApiKey", "apiKey")))

assertThat(Json.parse(testee.serialize(input)))
.isEqualTo(Json.parse(
"""{
| "linShareApiUrl" : "https://linshare.linagora.com/linshare/webservice",
| "linToApiUrl" : "https://linto.ai/demo",
| "linToApiKey" : "apiKey"
| }""".stripMargin))
}

@Test
def serializeShouldHandleNestedPropertiesCorrectly(): Unit = {
val input = LinagoraServicesDiscoveryConfiguration(services = List(
LinagoraServicesDiscoveryItem("mobileApps.Lin1.url1", "url1"),
LinagoraServicesDiscoveryItem("mobileApps.Lin1.url2", "url2"),
LinagoraServicesDiscoveryItem("mobileApps.Lin2.url3", "url3"),
LinagoraServicesDiscoveryItem("mobileApps.Lin4", "url4")))

assertThat(Json.parse(testee.serialize(input)))
.isEqualTo(Json.parse(
"""{
| "mobileApps": {
| "Lin1": {
| "url1": "url1",
| "url2": "url2"
| },
| "Lin2": {
| "url3": "url3"
| },
| "Lin4": "url4"
| }
|}""".stripMargin))
}

@Test
def serializeShouldTransformUnderscoreToSpaceInNestedKeys(): Unit = {
val input = LinagoraServicesDiscoveryConfiguration(services = List(
LinagoraServicesDiscoveryItem("linToApiKey", "apiKey"),
LinagoraServicesDiscoveryItem("mobileApps.Twake_Chat.logoURL", "https://xyz"),
LinagoraServicesDiscoveryItem("mobileApps.Twake_Chat.appId", "abc"),
LinagoraServicesDiscoveryItem("mobileApps.Twake_Drive.logoURL", "https://xyz"),
LinagoraServicesDiscoveryItem("mobileApps.Twake_Drive.webLink", "https://tdrive.linagora.com"),
LinagoraServicesDiscoveryItem("mobileApps.TwakeXyz.Logo_URL", "https://xyz")
))

assertThat(Json.parse(testee.serialize(input)))
.isEqualTo(Json.parse(
"""{
| "mobileApps": {
| "TwakeXyz": {
| "Logo URL": "https://xyz"
| },
| "Twake Chat": {
| "logoURL": "https://xyz",
| "appId": "abc"
| },
| "Twake Drive": {
| "logoURL": "https://xyz",
| "webLink": "https://tdrive.linagora.com"
| }
| },
| "linToApiKey": "apiKey"
|}""".stripMargin))
}

@Test
def serilizeShouldSuccessWhenEmptyConfiguration(): Unit = {
val input = LinagoraServicesDiscoveryConfiguration(services = List())

assertThat(Json.parse(testee.serialize(input)))
.isEqualTo(Json.parse("{}"))
}
}

0 comments on commit e4cdb07

Please sign in to comment.