2019-03-02 11:25:10 +01:00
|
|
|
import axios from 'axios'
|
2020-09-05 22:35:52 +02:00
|
|
|
import {objectToSnakeCase} from '@/helpers/case'
|
2021-07-09 20:10:57 +02:00
|
|
|
import {getToken} from '@/helpers/auth'
|
2019-03-02 11:25:10 +01:00
|
|
|
|
|
|
|
export default class AbstractService {
|
|
|
|
|
|
|
|
/////////////////////////////
|
|
|
|
// Initial variable definitions
|
|
|
|
///////////////////////////
|
|
|
|
|
|
|
|
http = null
|
|
|
|
loading = false
|
2020-08-02 19:17:29 +02:00
|
|
|
uploadProgress = 0
|
2019-03-02 11:25:10 +01:00
|
|
|
paths = {
|
|
|
|
create: '',
|
|
|
|
get: '',
|
|
|
|
getAll: '',
|
|
|
|
update: '',
|
|
|
|
delete: '',
|
|
|
|
}
|
2019-12-03 19:09:12 +01:00
|
|
|
// This contains the total number of pages and the number of results for the current page
|
|
|
|
totalPages = 0
|
|
|
|
resultCount = 0
|
2019-03-02 11:25:10 +01:00
|
|
|
|
|
|
|
/////////////
|
|
|
|
// Service init
|
|
|
|
///////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The abstract constructor.
|
|
|
|
* @param paths An object with all paths. Default values are specified above.
|
|
|
|
*/
|
|
|
|
constructor(paths) {
|
|
|
|
this.http = axios.create({
|
2020-05-05 22:44:58 +02:00
|
|
|
baseURL: window.API_URL,
|
2019-03-02 11:25:10 +01:00
|
|
|
headers: {
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
2019-06-05 22:15:30 +02:00
|
|
|
// Set the interceptors to process every request
|
|
|
|
let self = this
|
2020-04-12 23:54:46 +02:00
|
|
|
this.http.interceptors.request.use((config) => {
|
2019-06-05 22:15:30 +02:00
|
|
|
switch (config.method) {
|
|
|
|
case 'post':
|
2020-04-12 23:54:46 +02:00
|
|
|
if (this.useUpdateInterceptor()) {
|
|
|
|
config.data = self.beforeUpdate(config.data)
|
2021-09-04 21:57:09 +02:00
|
|
|
config.data = objectToSnakeCase(config.data)
|
2020-04-12 23:54:46 +02:00
|
|
|
}
|
2019-06-05 22:15:30 +02:00
|
|
|
break
|
|
|
|
case 'put':
|
2020-04-12 23:54:46 +02:00
|
|
|
if (this.useCreateInterceptor()) {
|
|
|
|
config.data = self.beforeCreate(config.data)
|
2021-09-04 21:57:09 +02:00
|
|
|
config.data = objectToSnakeCase(config.data)
|
2020-04-12 23:54:46 +02:00
|
|
|
}
|
2019-06-05 22:15:30 +02:00
|
|
|
break
|
|
|
|
case 'delete':
|
2020-04-12 23:54:46 +02:00
|
|
|
if (this.useDeleteInterceptor()) {
|
|
|
|
config.data = self.beforeDelete(config.data)
|
2021-09-04 21:57:09 +02:00
|
|
|
config.data = objectToSnakeCase(config.data)
|
2020-04-12 23:54:46 +02:00
|
|
|
}
|
2019-06-05 22:15:30 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
return config
|
|
|
|
})
|
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
// Set the default auth header if we have a token
|
2021-07-09 20:10:57 +02:00
|
|
|
const token = getToken()
|
|
|
|
if (token !== null) {
|
|
|
|
this.http.defaults.headers.common['Authorization'] = `Bearer ${token}`
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
2020-04-12 23:54:46 +02:00
|
|
|
|
2021-08-11 21:08:18 +02:00
|
|
|
if (paths) {
|
|
|
|
this.paths = {
|
|
|
|
create: paths.create !== undefined ? paths.create : '',
|
|
|
|
get: paths.get !== undefined ? paths.get : '',
|
|
|
|
getAll: paths.getAll !== undefined ? paths.getAll : '',
|
|
|
|
update: paths.update !== undefined ? paths.update : '',
|
|
|
|
delete: paths.delete !== undefined ? paths.delete : '',
|
|
|
|
}
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
}
|
2019-11-24 14:16:24 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether or not to use the create interceptor which processes a request payload into json
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
useCreateInterceptor() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether or not to use the update interceptor which processes a request payload into json
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
useUpdateInterceptor() {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether or not to use the delete interceptor which processes a request payload into json
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
useDeleteInterceptor() {
|
|
|
|
return true
|
|
|
|
}
|
2019-12-03 19:09:12 +01:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
/////////////////
|
|
|
|
// Helper functions
|
|
|
|
///////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an object with all route parameters and their values.
|
|
|
|
* @param route
|
|
|
|
* @returns object
|
|
|
|
*/
|
2021-06-03 16:27:41 +02:00
|
|
|
getRouteReplacements(route, parameters = {}) {
|
2019-03-02 11:25:10 +01:00
|
|
|
let replace$$1 = {}
|
|
|
|
let pattern = this.getRouteParameterPattern()
|
|
|
|
pattern = new RegExp(pattern instanceof RegExp ? pattern.source : pattern, 'g')
|
|
|
|
|
|
|
|
for (let parameter; (parameter = pattern.exec(route)) !== null;) {
|
2020-08-02 19:17:29 +02:00
|
|
|
replace$$1[parameter[0]] = parameters[parameter[1]]
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
|
2020-08-02 19:17:29 +02:00
|
|
|
return replace$$1
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Holds the replacement pattern for url paths, can be overwritten by implementations.
|
|
|
|
* @return {RegExp}
|
|
|
|
*/
|
|
|
|
getRouteParameterPattern() {
|
|
|
|
return /{([^}]+)}/
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
|
|
|
|
* @param path
|
|
|
|
* @param pathparams
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
getReplacedRoute(path, pathparams) {
|
|
|
|
let replacements = this.getRouteReplacements(path, pathparams)
|
2021-10-06 22:25:06 +02:00
|
|
|
return Object.entries(replacements).reduce(
|
|
|
|
(result, [parameter, value]) => result.replace(parameter, value),
|
|
|
|
path,
|
|
|
|
)
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* setLoading is a method which sets the loading variable to true, after a timeout of 100ms.
|
|
|
|
* It has the timeout to prevent the loading indicator from showing for only a blink of an eye in the
|
|
|
|
* case the api returns a response in < 100ms.
|
|
|
|
* But because the timeout is created using setTimeout, it will still trigger even if the request is
|
|
|
|
* already finished, so we return a method to call in that case.
|
|
|
|
* @returns {Function}
|
|
|
|
*/
|
|
|
|
setLoading() {
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
this.loading = true
|
|
|
|
}, 100)
|
|
|
|
return () => {
|
|
|
|
clearTimeout(timeout)
|
|
|
|
this.loading = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////////
|
|
|
|
// Default factories
|
|
|
|
// It is possible to specify a factory for each type of request.
|
|
|
|
// This makes it possible to have different models returned from different routes.
|
|
|
|
// Specific factories for each request are completly optional, if these are not specified, the defautl factory is used.
|
|
|
|
////////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The modelFactory returns an model from an object.
|
|
|
|
* This one here is the default one, usually the service definitions for a model will override this.
|
|
|
|
* @param data
|
|
|
|
* @returns {*}
|
|
|
|
*/
|
|
|
|
modelFactory(data) {
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is the model factory for get requests.
|
|
|
|
* @param data
|
|
|
|
* @return {*}
|
|
|
|
*/
|
|
|
|
modelGetFactory(data) {
|
|
|
|
return this.modelFactory(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is the model factory for get all requests.
|
|
|
|
* @param data
|
|
|
|
* @return {*}
|
|
|
|
*/
|
|
|
|
modelGetAllFactory(data) {
|
|
|
|
return this.modelFactory(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is the model factory for create requests.
|
|
|
|
* @param data
|
|
|
|
* @return {*}
|
|
|
|
*/
|
|
|
|
modelCreateFactory(data) {
|
|
|
|
return this.modelFactory(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is the model factory for update requests.
|
|
|
|
* @param data
|
|
|
|
* @return {*}
|
|
|
|
*/
|
|
|
|
modelUpdateFactory(data) {
|
|
|
|
return this.modelFactory(data)
|
|
|
|
}
|
|
|
|
|
|
|
|
//////////////
|
|
|
|
// Preprocessors
|
|
|
|
////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default preprocessor for get requests
|
|
|
|
* @param model
|
|
|
|
* @return {*}
|
|
|
|
*/
|
|
|
|
beforeGet(model) {
|
|
|
|
return model
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default preprocessor for create requests
|
|
|
|
* @param model
|
|
|
|
* @return {*}
|
|
|
|
*/
|
|
|
|
beforeCreate(model) {
|
|
|
|
return model
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default preprocessor for update requests
|
|
|
|
* @param model
|
|
|
|
* @return {*}
|
|
|
|
*/
|
2019-06-05 19:36:32 +02:00
|
|
|
beforeUpdate(model) {
|
2019-03-02 11:25:10 +01:00
|
|
|
return model
|
2019-06-05 19:36:32 +02:00
|
|
|
}
|
2019-03-02 11:25:10 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Default preprocessor for delete requests
|
|
|
|
* @param model
|
|
|
|
* @return {*}
|
|
|
|
*/
|
|
|
|
beforeDelete(model) {
|
|
|
|
return model
|
|
|
|
}
|
|
|
|
|
|
|
|
///////////////
|
|
|
|
// Global actions
|
|
|
|
/////////////
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Performs a get request to the url specified before.
|
|
|
|
* @param model The model to use. The request path is built using the values from the model.
|
|
|
|
* @param params Optional query parameters
|
|
|
|
* @returns {Q.Promise<any>}
|
|
|
|
*/
|
|
|
|
get(model, params = {}) {
|
|
|
|
if (this.paths.get === '') {
|
2021-10-09 16:04:19 +02:00
|
|
|
throw new Error('This model is not able to get data.')
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
|
2020-01-20 22:22:32 +01:00
|
|
|
return this.getM(this.paths.get, model, params)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This is a more abstract implementation which only does a get request.
|
|
|
|
* Services which need more flexibility can use this.
|
|
|
|
* @param url
|
|
|
|
* @param model
|
|
|
|
* @param params
|
|
|
|
* @returns {Q.Promise<unknown>}
|
|
|
|
*/
|
|
|
|
getM(url, model = {}, params = {}) {
|
2019-03-02 11:25:10 +01:00
|
|
|
const cancel = this.setLoading()
|
2020-04-12 23:54:46 +02:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
model = this.beforeGet(model)
|
2020-04-12 23:54:46 +02:00
|
|
|
const finalUrl = this.getReplacedRoute(url, model)
|
|
|
|
|
2021-10-09 16:04:19 +02:00
|
|
|
return this.http.get(finalUrl, {params})
|
2019-03-02 11:25:10 +01:00
|
|
|
.then(response => {
|
2020-08-11 20:18:59 +02:00
|
|
|
const result = this.modelGetFactory(response.data)
|
|
|
|
result.maxRight = Number(response.headers['x-max-right'])
|
|
|
|
return Promise.resolve(result)
|
2019-03-02 11:25:10 +01:00
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
cancel()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-09-04 21:26:38 +02:00
|
|
|
getBlobUrl(url, method = 'GET', data = {}) {
|
|
|
|
return this.http({
|
|
|
|
url: url,
|
|
|
|
method: method,
|
|
|
|
responseType: 'blob',
|
|
|
|
data: data,
|
|
|
|
}).then(response => {
|
|
|
|
return window.URL.createObjectURL(new Blob([response.data]))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
/**
|
|
|
|
* Performs a get request to the url specified before.
|
|
|
|
* The difference between this and get() is this one is used to get a bunch of data (an array), not just a single object.
|
|
|
|
* @param model The model to use. The request path is built using the values from the model.
|
|
|
|
* @param params Optional query parameters
|
2019-12-03 19:09:12 +01:00
|
|
|
* @param page The page to get
|
2019-03-02 11:25:10 +01:00
|
|
|
* @returns {Q.Promise<any>}
|
|
|
|
*/
|
2019-12-03 19:09:12 +01:00
|
|
|
getAll(model = {}, params = {}, page = 1) {
|
2019-03-02 11:25:10 +01:00
|
|
|
if (this.paths.getAll === '') {
|
2021-10-09 16:04:19 +02:00
|
|
|
throw new Error('This model is not able to get data.')
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
|
2019-12-03 19:09:12 +01:00
|
|
|
params.page = page
|
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
const cancel = this.setLoading()
|
|
|
|
model = this.beforeGet(model)
|
2020-04-12 23:54:46 +02:00
|
|
|
const finalUrl = this.getReplacedRoute(this.paths.getAll, model)
|
|
|
|
|
|
|
|
return this.http.get(finalUrl, {params: params})
|
2019-03-02 11:25:10 +01:00
|
|
|
.then(response => {
|
2019-12-03 19:09:12 +01:00
|
|
|
this.resultCount = Number(response.headers['x-pagination-result-count'])
|
|
|
|
this.totalPages = Number(response.headers['x-pagination-total-pages'])
|
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
if (Array.isArray(response.data)) {
|
|
|
|
return Promise.resolve(response.data.map(entry => {
|
|
|
|
return this.modelGetAllFactory(entry)
|
|
|
|
}))
|
|
|
|
}
|
2020-04-12 23:54:46 +02:00
|
|
|
if (response.data === null) {
|
2020-02-14 17:45:06 +01:00
|
|
|
return Promise.resolve([])
|
|
|
|
}
|
2019-03-02 11:25:10 +01:00
|
|
|
return Promise.resolve(this.modelGetAllFactory(response.data))
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
cancel()
|
|
|
|
})
|
|
|
|
}
|
2020-04-12 23:54:46 +02:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
/**
|
|
|
|
* Performs a put request to the url specified before
|
|
|
|
* @param model
|
|
|
|
* @returns {Promise<any | never>}
|
|
|
|
*/
|
|
|
|
create(model) {
|
|
|
|
if (this.paths.create === '') {
|
2021-10-09 16:04:19 +02:00
|
|
|
throw new Error('This model is not able to create data.')
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const cancel = this.setLoading()
|
2020-04-12 23:54:46 +02:00
|
|
|
const finalUrl = this.getReplacedRoute(this.paths.create, model)
|
|
|
|
|
|
|
|
return this.http.put(finalUrl, model)
|
2019-03-02 11:25:10 +01:00
|
|
|
.then(response => {
|
2020-08-11 20:18:59 +02:00
|
|
|
const result = this.modelCreateFactory(response.data)
|
2020-09-05 22:35:52 +02:00
|
|
|
if (typeof model.maxRight !== 'undefined') {
|
2020-08-11 20:18:59 +02:00
|
|
|
result.maxRight = model.maxRight
|
|
|
|
}
|
|
|
|
return Promise.resolve(result)
|
2019-03-02 11:25:10 +01:00
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
cancel()
|
|
|
|
})
|
|
|
|
}
|
2020-04-12 23:54:46 +02:00
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
/**
|
2020-04-18 01:46:07 +02:00
|
|
|
* An abstract implementation to send post requests.
|
|
|
|
* Services can use this to implement functions to do post requests other than using the update method.
|
|
|
|
* @param url
|
2019-03-02 11:25:10 +01:00
|
|
|
* @param model
|
2020-04-18 01:46:07 +02:00
|
|
|
* @returns {Q.Promise<unknown>}
|
2019-03-02 11:25:10 +01:00
|
|
|
*/
|
2020-04-18 01:46:07 +02:00
|
|
|
post(url, model) {
|
2019-03-02 11:25:10 +01:00
|
|
|
const cancel = this.setLoading()
|
2020-04-12 23:54:46 +02:00
|
|
|
|
2020-04-18 01:46:07 +02:00
|
|
|
return this.http.post(url, model)
|
2019-03-02 11:25:10 +01:00
|
|
|
.then(response => {
|
2020-08-11 20:18:59 +02:00
|
|
|
const result = this.modelUpdateFactory(response.data)
|
2020-09-05 22:35:52 +02:00
|
|
|
if (typeof model.maxRight !== 'undefined') {
|
2020-08-11 20:18:59 +02:00
|
|
|
result.maxRight = model.maxRight
|
|
|
|
}
|
|
|
|
return Promise.resolve(result)
|
2019-03-02 11:25:10 +01:00
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
cancel()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-04-18 01:46:07 +02:00
|
|
|
/**
|
|
|
|
* Performs a post request to the update url
|
|
|
|
* @param model
|
|
|
|
* @returns {Q.Promise<any>}
|
|
|
|
*/
|
|
|
|
update(model) {
|
|
|
|
if (this.paths.update === '') {
|
2021-10-09 16:04:19 +02:00
|
|
|
throw new Error('This model is not able to update data.')
|
2020-04-18 01:46:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const finalUrl = this.getReplacedRoute(this.paths.update, model)
|
|
|
|
return this.post(finalUrl, model)
|
|
|
|
}
|
|
|
|
|
2019-03-02 11:25:10 +01:00
|
|
|
/**
|
|
|
|
* Performs a delete request to the update url
|
|
|
|
* @param model
|
|
|
|
* @returns {Q.Promise<any>}
|
|
|
|
*/
|
|
|
|
delete(model) {
|
|
|
|
if (this.paths.delete === '') {
|
2021-10-09 16:04:19 +02:00
|
|
|
throw new Error('This model is not able to delete data.')
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const cancel = this.setLoading()
|
2020-04-12 23:54:46 +02:00
|
|
|
const finalUrl = this.getReplacedRoute(this.paths.delete, model)
|
|
|
|
|
|
|
|
return this.http.delete(finalUrl, model)
|
2019-03-02 11:25:10 +01:00
|
|
|
.then(response => {
|
|
|
|
return Promise.resolve(response.data)
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
cancel()
|
|
|
|
})
|
|
|
|
}
|
2020-08-02 19:17:29 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Uploads a file to a url.
|
|
|
|
* @param url
|
|
|
|
* @param file
|
|
|
|
* @param fieldName The name of the field the file is uploaded to.
|
|
|
|
* @returns {Q.Promise<unknown>}
|
|
|
|
*/
|
|
|
|
uploadFile(url, file, fieldName) {
|
|
|
|
return this.uploadBlob(url, new Blob([file]), fieldName, file.name)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Uploads a blob to a url.
|
|
|
|
* @param url
|
|
|
|
* @param blob
|
|
|
|
* @param fieldName
|
|
|
|
* @param filename
|
|
|
|
* @returns {Q.Promise<unknown>}
|
|
|
|
*/
|
|
|
|
uploadBlob(url, blob, fieldName, filename) {
|
|
|
|
const data = new FormData()
|
|
|
|
data.append(fieldName, blob, filename)
|
|
|
|
return this.uploadFormData(url, data)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Uploads a form data object.
|
|
|
|
* @param url
|
|
|
|
* @param formData
|
|
|
|
* @returns {Q.Promise<unknown>}
|
|
|
|
*/
|
|
|
|
uploadFormData(url, formData) {
|
2021-09-04 21:26:38 +02:00
|
|
|
console.log(formData, formData._boundary)
|
|
|
|
|
2020-08-02 19:17:29 +02:00
|
|
|
const cancel = this.setLoading()
|
|
|
|
return this.http.put(
|
|
|
|
url,
|
|
|
|
formData,
|
|
|
|
{
|
|
|
|
headers: {
|
|
|
|
'Content-Type':
|
|
|
|
'multipart/form-data; boundary=' + formData._boundary,
|
|
|
|
},
|
|
|
|
onUploadProgress: progressEvent => {
|
|
|
|
this.uploadProgress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
)
|
|
|
|
.then(response => {
|
|
|
|
return Promise.resolve(this.modelCreateFactory(response.data))
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
this.uploadProgress = 0
|
|
|
|
cancel()
|
|
|
|
})
|
|
|
|
}
|
2019-03-02 11:25:10 +01:00
|
|
|
}
|