picocli - parse file automatically when specified - kotlin

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
}
}

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!

I want send a filepart image with Vertx, I want replicate this case in code like Postman

PostmanExample
fun sendFileToMatch(path:String){
val client = WebClient.create(vertx);
var form = MultipartForm.create()
.binaryFileUpload("image","imageName" , path, "image/jpeg")
client.post(8888, "localhost", "/search?")
.putHeader("content-type", "multipart/form-data")
.sendMultipartForm(form) { }
}
when I run the code show bad request I have put exactly key "image" and send filepart image
TL;DR - your client code seems fine.
The only suspicious part is the path itself, as you don't specify how exactly you get it in your code, and the fact that you didn't specify how you handle response from the server: you just do {} in your example
Here is a fully working example for you to refer to, though:
val vertx = Vertx.vertx()
val router = Router.router(vertx)
router.route().handler(BodyHandler.create());
router.post("/search").handler {
val uploads: Set<FileUpload> = it.fileUploads()
uploads.forEach { upload ->
println(upload.name()) // "image"
println(upload.fileName()) // "imageName"
println(upload.size()) // 42537
}
it.response().end("OK!")
}
vertx.createHttpServer().requestHandler(router)
.listen(8888)
// We read the PNG file from /resources
val path = object {}.javaClass.getResource("5EWx9.png").path
val form = MultipartForm.create()
.binaryFileUpload("image","imageName" , path, "image/png")
val client = WebClient.create(vertx);
client.post(8888, "localhost", "/search?")
.putHeader("content-type", "multipart/form-data")
.sendMultipartForm(form) {
if (it.succeeded()) {
println(it.result().bodyAsString()) // "OK!"
}
else {
println(it.cause())
}
}
As the file to upload, I used the PostmanExample you've provided, which is a PNG image, which I put in the /resources directory of my project.

500 The operation couldn’t be completed. (PostgreSQL.DatabaseError error 1.)

While doing Ray Wenderlich tutorial "Server Side Swift with Vapor: Persisting Models" I tried to add one more parameter(param) to the class Acronyms.
import Vapor
final class Acronym: Model {
var id: Node?
var exists: Bool = false
var short: String
var long: String
var param: String
init(short: String, long: String, param: String) {
self.id = nil
self.short = short
self.long = long
self.param = param
}
init(node: Node, in context: Context) throws {
id = try node.extract("id")
short = try node.extract("short")
long = try node.extract("long")
param = try node.extract("param")
}
func makeNode(context: Context) throws -> Node {
return try Node(node: [
"id": id,
"short": short,
"long": long,
"param": param
])
}
static func prepare(_ database: Database) throws {
try database.create("acronyms") { users in
users.id()
users.string("short")
users.string("long")
users.string("param")
}
}
static func revert(_ database: Database) throws {
try database.delete("acronyms")
}
}
At first I run this code without one more parameter. And it works. But when i added one it fails.
Error: 500The operation couldn’t be completed. (PostgreSQL.DatabaseError error 1.)
My main.swift:
import Vapor
import VaporPostgreSQL
let drop = Droplet(
preparations: [Acronym.self],
providers: [VaporPostgreSQL.Provider.self]
)
drop.get("hello") { request in
return "Hello, world!"
}
drop.get("version") { req in
if let db = drop.database?.driver as? PostgreSQLDriver {
let version = try db.raw("SELECT version()")
return try JSON(node: version)
} else {
return "No db connection"
}
}
drop.get("test") { request in
var acronym = Acronym(short: "AFK", long: "Away From Keyboard", param: "One More Parametr")
try acronym.save()
return try JSON(node: Acronym.all().makeNode())
}
drop.run()
I assume you didn't revert the database. You changed the model's properties, so just write in terminal vapor run prepare --revert . That will revert your database and vapor will be able to create new parameter.
Another case when you done this
vapor run prepare --revert
and the error is still there.
You should to check table name that you create in your prepare method in your model.
static func prepare(_ database: Database) throws {
try database.create(entity) { users in
...
}
}
entity is the name of the table, as far as Vapor/Fluent thinks. By default, it's the name of the model, with an -s on the end.
For example: if you create Car model you should to name your table "cars". So Car+s
static var entity = "cars"
Another example: You have model Carwash that becomes the grammatically incorrect carwashs. So you should to name is carwashs or use entity like this.
static var entity = "carwashes"
I run into the exactly same error, in my prepare method:
public static func prepare(_ database: Database) throws {
try database.create(self.entity) { tasks in
tasks.id()
tasks.string(Identifiers.title)
}
}
I was using self to refer to my database, like this:
public static func prepare(_ database: Database) throws {
try self.database.create(self.entity) { tasks in
tasks.id()
tasks.string(Identifiers.title)
}
}
Apparently, you must access the Database instance passed in as parameter, rather than the static Database variable. Hope this will help someone.

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()
}

How to validate file upload? [Play Framework]

I have a simple bean, like that:
package models;
import play.data.validation.Constraints;
public class Upload
{
#Constraints.Required
#Constraints.MinLength(4)
#Constraints.MaxLength(40)
public String name;
#Constraints.Required
public String inputFile;
}
and form, like that:
#form(action = routes.Application.submit(), 'enctype -> "multipart/form-data") {
#inputText(
uploadForm("name"),
'_label -> "Name"
)
#inputFile(
uploadForm("inputFile"),
'_label -> "Queries"
)
}
What is the best way to validate inputFile?
Is it possible do to that with annotations?
#Required constraint does not work at all.
I want it to be selected + add some limitation on size.
make your form like:
<input type="file" name="inputFile">
In you submit method add this:
// from official documentation
public static Result submit() {
MultipartFormData body = request().body().asMultipartFormData();
FilePart file = body.getFile("inputFile");
if (inputFile != null) {
String fileName = picture.getFilename();
String contentType = picture.getContentType();
File file = picture.getFile();
// method the check size
if(!validateFileSize){
return redirect(routes.Application.index()); // error in file size
}
return ok("File uploaded");
} else {
// here comes the validation
flash("error", "Missing file");
return redirect(routes.Application.index());
}
}
Something like the following, maybe?
MultipartFormData body = request().body().asMultipartFormData();
if (!body.getFiles().isEmpty()) {
// do your work
}