how to use socket IO in kotlin? - socket.io

I want to initialize socket IO in my kotlin app.
my problem is here :
private var mSocket: Socket? = null
{
try {
mSocket = IO.socket("http://chat.socket.io")
} catch (URISyntaxException e) {
}
}
import com.github.nkzawa.socketio.client.IO
cant recognize

I searched for this some more and found this solution:
You connect your ws like this:
val opts = IO.Options()
opts.path = "/path/to/ws"
opts.transports = arrayOf(WebSocket.NAME) // Set the transfer to 'websocket' instead of 'polling'
val webSocket = IO.socket("http(s)://your.ip.here", opts)
webSocket.connect()
.on(Socket.EVENT_CONNECT) {
// Do your stuff here
}
.on("foo") { parameters -> // do something on recieving a 'foo' event
// 'parameters' is an Array of all parameters you sent
// Do your stuff here
}
If you want to emit an event, you'll call:
webSocket.emit("foo", "bar") // Emits a 'foo' event with 'bar' as a parameter
You will need to use
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
so be sure to add the corresponding libraries to your build.gradle
dependencies {
...
implementation 'com.github.nkzawa:socket.io-client:0.6.0'
}

first import this
import com.github.nkzawa.socketio.client.IO;
import com.github.nkzawa.socketio.client.Socket;
and then initialize this one
val socket = IO.socket("http://localhost:4000/")
socket.on(Socket.EVENT_CONNECT, Emitter.Listener {
socket.emit("messages", "hi")
});
socket.connect()

It's a static block in java But we can't wirte same as in Kotlin.
We can use its like a companion object.
companion object{
private var mSocket: Socket?=null
init {
try {
mSocket = IO.socket(Constants.Chat_URl)
}
catch (e: Exception){
throw RuntimeException(e)
}
}
}

In Kotlin you can make a Socket Client like the following. All the Exceptions are handled here too.
fun pingYourTCPServerWith(message: String): String{
try {
val socket = Socket("<YOUR IP ADDRESS>", <YOUR PORT HERE>)
socket.use {
var responseString : String? = null
it.getOutputStream().write(message.toByteArray())
val bufferReader = BufferedReader(InputStreamReader(it.inputStream))
while (true) {
val line = bufferReader.readLine() ?: break
responseString += line
if (line == "exit") break
}
println("Received: $responseString")
bufferReader.close()
it.close()
return responseString!!
}
}catch (he: UnknownHostException){
val exceptionString = "An exception occurred:\n ${he.printStackTrace()}"
return exceptionString
}catch (ioe: IOException){
val exceptionString = "An exception occurred:\n ${ioe.printStackTrace()}"
return exceptionString
} catch (ce: ConnectException){
val exceptionString = "An exception occurred:\n ${ce.printStackTrace()}"
return exceptionString
}catch (se: SocketException){
val exceptionString = "An exception occurred:\n ${se.printStackTrace()}"
return exceptionString
}
}

The right syntax is below for anyone who is interested in the future
private lateinit var mSocket:Socket
fun socket(){
try {
mSocket=IO.socket("http://host:port")
}
catch(e: URISyntaxException){
println("Exception"+e)
}
}

Related

spring-boot TCP server: wait for `empty line` before answering a `multi-line` request

I would like to write a custom check_policy_service (http://www.postfix.org/SMTPD_POLICY_README.html) for postfix with spring-boot.
Short: postfix sends multiple lines like:
foo=bar
me=you
year=123
[empty line]
The empty line indicates, that the request is complete, now the spring-boot app should handle the data and return something like action=ok
Problem:
In my current setup the app tries to handle line 1 foo=bar immediately and does not wait until the [empty line] is sent. How can I make the app wait for the empty line?
Current setup:
spring-boot 2.6.3
#Configuration
class TcpServerConfig {
// #Value("\${tcp.server.port}")
private val port = 6676
#Bean
fun serverConnectionFactory(): AbstractServerConnectionFactory {
val serverConnectionFactory = TcpNioServerConnectionFactory(port)
serverConnectionFactory.setUsingDirectBuffers(true)
serverConnectionFactory.isSingleUse = false // reuse socket
return serverConnectionFactory
}
#Bean
fun inboundChannel(): MessageChannel {
return DirectChannel()
}
#Bean
fun inboundGateway(
serverConnectionFactory: AbstractServerConnectionFactory,
inboundChannel: MessageChannel
): TcpInboundGateway {
val tcpInboundGateway = TcpInboundGateway()
tcpInboundGateway.setConnectionFactory(serverConnectionFactory)
tcpInboundGateway.setRequestChannel(inboundChannel)
return tcpInboundGateway
}
}
#MessageEndpoint
class TcpServerEndpoint {
#ServiceActivator(inputChannel = "inboundChannel")
fun process(message: ByteArray): ByteArray {
// I would need to have `message` contain every line until empty line - not a single line
println(String(message))
// TODO handle message accordingly
return "action=ok\n".toByteArray()
}
}
I am not sure where to hook in. Maybe DirectChannel needs to be something else?
I wrote a quick non spring-boot implementation which works, so basically I need the spring-boot-ified version of:
fun main(args: Array<String>) {
val server = ServerSocket(9999)
println("Server is running on port ${server.localPort}")
while (true) {
val client = server.accept()
println("Client connected: ${client.inetAddress.hostAddress}")
// Run client in it's own thread.
thread { ClientHandler(client).run() }
}
}
class ClientHandler(client: Socket) {
private val client: Socket = client
private val reader: BufferedReader = BufferedReader(InputStreamReader(client.getInputStream()))
private val writer: PrintWriter = PrintWriter(client.getOutputStream(), true)
private var running: Boolean = false
fun run() {
running = true
while (running) {
try {
do {
val line = reader.readLine() ?: break
// TODO collect all lines in a list and handle
} while (line.isNotEmpty())
write("action=ok")
} catch (ex: Exception) {
// TODO: Implement exception handling
ex.printStackTrace()
shutdown()
} finally {
}
}
}
private fun write(message: String) {
writer.write((message + "\n\n"))
writer.flush()
}
private fun shutdown() {
running = false
client.close()
println("${client.inetAddress.hostAddress} closed the connection")
}
}
The magic part is read all lines until line is empty and then handle the request(s)
You need to set the Deserializer so that Spring knows when to release the read content (so far) to the handler as described in the docs.
In your case, you should write a custom deserializer implementing the interface org.springframework.core.serializer.Deserializer that releases the content to the handler as soon as it has detected two newlines in a row. Make it return a List<String> with the lines sent by Postfix:
class DoubleNewLineDeserializer: Deserializer<List<String>> {
companion object {
const val UNIX_NEWLINE = "\n\n"
const val WINDOWS_NEWLINE = "\r\n\r\n"
val CRLFRegex = Regex("[\r\n]+")
}
override fun deserialize(inputStream: InputStream): List<String> {
val builder = StringBuilder()
while(!builder.endsWith(UNIX_NEWLINE) && !builder.endsWith(WINDOWS_NEWLINE))
builder.append(inputStream.read().toChar())
return builder.toString().split(CRLFRegex).filter { it.isNotEmpty() }
}
}
This deserializer reads input until it finds a double new line (here either \n\n or \r\n\r\n, you can modify this as you want) at which point it removes the message delimiter and then returns the message as a list of lines.
Finally, set your new deserializer on the ServerConnectionFactory:
serverConnectionFactory.deserializer = DoubleNewLineDeserializer()
Input to TCP socket:
foo=bar
me=you
year=123
[empty line]
[empty line]
Input to handler:
[foo=bar, me=you, year=123]

GraphQL.ExecutionError: Error trying to resolve

Summary:
My GraphQL ExecuteAsync returns a result that contains. According to the stackTrace provided below, the system cannot resolve my custom type remitsGeneralSearch. The remitsGeneralSearch resolver can return a type called ClaimPaymentOrCheckSearchGraphType which is a UnionGraphType.
StackTrace:
["GraphQL.ExecutionError: Error trying to resolve remitsGeneralSearch.\n ---> System.InvalidOperationException: Unexpected type: \n at GraphQL.Execution.ExecutionStrategy.BuildExecutionNode(ExecutionNode parent, IGraphType graphType, Field field, FieldType fieldDefinition, String[] path)\n at GraphQL.Execution.ExecutionStrategy.SetSubFieldNodes(ExecutionContext context, ObjectExecutionNode parent, Dictionary`2 fields)\n at GraphQL.Execution.ExecutionStrategy.SetSubFieldNodes(ExecutionContext context, ObjectExecutionNode parent)\n at GraphQL.Execution.ExecutionStrategy.ExecuteNodeAsync(ExecutionContext context, ExecutionNode node)\n --- End of inner exception stack trace ---"]4008305)
GraphQL Version: 2.4.0
FrameWork: .Net
OS: MacOS Catalina
Links Referenced: https://github.com/graphql-dotnet/graphql-dotnet/issues/964
CODE SNIPPETS:
RESOLVER:
FieldAsync<ClaimPaymentOrCheckSearchGraphType>(
"remitsGeneralSearch",
resolve: async context =>
{
var securityFilter = await GetUserRemitFilters(context);
var range = context.GetRange();
var sortFields = context.GetArgument<List<SortField>>("sort") ?? Enumerable.Empty<SortField>();
var whereClaimPayment = context.GetArgument<ClaimPaymentSearchFilter>("whereClaimPayment");
Connection<ClaimPaymentSearchRow> claimPaymentSearchRowResult;
try
{
using (LogContext.PushProperty("where", whereClaimPayment, true))
{
//claimPaymentSearchRowResult = await DMAQueryService.GetRemitReadersAsync(context);
var whereArguments = context.Arguments["whereClaimPayment"] as Dictionary<string, object>;
claimPaymentSearchRowResult = await DMAQueryService.GetRemitReadersAsync(
range,
whereClaimPayment,
whereArguments,
sortFields,
securityFilter,
context.CancellationToken
);
}
}
catch (Exception e)
{
_logger.LogInformation("Exception occurred {e}", e);
throw e;
}
var userRemitFilters = context.UserContext as Services.DMA.UserRemitFilters;
if (claimPaymentSearchRowResult.EdgeCount > 0)
{
return claimPaymentSearchRowResult;
}
var _whereCheckSearch = context.GetArgument<CheckSearchFilter>("whereCheck");
try
{
Connection<CheckSearchRow> checkSearchRowResult;
using (LogContext.PushProperty("whereCheck", _whereCheckSearch, true))
{
checkSearchRowResult = await DMAQueryService.GetCheckReadersAsync(context);
return checkSearchRowResult;
}
}
catch (Exception e)
{
throw e;
}
},arguments: queryArguments
);
}
catch (Exception e)
{
throw e;
}
Custom GraphType:
[Transient]
public class ClaimPaymentOrCheckSearchGraphType : UnionGraphType
{
private readonly ILogger<ClaimPaymentOrCheckSearchGraphType> _logger;
public ClaimPaymentOrCheckSearchGraphType(
ILogger<ClaimPaymentOrCheckSearchGraphType> logger,
ConnectionGraphType<ClaimPaymentSearchGraphType> claimPaymentSearchGraphType,
ConnectionGraphType<CheckSearchGraphType> checkSearchGraphType
)
{
_logger = logger;
Type<ConnectionGraphType<ClaimPaymentSearchGraphType>>();
Type<ConnectionGraphType<CheckSearchGraphType>>();
ResolveType = obj =>
{
try
{
if (obj is Connection<ClaimPaymentSearchRow>)
{
return claimPaymentSearchGraphType;
}
if (obj is Connection<CheckSearchRow>)
{
return checkSearchGraphType;
}
throw new ArgumentOutOfRangeException($"Could not resolve graph type for {obj.GetType().Name}");
}
catch (Exception e)
{
_logger.LogInformation("ClaimPaymentOrCheckSearchGraphType Exception {e}: ", e);
throw e;
}
};
}
}
Link to answer found here: https://github.com/graphql-dotnet/graphql-dotnet/issues/2674Try replacing this:
Type<ConnectionGraphType<ClaimPaymentSearchGraphType>>();
Type<ConnectionGraphType<CheckSearchGraphType>>();
with this:
AddPossibleType(claimPaymentSearchGraphType);
AddPossibleType(checkSearchGraphType);
I'm thinking that if you're registering these types as transients, then the copy that gets registered to the schema initialization code is a different copy that gets returned from ResolveType. Because of that, its fields' ResolvedType properties was never set, and so null is passed into BuildExecutionNode for the graphType instead of a resolved type.
If the ResolveType method could return a type rather than an instance, there wouldn't be an issue, but unfortunately that's not the way it works. Or you could register the type as a singleton.

Validate Request in Ktor

I have an API maked with Ktor and when som field of the request failed, it returns 500 error and I want to check all request data and return, in this case, 422.
Request class:
#Serializable
data class LoginRequest (
val email: String,
val password: String
)
Routing
route("v1/auth/login") {
post {
val loginRequest = call.receive<LoginRequest>()
//LOGIN METHOD
}
}
The error that now Ktor shows is:
[eventLoopGroupProxy-4-1] ERROR Application - Unhandled: POST - /v1/auth/login
kotlinx.serialization.MissingFieldException: Field 'password' is required for type with serial name
What is the best way to ensure that the system does not fail and respond with a BadRequest?
If you wanna catch an exception in a specific place, you can use try/catch:
try {
val loginRequest = call.receive<LoginRequest>()
...
} catch (e: SerializationException) {
// serialization exceptions
call.respond(HttpStatusCode.UnprocessableEntity)
} catch (t: Throwable) {
// other exceptions
call.respond(HttpStatusCode.InternalServerError)
}
If you wanna some global try/catch, Ktor has StatusPages feature for such case: it'll catch all exceptions during calls processing.
Same as with try/catch, you can catch a specific exception, like SerializationException, or use Exception/Throwable for any other exception.
install(StatusPages) {
exception<SerializationException> { cause ->
// serialization exceptions
call.respond(HttpStatusCode.UnprocessableEntity)
}
exception<Throwable> { cause ->
// other exceptions
call.respond(HttpStatusCode.InternalServerError)
}
}
You can make fields nullable with the default null value, ignore errors when unknown properties are encountered and validate the result object manually. Here is an example:
import io.ktor.application.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.serialization.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
#Serializable
data class LoginRequest (
val email: String? = null,
val password: String? = null
)
suspend fun main() {
embeddedServer(Netty, port = 8080) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
})
}
routing {
post("/") {
val request = call.receive<LoginRequest>()
if (request.email == null || request.password == null) {
call.respond(HttpStatusCode.UnprocessableEntity)
return#post
}
call.respond(HttpStatusCode.OK)
}
}
}.start()
}
post {
try {
val customer = call.receive<Customer>()
customerStorage.add(customer)
call.respondText("Customer stored correctly", status = HttpStatusCode.Created)
} catch (e: SerializationException) {
call.respondText(e.localizedMessage, status = HttpStatusCode.UnprocessableEntity)
} catch (e: Exception) {
call.respondText(e.localizedMessage, status = HttpStatusCode.InternalServerError)
}
}

How to convert ByteReadChannel into Flow<ByteBuffer>

How can I convert io.ktor.utils.io.ByteReadChannel into kotlinx.coroutines.flow.Flow<java.nio.ByteBuffer>?
I use Ktor with this routing:
post("/upload") {
val channel: ByteReadChannel = call.receiveChannel()
val flow: Flow<ByteBuffer> = channel.asByteBufferFlow() // my custom extension method
transaction.execute {
testDao.saveFile(flow)
}
call.respond("OK")
}
The DAO uses R2DBC and Blob like this:
override suspend fun saveFile(input: Flow<ByteBuffer>) {
val connection = requireR2DBCTransactionConnection()
val publisher: Publisher<ByteBuffer> = input.asPublisher()
val statement: Statement = connection.createStatement("insert into bindata (data) values ($1)")
statement.bind(0, Blob.from(publisher))
val count: Int = statement.execute().awaitFirst().rowsUpdated.awaitFirst()
if (count != 1) {
throw IllegalStateException()
}
}
I tried to write this extension method but I failed:
fun ByteReadChannel.asByteBufferFlow(): Flow<ByteBuffer> = object : AbstractFlow<ByteBuffer>() {
override suspend fun collectSafely(collector: FlowCollector<ByteBuffer>) {
/* I have no idea */
}
}
My main problem is that I have not found any similar sample and both ByteBuffer and ByteReadChannel is new for me.

Trapping errors in Aurelias HTTP client

Hi All (Especially the Aurelia core team hanging about round here)
I have an aurelia app using the "aurelia-http-client" to make requests to my back end API.
My back end API is a C# based service running on Nancy.
In my front end Iv'e abstracted the http client out to my own network lib as follows:
import { inject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
import { HttpClient } from 'aurelia-http-client';
import environment from './environment';
#inject(HttpClient, Router)
export default class httpservice {
private http: HttpClient = null;
private router: Router = null;
private authService: any = null;
private authToken: string = "";
constructor(HttpClient, Router) {
this.http = HttpClient;
this.router = Router;
HttpClient.configure(http => {
http.withBaseUrl(environment.servicebase);
});
}
public setAuthService(authService: any) {
this.authService = authService;
}
public get(url: string, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let self = this;
let client = this.http
.createRequest(url)
.asGet()
.withHeader("AuthenticationToken", myToken)
.withInterceptor({
responseError(responseError) {
console.log(responseError);
if (responseError.statusCode === 401) {
if (myAuth) {
myAuth.destroySession();
}
}
if (responseError.statusCode === 404) {
self.router.navigateToRoute("missing");
}
return responseError;
}
});
return client;
}
public post(url: string, postData: any, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let self = this;
let client = this.http
.createRequest(url)
.asPost().withContent(postData)
.withHeader("AuthenticationToken", myToken)
.withInterceptor({
responseError(responseError) {
console.log(responseError);
if (responseError.statusCode === 401) {
if (myAuth) {
myAuth.destroySession();
}
}
if (responseError.statusCode === 404) {
self.router.navigateToRoute("missing");
}
return responseError;
}
});
return client;
}
}
and I then use this in my other modules/classes as follows:
import { Aurelia, inject } from 'aurelia-framework';
import HttpService from './httpservice';
import environment from './environment';
import { EventAggregator } from 'aurelia-event-aggregator';
#inject(EventAggregator, Aurelia, HttpService)
export default class Authservice {
public http: HttpService = null;
public app: Aurelia = null;
public ea: EventAggregator = null;
public authToken: any = null;
private loginUrl: string = "";
private logoutUrl: string = "";
private checkUrl: string = "";
constructor(eventAggregator, aurelia, httpService) {
this.http = httpService;
this.app = aurelia;
this.ea = eventAggregator;
this.loginUrl = "/login";
}
public getAuthToken() {
if (!sessionStorage[environment.tokenname] ||
(sessionStorage[environment.tokenname] == null)) {
return null;
}
return sessionStorage[environment.tokenname];
}
public login(loginName, password) {
let postData = {
loginName: loginName,
password: password
};
let client = this.http.post(this.loginUrl, postData);
client.send()
.then((response) => response.content)
.then((data) => {
if (data.error) {
this.ea.publish("loginMessage", { message: data.errorMessage });
return;
}
if (data.authenticationFailed) {
this.ea.publish("loginMessage", { message: "Invalid user name and/or password supplied." });
return;
}
if (data.accountSuspended) {
this.ea.publish("loginMessage", { message: "Your account has been suspended, please contact support." });
return;
}
sessionStorage[environment.tokenname] = data.token;
sessionStorage["displayedLoginName"] = data.displayName;
location.assign('#/');
this.app.setRoot('app');
})
.catch(() =>
{
debugger;
alert("Something bad happened trying to connect to server.");
});
}
public isAuthenticated() {
// TODO: hook this up to check auth token validity via rest call???
let token = this.getAuthToken();
return token !== null;
}
}
enum LoginStates {
LoginValid = 0,
BadUserNameOrPassword,
AccountSuspended
}
Please note I've stripped some of the code out of the auth library to reduce confusion
In general ALL of this works well. The interceptors get triggered when 401s and 404s occur, and if I add a 500 that get's handled too, so where all good there.
The problem I have is handling communication failures.
As you can see in the login routine, I have a catch following the then.
I expected that if the server couldn't be reached or some other base communications failure occurred, that this catch would trigger rather than the "then" and thus allow me to handle the error, but instead it does not.
What I get instead is this in the console:
Worse still, my login routine doesn't abort, it actually succeeds and allows the logged in page to be shown.
It seems that while the library is making the OPTIONS call (Which is when this error occurs) none of my user code is taken into account.
The OPTIONS call is required for successful pre-flight/ajax requests, so stopping this happening is not an option, and I feel that if the OPTIONS call did not abort, but made it to the POST call,t hat my error handling would then be taken into consideration.
It seems silly to be not able to trap errors like this, especially in today's mobile world where a device may be out of coverage or temporarily offline.
If anyone has any thoughts on how this can be solved, I'd love to hear them.
Update 1
My problem seems to be related to this one:
aurelia-fetch-client - a promise was rejected with a non-error: [object Response]
However, I'm not using "useStandardConfiguration()" which is apparently the cause for that case. I'm also not using the fetch client, however I do note that the API in both clients is practically the same, so I wonder if the underlying code is also similar.
Ok.... so, after a long hard afternoon of head scratching and hair pulling, it turns out, the whole thing is actually linked to a reported issue with the "BlueBird promises library" which is what aurelia uses to manage it's promises.
The link to the issue with BlueBird can be found here:
https://github.com/petkaantonov/bluebird/issues/990
It's not specifically an issue according to the BB dev's but to many folks encountering it, it sure looks like one.
The bottom line is that the library is not designed to throw the errors generated directly by it (As the example on the issue page shows)
The correct way according to the BB team, is to either throw a new error completely, or derive a new instance from the one passed to the promise, and alter the parameters to it before then re-throwing it.
Of course, because of the abstraction in Aurelia, this is not an option for most of us, unless we want to go about changing the http client library code.
Some of the marks for this need to go to "TheBlueFox" for His/Her comments above.
The solution ended up being something like the following:
import { inject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
import { HttpClient, Interceptor } from 'aurelia-http-client';
import environment from './environment';
import Debugger = require("_debugger");
#inject(HttpClient, Router)
export default class httpservice {
private http: HttpClient = null;
private router: Router = null;
private authService: any = null;
private authToken: string = "";
private myInterceptors: Interceptor;
constructor(HttpClient, Router) {
this.http = HttpClient;
this.router = Router;
HttpClient.configure(http => {
http.withBaseUrl(environment.servicebase);
http.withInterceptor(new HttpInterceptors());
});
}
public setAuthService(authService: any) {
this.authService = authService;
}
public get(url: string, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let client = this.http
.createRequest(url)
.asGet()
.withHeader("AuthenticationToken", myToken);
return client;
}
public post(url: string, postData: any, authObject?: any): any {
let myAuth = this.authService ? this.authService : authObject;
let myToken = "";
if (myAuth) {
myToken = myAuth.getAuthToken();
}
let self = this;
let client = this.http
.createRequest(url)
.asPost().withContent(postData)
.withHeader("AuthenticationToken", myToken);
return client;
}
}
class HttpInterceptors implements Interceptor {
responceError(error)
{
if (error.statusCode === 0) {
throw new Error("Could not contact server");
}
if (error.statusCode === 401) {
// do auth handling here
}
if (error.statusCode === 404) {
// do 404 handling here
}
return error;
}
}
The magic is in the HttpInterceptors class attached to the bottom of my HttpService. You should be able to see a check for a status code of 0, and that the actual action performed here is to throw a new error.
It's the action of this new error being thrown that then causes the "catch" in the actual call to the http client to be caught.
If you don't throw at that point, then everything just falls apart and you get the scenario seen in my original question post, throw and you get to catch it and deal with it in user code.
This way of doing things is also apparent in the aurelia-fetch-client too, as that works in a broadly similar way, using the BlueBird promise library.

Resources