Parse map with dynamic keys with Klaxon - kotlin

I have the following JSON snippet:
{
"hd":{
"hdEnabled":true,
"defaultStreamQualitySetting":"HD720",
"streamQualitySettings":{
"SD":"SD - low quality",
"HD720":"Standard HD - 720p",
"HD1080":"Full HD - 1080p"
}
}
}
I want to parse the streamQualitySettings with Klaxon and Gson to an object called 'Option' that has a key & description so that I end of with a list of 3 options
Option(key = SD, description = "SD - low quality")
Option(key = HD720, description = "Standard HD - 720p")
Option(key = HD1080, description = "Full HD - 1080p")
How can I achieve this with Klaxon (or Gson)?
This is my code
val jsonArray = bootstrapJsonObject()
.lookup<JsonArray<JsonObject>>("hd.streamQualitySettings")
.first()
val gson = Gson()
val options = ArrayList<Option>()
jsonArray.forEach {
options.add(gson.fromJson(it.toJsonString(), Option::class.java))
}

Why are you using both gson and klaxon? If you want to use gson, then kotson is an alternative with a fluent kotlin dsl.
Here is a solution with klaxon:
fun convert(input: String): List<Option> {
val streamObj = (Parser.default().parse(StringBuilder(input)) as JsonObject)
.obj("hd")!!
.obj("streamQualitySettings")!!
return streamObj.keys.map { Option(it, streamObj.string(it)!!) }
}
Parse, then move down to the streamQualitySettings.
Get all the keys and map them to Option.

Related

Google Cloud Functions - Realtime Database Trigger - how to deserialize data JSON to POJO?

As described on the Google Cloud Functions docs, it is possible to trigger a Function based on Firebase Realtime Database events (write/create/update/delete).
The following docs sample explains how to get the delta snapshot.
public class FirebaseRtdb implements RawBackgroundFunction {
private static final Logger logger = Logger.getLogger(FirebaseRtdb.class.getName());
// Use GSON (https://github.com/google/gson) to parse JSON content.
private static final Gson gson = new Gson();
#Override
public void accept(String json, Context context) {
logger.info("Function triggered by change to: " + context.resource());
JsonObject body = gson.fromJson(json, JsonObject.class);
boolean isAdmin = false;
if (body != null && body.has("auth")) {
JsonObject authObj = body.getAsJsonObject("auth");
isAdmin = authObj.has("admin") && authObj.get("admin").getAsBoolean();
}
logger.info("Admin?: " + isAdmin);
if (body != null && body.has("delta")) {
logger.info("Delta:");
logger.info(body.get("delta").toString());
}
}
}
The sample works perfectly but the question is: How can I deserialize this delta to a POJO?
I tried:
val mObject = gson.fromJson(body.get("delta").toString(), MyCustomObject::class.java)
But I am getting:
com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was BEGIN_OBJECT
As far as I know it is because MyObject class has a List<T> field, and Firebase Database always convert Lists to Maps with integer keys.
I preferably do not want to change every List<T> to Map<Int,T>, because I have a lot of classes :(
Thanks in advance!
So, here is what I ended up doing (maybe not the best solution!):
1) Create a custom Json Deserializer for Firebase-coming lists:
class ListFirebaseDeserializer<T> : JsonDeserializer<ArrayList<T>> {
override fun deserialize(json: JsonElement?, typeOfT: Type?, context: JsonDeserializationContext?): ArrayList<T> {
val result = ArrayList<T>()
val typeOfElement = (typeOfT as ParameterizedType).actualTypeArguments[0]
json?.let {
json.asJsonObject.entrySet().forEach {
entry->
result.add(Gson().fromJson(entry.value, typeOfElement))
}
}
return result
}
}
This takes the lists that Firebase turned into maps and convert it back to actual lists.
2) Annotate every list in my POJO with #JsonAdapter(ListFirebaseDeserializer::class), for instance:
class MyCustomObject {
#JsonAdapter(ListFirebaseDeserializer::class)
var myPaymentList = ArrayList<Payment>()
}
It could be a pain if you already have lots of lists to annotate, but it is better than having to use maps instead.
Hope it helps!

picocli - parse file automatically when specified

I have a CLI with multiple sub-commands, some of the sub-commands have an optional flag -f with which an input file can be specified, e.g.
#CommandLine.Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {
#Option(names = ["-f", "--file"], description = ["Input file"])
var filename: String? = null
override fun run() {
var content = read_file(filename)
}
}
#CommandLine.Command(name = "query", description = ["Query something"])
class QueryUserCommand: Runnable {
#Option(names = ["-f", "--file"], description = ["Input file"])
var filename: String? = null
override fun run() {
var content = read_file(filename)
}
}
The input file format can be different from command to command. Ideally, I'd like to parse the file automatically if it was specified as an argument.
Also the file content can be different on each command (but will be a specific format, CSV or JSON).
For example I'd like to have something like this
data class First(val col1, val col2)
data class Second(val col1, val col2, val col3)
class CustomOption(// regular #Option parameters, targetClass=...) {
// do generic file parsing
}
#CommandLine.Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {
#CustomOption(names = ["-f", "--file"], description = ["Input file"], targetClass=First))
var content: List<First> = emptyList()
override fun run() {
// content now contains the parse file
}
}
#CommandLine.Command(name = "query", description = ["Query something"])
class QueryUserCommand: Runnable {
#CustomOption(names = ["-f", "--file"], description = ["Input file"], targetClass=Second))
var content: List<Second> = emptyList()
override fun run() {
// content now contains the parse file
}
}
Would anyone have an idea if this is possible or how to do it?
To rephrase the question: how to do additional processing of input parameters during the parsing process rather than during the command execution?
(Note that the OP did not specify why this is desirable. I assume the goal is either to leverage picocli's error reporting, or to encapsulate the parsing logic somewhere for easier testing and reuse. The OP may want to expand on the underlying goal if the solution below is not satisfactory.)
One idea is to use picocli's custom parameter processing.
It is possible to specify a IParameterConsumer for an option that will process the parameter for that option.
So, for example when the user specifies get -f somefile, the custom parameter consumer will be responsible for processing the somefile argument. An implementation can look something like this:
// java implementation, sorry I am not that fluent in Kotlin...
class FirstConsumer implements IParameterConsumer {
public void consumeParameters(Stack<String> args,
ArgSpec argSpec,
CommandSpec commandSpec) {
if (args.isEmpty()) {
throw new ParameterException(commandSpec.commandLine(),
"Missing required filename for option " +
((OptionSpec) argSpec).longestName());
}
String arg = args.pop();
First first = parseFile(new File(arg), commandSpec);
List<String> list = argSpec.getValue();
list.add(first);
}
private First parseFile(File file,
ArgSpec argSpec,
CommandSpec commandSpec) {
if (!file.isReadable()) {
throw new ParameterException(commandSpec.commandLine(),
"Cannot find or read file " + file + " for option " +
((OptionSpec) argSpec).longestName());
}
// other validation...
// parse file contents...
// finally, return the result...
return new First(...);
}
}
Once the parameter consumer classes are defined, you can use them as follows:
#Command(name = "get", description = ["Get something"])
class GetUserCommand: Runnable {
#Option(names = ["-f", "--file"], description = ["Input file"],
parameterConsumer = FirstConsumer::class))
var content: List<First> = emptyList()
override fun run() {
// content now contains the parsed file
}
}

Is it possible to combine mapAreas from ArcGISOnline in one offline map on android?

I try to create an offline map using ArcGIS Runtime SDK for Android 100.5.0. I follow preplanned workflow according the guide https://developers.arcgis.com/android/latest/guide/take-map-offline-preplanned.htm. I create mapAreas in ArcGISOnline and try to download them from device. I want to get an offline map, which contains all mapAreas together like in app maps.me(on a big map you have downloaded regions with deeper detailing), but instead I am getting an offline map made from last downloaded area. So I created mapArea "Europe" with scale world - cities and mapArea "Berlin" with scale cities - buildings (both basemaps - openstreetmaps, no feature layers) and downloaded them successfully, see 2 tpk files in a folder, but mobile_map.mmpk and package.info files only contain data related to last loaded area. Is it possible at all to get what I want, combine tpk files in one map?
My code in Kotlin:
val portal = Portal("https://www.arcgis.com/", false)
val portalItem = PortalItem(portal, itemID)
val offlineMapTask = OfflineMapTask(portalItem)
//get all of the preplanned map areas in the web map
val mapAreasFuture = offlineMapTask.preplannedMapAreasAsync
mapAreasFuture.addDoneListener {
try {
// get the list of areas
val mapAreas = mapAreasFuture.get()
val directory = getDirectory()
prepareDir(directory)
// loop through the map areas
var i = 0
for (mapArea in mapAreas) {
mapArea.loadAsync()
mapArea.addDoneLoadingListener {
val downloadJob = offlineMapTask.downloadPreplannedOfflineMap(mapArea, directory)
downloadJob.start()
downloadJob.addJobDoneListener {
i++
if (i == mapAreas.size) {
val offlineMapPackage = MobileMapPackage(path)
offlineMapPackage.addDoneLoadingListener({
if (offlineMapPackage.getLoadStatus() === LoadStatus.LOADED) {
val mobileMap = offlineMapPackage.getMaps().get(0)
myCompletionFuncToShowMap(mobileMap)
} else {
println("PACKAGING FAILED")
}
})
offlineMapPackage.loadAsync()
}
}
}
}
} catch (e: InterruptedException) {
e.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
I duplicated the question on community.esri.com and got an answer, thanks to Luke Smallwood from esri. So, yes, it is possible. The "Europe" map area has to be added as basemap layer if I set mapView.map to Berlin and also every map area has to be saved in its own directory.
Downloading areas:
val portal = Portal("https://www.arcgis.com/", false)
val portalItem = PortalItem(portal, itemID)
val offlineMapTask = OfflineMapTask(portalItem)
//get all of the preplanned map areas in the web map
val mapAreasFuture = offlineMapTask.preplannedMapAreasAsync
mapAreasFuture.addDoneListener {
try {
// get the list of areas
val mapAreas = mapAreasFuture.get()
val directory = getDirectory() //my function returns String path
// loop through the map areas
for (mapArea in mapAreas) {
mapArea.loadAsync()
mapArea.addDoneLoadingListener {
val downloadJob = offlineMapTask.downloadPreplannedOfflineMap(mapArea, directory + "/" + mapArea.portalItem.title)
downloadJob.start()
downloadJob.addJobDoneListener {
val offlineMapPackage = MobileMapPackage(directory + "/" + mapArea.portalItem.title)
offlineMapPackage.addDoneLoadingListener({
if (offlineMapPackage.getLoadStatus() != LoadStatus.LOADED) {
println("PACKAGING FAILED")
}
})
offlineMapPackage.loadAsync()
}
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
Files after downloading:
.../files/maps/Berlin/p13/129294b8-3a70-4d79-a421-24ff14cb19fc.tpk
.../files/maps/Berlin/p13/mobile_map.mmap
.../files/maps/Berlin/package.info
.../files/maps/Europa/p13/2547a985-c98f-49be-a187-5ae3b7a9da09.tpk
.../files/maps/Europa/p13/mobile_map.mmap
.../files/maps/Europa/package.info
Displaying an offline map:
val path = getDirectory() + "/Berlin"
val offlineMapPackage = MobileMapPackage(path)
offlineMapPackage.addDoneLoadingListener {
if (offlineMapPackage.getLoadStatus() == LoadStatus.LOADED && !offlineMapPackage.getMaps().isEmpty()) {
mapView.map = offlineMapPackage.getMaps().get(0)
val cache = TileCache(getDirectory() + "/Europa/p13/2547a985-c98f-49be-a187-5ae3b7a9da09.tpk")
val layer = ArcGISTiledLayer(cache)
mapView.map.basemap.baseLayers.add(layer)
mapView.map.minScale = 1.8489297737236E7
mapView.map.maxScale = 2256.994353
} else {
println("NO MAP FILES")
}
}
offlineMapPackage.loadAsync()
It is important to adjust mapView.map.minScale to include Europe levels, otherwise on device the map only allows to scale between Berlin levels. The scale levels are listed here https://www.esri.com/arcgis-blog/products/product/mapping/web-map-zoom-levels-updated/ .

Swagger-ui is not showing control documentation

I am trying to use springfox-swagger-ui for the documentation of my rest services. I had used the next basic configuration in a kotlin project:
This is my Docket class:
#Configuration
#EnableSwagger2
open class SwaggerConfig {
#Bean
open fun newsApi(): Docket {
return Docket(DocumentationType.SWAGGER_2)
.groupName("api-infos")
.apiInfo(apiInfo())
.directModelSubstitute(LocalDateTime::class.java, Date::class.java)
.select()
.paths(regex("/api.*"))
.build()
}
private fun apiInfo(): ApiInfo {
return ApiInfoBuilder()
.title("Infos REST api")
.description("Swagger test for Api ESPN")
.termsOfServiceUrl("http://en.wikipedia.org/wiki/Terms_of_service")
.contact("rodolfo.silva#globant.com")
.license("Apache License Version 2.0")
.licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
.version("1.0")
.build()
}
}
And this is my controller:
#Controller
#ProductApi(
id = "v1_browse_player",
title = "Browse Player (v1)",
description = "")
#Api(value = "controller", description = "Controllers API", produces = "application/json")
#RequestMapping("/api/infos")
class BrowsePlayerController {
#Autowired
lateinit var browsePlayerService: BrowsePlayerServiceRepresentable
#GetRequest(
path = "/v1/browse/players",
timeToLive = 300,
queries = [
QueryParameter(name = "swid", required = true),
QueryParameter(name = "uid"),
QueryParameter(name = "seeAll", type = java.lang.Boolean::class),
QueryParameter(name = "lang", required = true),
QueryParameter(name = "region", required = true),
QueryParameter(name = "version", required = true, type = Integer::class),
QueryParameter(name = "appName", required = true),
QueryParameter(name = "platform", required = true)
]
)
#ApiOperation(value = "Get the players", notes = "Returns one info for playerBrowse")
fun processBrowsePlayerRequest(transaction: Transaction, apiRequest: ApiRequest): Single<BrowsePlayerResponse?> {
val applicationContext = RequestBasedApplicationContext(apiRequest)
val standardContext = RequestBasedStandardContext(
RequestBasedVersionContext(apiRequest),
applicationContext,
RequestBasedEditionContext(apiRequest, applicationContext),
RequestBasedPlatformContext(apiRequest),
transaction
)
val swidContext = RequestBasedSWIDContext(apiRequest)
val uidContext = if (checkUIDPresent(apiRequest)) RequestBasedUIDContext(apiRequest) else null
val seeAllContext = RequestBasedSeeAllContext(apiRequest)
val requestBrowsePlayerContext = RequestBrowsePlayerContext(standardContext, swidContext, uidContext, seeAllContext, apiRequest)
return browsePlayerService.getEntitiesBrowse(requestBrowsePlayerContext)
}
private fun checkUIDPresent(apiRequest: ApiRequest): Boolean =
apiRequest.parameters["uid"] != null
}
I had used a very basic configuration, the ApiOperation, Api and RequestMapping("/api/infos") tags, also at the data class level, the next configuration:
#JsonInclude(JsonInclude.Include.NON_NULL)
data class TopBrowsePlayerHeader(val title: String, val searchURL: String?)
#ApiModel(value = "Info entity", description = "Entity class BrowsePlayerResponse")
data class BrowsePlayerResponse(
#ApiModelProperty(value = "The header of the info", required = false)
val header: TopBrowsePlayerHeader,
#ApiModelProperty(value = "The analytics node of the info", required = true)
val analytics: Analytics,
#ApiModelProperty(value = "The sections node of the info", required = true)
val sections: List<Section>)
When I load the http://localhost:8080/swagger-ui.html#/api-controller (swagger browser). I can't see my controller structure. It seems like there is a predefined endpoint which is shown:
http://localhost:8080/v2/api-docs?group=api-infos
I am not pretty familiar with this configuration. Any ideas to the correct configuration?
Thanks
Try to replace paths value with PathSelectors.any():
#Bean
open fun newsApi() : Docket {
return Docket(DocumentationType.SWAGGER_2)
.groupName("api-infos")
.apiInfo(apiInfo())
.directModelSubstitute(LocalDateTime::class.java, Date::class.java)
.select()
.paths(PathSelectors.any())
.build()
}
The default value for swagger path is /v2/api-docs.
You can change is in the application.properties with the springfox.documentation.swagger.v2.path key to whatever you want to.
?group=api-infos comes from the .groupName("api-infos") value.
If you don't want to group your APIs for any reasons (like sets of selected APIs for specific clients) then remove the .groupName(...).

Write a large Inputstream to File in Kotlin

I have a large stream of text coming back from REST web service and I would like to write it directly to file. What is the simplest way of doing this?
I have written the following function extension that WORKS. But I can't help thinking that there is a cleaner way of doing this.
Note: I was hoping to use try with resources to auto close the stream and file
fun File.copyInputStreamToFile(inputStream: InputStream) {
val buffer = ByteArray(1024)
inputStream.use { input ->
this.outputStream().use { fileOut ->
while (true) {
val length = input.read(buffer)
if (length <= 0)
break
fileOut.write(buffer, 0, length)
}
fileOut.flush()
}
}
}
You can simplify your function by using the copyTo function:
fun File.copyInputStreamToFile(inputStream: InputStream) {
this.outputStream().use { fileOut ->
inputStream.copyTo(fileOut)
}
}
My proposition is:
fun InputStream.toFile(path: String) {
File(path).outputStream().use { this.copyTo(it) }
}
without closing current stream
InputStream.toFile("/path/filename")
also, do not forget to handle exceptions, for example if write permission is denied :)
I suggest to make like this:
fun InputStream.toFile(path: String) {
use { input ->
File(path).outputStream().use { input.copyTo(it) }
}
}
and then to use like:
InputStream.toFile("/some_path_to_file")
You needs to do like this
#Throws
fun copyDataBase() {
var myInput = context.getAssets().open(DB_NAME)
var outFileName = DB_PATH + DB_NAME
var fileOut: OutputStream = FileOutputStream(outFileName)
val buffer: ByteArray = ByteArray(1024)
var length: Int? = 0
while (true) {
length = myInput.read(buffer)
if (length <= 0)
break
fileOut.write(buffer, 0, length)
}
fileOut.flush()
fileOut.close()
myInput.close()
throw IOException()
}
What appears to have worked for me is this:
fun fileCopyer(localFileA: File, localFileB: File) {
var output = localFileA.inputStream()
output.copyTo(localFileB.outputStream())
output.close()
}