1 | ;
|
2 |
|
3 | var path = require('path');
|
4 | var util = require('util');
|
5 | var async = require('async');
|
6 | var fileSystem = process.requireApi('lib/fileSystem.js');
|
7 |
|
8 | /**
|
9 | * Provides functions for common JavaScript operations.
|
10 | *
|
11 | * // Load module "util"
|
12 | * var util = require('@openveo/api').util;
|
13 | *
|
14 | * @module util
|
15 | * @class util
|
16 | * @main util
|
17 | */
|
18 |
|
19 | /**
|
20 | * Merges, recursively, all properties of object2 in object1.
|
21 | *
|
22 | * This will not create copies of objects.
|
23 | *
|
24 | * @method merge
|
25 | * @static
|
26 | * @param {Object} object1 The JavaScript final object
|
27 | * @param {Object} object2 A second JavaScript object to merge into
|
28 | * the first one
|
29 | * @return {Object} object1
|
30 | */
|
31 | module.exports.merge = function(object1, object2) {
|
32 | if (!object2)
|
33 | return object1;
|
34 |
|
35 | if (!object1)
|
36 | return object2;
|
37 |
|
38 | for (var property in object2) {
|
39 |
|
40 | try {
|
41 |
|
42 | // Object property is an object
|
43 | // Recusively merge its properties
|
44 | if (typeof object2[property] === 'object' && !util.isArray(object2[property])) {
|
45 | object1[property] = object1[property] || {};
|
46 | object1[property] = this.merge(object1[property], object2[property]);
|
47 | } else
|
48 | object1[property] = object2[property];
|
49 |
|
50 | } catch (e) {
|
51 |
|
52 | // Property does not exist in object1, create it
|
53 | object1[property] = object2[property];
|
54 |
|
55 | }
|
56 |
|
57 | }
|
58 |
|
59 | return object1;
|
60 | };
|
61 |
|
62 | /**
|
63 | * Makes union of two arrays.
|
64 | *
|
65 | * @method joinArray
|
66 | * @static
|
67 | * @param {Array} [array1] An array
|
68 | * @param {Array} [array2] An array
|
69 | * @return {Array} The union of the two arrays
|
70 | */
|
71 | module.exports.joinArray = function(array1, array2) {
|
72 | return array1.concat(array2.filter(function(item) {
|
73 | return array1.indexOf(item) < 0;
|
74 | }));
|
75 | };
|
76 |
|
77 | /**
|
78 | * Makes intersection of two arrays.
|
79 | *
|
80 | * @method intersectArray
|
81 | * @static
|
82 | * @param {Array} [array1] An array
|
83 | * @param {Array} [array2] An array
|
84 | * @return {Array} The intersection of the two arrays
|
85 | */
|
86 | module.exports.intersectArray = function(array1, array2) {
|
87 | return array2.filter(function(item) {
|
88 | return array1.indexOf(item) >= 0;
|
89 | });
|
90 | };
|
91 |
|
92 | /**
|
93 | * Compares two arrays.
|
94 | *
|
95 | * Shallow validates that two arrays contains the same elements, no more no less.
|
96 | *
|
97 | * @method areSameArrays
|
98 | * @static
|
99 | * @param {Array} [array1] An array
|
100 | * @param {Array} [array2] An array
|
101 | * @return {Boolean} true if arrays are the same, false otherwise
|
102 | */
|
103 | module.exports.areSameArrays = function(array1, array2) {
|
104 | if (array1.length === array2.length && this.intersectArray(array1, array2).length === array1.length)
|
105 | return true;
|
106 | else
|
107 | return false;
|
108 | };
|
109 |
|
110 | /**
|
111 | * Checks if an email address is valid or not.
|
112 | *
|
113 | * @method isEmailValid
|
114 | * @static
|
115 | * @param {String} email The email address
|
116 | * @return {Boolean} true if the email is valid, false otherwise
|
117 | */
|
118 | module.exports.isEmailValid = function(email) {
|
119 | var reg = new RegExp('[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9]' +
|
120 | '(?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?');
|
121 | return reg.test(email);
|
122 | };
|
123 |
|
124 | /**
|
125 | * Checks if a value is isContained into another comparing primitive types.
|
126 | *
|
127 | * All values in expectedValue must be found in value to pass the test.
|
128 | *
|
129 | * @method isContained
|
130 | * @static
|
131 | * @param {Object|Number|String|Array} expectedValue The value expecting to be found in "value"
|
132 | * @return {Boolean} true if the expected value has been found in value
|
133 | */
|
134 | module.exports.isContained = function(expectedValue, value) {
|
135 | if (Object.prototype.toString.call(expectedValue) === '[object Array]') {
|
136 | if (Object.prototype.toString.call(value) !== '[object Array]')
|
137 | return false;
|
138 |
|
139 | for (var i = 0; i < expectedValue.length; i++) {
|
140 | if (!this.isContained(expectedValue[i], value[i]))
|
141 | return false;
|
142 | }
|
143 | } else if (Object.prototype.toString.call(expectedValue) === '[object Object]') {
|
144 | if (Object.prototype.toString.call(value) !== '[object Object]')
|
145 | return false;
|
146 |
|
147 | for (var property in expectedValue) {
|
148 | if (!this.isContained(expectedValue[property], value[property]))
|
149 | return false;
|
150 | }
|
151 | } else if (expectedValue !== value)
|
152 | return false;
|
153 |
|
154 | return true;
|
155 | };
|
156 |
|
157 | /**
|
158 | * Validates first level object properties using the given validation description object.
|
159 | *
|
160 | * It helps validating that an object, coming from a request query string parameters correspond to the expected
|
161 | * type, if it has to be required, if it must be contained into a list of values etc.
|
162 | *
|
163 | * Available features by types :
|
164 | * - **string**
|
165 | * - **default** Specify a default value
|
166 | * - **required** Boolean to indicate if the value is required (if default is specified, value will always be set)
|
167 | * - **in** Specify an array of strings to validate that the value is inside this array
|
168 | * - **number**
|
169 | * - **default** Specify a default value
|
170 | * - **required** Boolean to indicate if the value is required (if default is specified, value will always be set)
|
171 | * - **in** Specify an array of numbers to validate that the value is inside this array
|
172 | * - **gt** Specify a number to validate that the value is greater than this number
|
173 | * - **lt** Specify a number to validate that the value is lesser than this number
|
174 | * - **gte** Specify a number to validate that the value is greater or equal to this number
|
175 | * - **lte** Specify a number to validate that the value is lesser or equal to this number
|
176 | * - **array<string>**
|
177 | * - **required** Boolean to indicate if the value is required (an empty array is not an error)
|
178 | * - **in** Specify an array of values to validate that each value of the array is inside this array
|
179 | * - **array<number>**
|
180 | * - **required** Boolean to indicate if the value is required (an empty array is not an error)
|
181 | * - **in** Specify an array of values to validate that each value of the array is inside this array
|
182 | * - **array<object>**
|
183 | * - **required** Boolean to indicate if the value is required (an empty array is not an error)
|
184 | * - **date**
|
185 | * - **required** Boolean to indicate if the value is required
|
186 | * - **gt** Specify a date to validate that the value is greater than this date
|
187 | * - **lt** Specify a date to validate that the value is lesser than this date
|
188 | * - **gte** Specify a date to validate that the value is greater or equal to this date
|
189 | * - **lte** Specify a date to validate that the value is lesser or equal to this date
|
190 | * - **object**
|
191 | * - **default** Specify a default value
|
192 | * - **required** Boolean to indicate if the value is required (if default is specified, value will always be set)
|
193 | * - **boolean**
|
194 | * - **default** Specify a default value
|
195 | * - **required** Boolean to indicate if the value is required (if default is specified, value will always be set)
|
196 | * - **file**
|
197 | * - **required** Boolean to indicate if the value is required
|
198 | * - **in** Specify an array of types to validate that the file's type is inside this array
|
199 | *
|
200 | * @example
|
201 | *
|
202 | * // Get util
|
203 | * var util = require('@openveo/api').util;
|
204 | * var fileSystem = require('@openveo/api').fileSystem;
|
205 | *
|
206 | * // Validate parameters
|
207 | * var params = util.shallowValidateObject({
|
208 | * myStringProperty: 'my value',
|
209 | * myNumberProperty: 25,
|
210 | * myArrayStringProperty: ['value1', 'value2'],
|
211 | * myArrayNumberProperty: [10, 5],
|
212 | * myArrayObjectProperty: [{}, {}],
|
213 | * myDateProperty: '02/25/2016',
|
214 | * myObjectProperty: {firstKey: 'firstValue'},
|
215 | * myBooleanProperty: true,
|
216 | * myFileProperty: 88 13 70 17 // At least the first 300 bytes of the file
|
217 | * }, {
|
218 | * myStringProperty: {type: 'string', required: true, default: 'default', in: ['my value', 'value']},
|
219 | * myNumberProperty: {type: 'number', required: true, default: 0, in: [0, 5, 10], gte: 0, lte: 5},
|
220 | * myArrayStringProperty: {type: 'array<string>', required: true, in: ['value1', 'value2']},
|
221 | * myArrayNumberProperty: {type: 'array<number>', required: true, in: [42, 43]},
|
222 | * myArrayObjectProperty: {type: 'array<object>', required: true},
|
223 | * myDateProperty: {type: 'date', required: true, gte: '02/20/2016', lte: '03/30/2016'},
|
224 | * myObjectProperty: {type: 'object', required: true},
|
225 | * myBooleanProperty: {type: 'boolean', required: true},
|
226 | * myFileProperty: {type: 'file', required: true, in: [
|
227 | * fileSystem.FILE_TYPES.JPG,
|
228 | * fileSystem.FILE_TYPES.PNG,
|
229 | * fileSystem.FILE_TYPES.GIF,
|
230 | * fileSystem.FILE_TYPES.MP4,
|
231 | * fileSystem.FILE_TYPES.TAR
|
232 | * ]}
|
233 | * });
|
234 | *
|
235 | * console.log(params);
|
236 | *
|
237 | * @method shallowValidateObject
|
238 | * @static
|
239 | * @param {Object} objectToAnalyze The object to analyze
|
240 | * @param {Object} validationDescription The validation description object
|
241 | * @return {Object} A new object with the list of properties as expected
|
242 | * @throws {Error} An error if a property does not respect its associated rules
|
243 | */
|
244 | module.exports.shallowValidateObject = function(objectToAnalyze, validationDescription) {
|
245 | var properties = {};
|
246 |
|
247 | // Iterate through the list of expected properties
|
248 | for (var name in validationDescription) {
|
249 | var expectedProperty = validationDescription[name];
|
250 | var value = objectToAnalyze[name];
|
251 |
|
252 | if (expectedProperty) {
|
253 |
|
254 | // This property was expected
|
255 |
|
256 | // Options
|
257 | var required = expectedProperty.required || false;
|
258 | var inside = expectedProperty.in || null;
|
259 | var defaultValue = expectedProperty.default !== undefined ? expectedProperty.default : null;
|
260 | var gt = expectedProperty.gt !== undefined ? expectedProperty.gt : null;
|
261 | var lt = expectedProperty.lt !== undefined ? expectedProperty.lt : null;
|
262 | var gte = expectedProperty.gte !== undefined ? expectedProperty.gte : null;
|
263 | var lte = expectedProperty.lte !== undefined ? expectedProperty.lte : null;
|
264 |
|
265 | switch (expectedProperty.type) {
|
266 | case 'string':
|
267 | value = value !== undefined ? String(value) : defaultValue;
|
268 | if (inside && inside.indexOf(value) < 0)
|
269 | throw new Error('Property ' + name + ' must be one of ' + inside.join(', '));
|
270 | break;
|
271 | case 'number':
|
272 | value = value !== undefined ? parseInt(value) : defaultValue;
|
273 | value = isNaN(value) ? defaultValue : value;
|
274 | if (gt !== null) gt = parseInt(gt);
|
275 | if (lt !== null) lt = parseInt(lt);
|
276 | if (gte !== null) gte = parseInt(gte);
|
277 | if (lte !== null) lte = parseInt(lte);
|
278 |
|
279 | if (value === null) break;
|
280 |
|
281 | if (gt !== null && value <= gt)
|
282 | throw new Error('Property ' + name + ' must be greater than ' + gt);
|
283 |
|
284 | if (lt !== null && value >= lt)
|
285 | throw new Error('Property ' + name + ' must be lesser than ' + lt);
|
286 |
|
287 | if (gte !== null && value < gte)
|
288 | throw new Error('Property ' + name + ' must be greater or equal to ' + gte);
|
289 |
|
290 | if (lte !== null && value > lte)
|
291 | throw new Error('Property ' + name + ' must be lesser or equal to ' + lte);
|
292 |
|
293 | if (inside && inside.indexOf(value) < 0)
|
294 | throw new Error('Property ' + name + ' must be one of ' + inside.join(', '));
|
295 |
|
296 | break;
|
297 | case 'array<string>':
|
298 | case 'array<number>':
|
299 | case 'array<object>':
|
300 | var arrayType = /array<([^>]*)>/.exec(expectedProperty.type)[1];
|
301 |
|
302 | if ((typeof value === 'string' || typeof value === 'number') && arrayType !== 'object') {
|
303 | value = arrayType === 'string' ? String(value) : parseInt(value);
|
304 | value = value ? [value] : null;
|
305 | } else if (Object.prototype.toString.call(value) === '[object Array]') {
|
306 | var arrayValues = [];
|
307 | for (var i = 0; i < value.length; i++) {
|
308 |
|
309 | if (arrayType === 'string' || arrayType === 'number') {
|
310 | var convertedValue = arrayType === 'string' ? String(value[i]) : parseInt(value[i]);
|
311 | if (convertedValue) {
|
312 |
|
313 | if (inside && inside.indexOf(convertedValue) < 0)
|
314 | throw new Error('Property ' + name + ' has a value (' + convertedValue +
|
315 | ') which is not part of ' + inside.join('or '));
|
316 |
|
317 | arrayValues.push(convertedValue);
|
318 | }
|
319 | }
|
320 |
|
321 | if (arrayType === 'object' && Object.prototype.toString.call(value[i]) === '[object Object]')
|
322 | arrayValues.push(value[i]);
|
323 | }
|
324 |
|
325 | value = arrayValues.length ? arrayValues : null;
|
326 | } else if (typeof value !== 'undefined')
|
327 | throw new Error('Property ' + name + ' must be a "' + expectedProperty.type + '"');
|
328 | else
|
329 | value = null;
|
330 |
|
331 | break;
|
332 | case 'file':
|
333 | if (typeof value === 'string' || (value instanceof Buffer)) {
|
334 | var fileBuffer = (value instanceof Buffer) ? value : Buffer.from(value, 'binary');
|
335 | var fileType = fileSystem.getFileTypeFromBuffer(fileBuffer);
|
336 |
|
337 | if (!fileType) {
|
338 | throw new Error(
|
339 | 'Property ' + name + ' must be a supported file (' +
|
340 | Object.keys(fileSystem.FILE_TYPES).join(', ') + ')'
|
341 | );
|
342 | }
|
343 |
|
344 | if (inside && inside.indexOf(fileType) < 0)
|
345 | throw new Error('Property ' + name + ' must be a ' + inside.join('or ') + ' file');
|
346 |
|
347 | value = {type: fileType, file: fileBuffer};
|
348 | break;
|
349 | }
|
350 |
|
351 | value = null;
|
352 | break;
|
353 | case 'date':
|
354 | var date;
|
355 |
|
356 | if (!value)
|
357 | value = null;
|
358 | else {
|
359 | if (typeof value === 'string') {
|
360 |
|
361 | // Convert literal date into Date object
|
362 | if (!isNaN(new Date(value).getTime()))
|
363 | date = new Date(value).getTime();
|
364 | else if (!isNaN(parseInt(value)))
|
365 | date = new Date(parseInt(value)).getTime();
|
366 | else
|
367 | date = null;
|
368 |
|
369 | } else if (Object.prototype.toString.call(value) === '[object Date]') {
|
370 |
|
371 | // Already a Date object
|
372 | date = value.getTime();
|
373 |
|
374 | }
|
375 |
|
376 | if (date)
|
377 | value = date;
|
378 |
|
379 | if (gt) {
|
380 | var gtDate = typeof gt === 'object' ? gt : new Date(gt);
|
381 | if (value <= gtDate.getTime())
|
382 | throw new Error('Property ' + name + ' must be greater than ' + gtDate.toString());
|
383 | }
|
384 |
|
385 | if (lt) {
|
386 | var ltDate = typeof lt === 'object' ? lt : new Date(lt);
|
387 | if (value >= ltDate.getTime())
|
388 | throw new Error('Property ' + name + ' must be lesser than ' + ltDate.toString());
|
389 | }
|
390 |
|
391 | if (gte) {
|
392 | var gteDate = typeof gte === 'object' ? gte : new Date(gte);
|
393 | if (value < gteDate.getTime())
|
394 | throw new Error('Property ' + name + ' must be greater or equal to ' + gteDate.toString());
|
395 | }
|
396 |
|
397 | if (lte) {
|
398 | var lteDate = typeof lte === 'object' ? lte : new Date(lte);
|
399 | if (value > lteDate.getTime())
|
400 | throw new Error('Property ' + name + ' must be lesser or equal to ' + lteDate.toString());
|
401 | }
|
402 | }
|
403 | break;
|
404 | case 'object':
|
405 | var valueType = Object.prototype.toString.call(value);
|
406 | value = value !== undefined && valueType ? value : defaultValue;
|
407 | break;
|
408 | case 'boolean':
|
409 | value = (value === undefined || value === null) ? defaultValue : Boolean(value);
|
410 | break;
|
411 | default:
|
412 | value = null;
|
413 | }
|
414 |
|
415 | if (required && (value === null || typeof value === 'undefined'))
|
416 | throw new Error('Property ' + name + ' required');
|
417 | else if (value !== null && typeof value !== 'undefined')
|
418 | properties[name] = value;
|
419 |
|
420 | }
|
421 |
|
422 | }
|
423 |
|
424 | return properties;
|
425 | };
|
426 |
|
427 | /**
|
428 | * Validates that files are in the expected type.
|
429 | *
|
430 | * Available features for validation object:
|
431 | * - **in** Specify an array of types to validate that the file type is inside this array
|
432 | *
|
433 | * @example
|
434 | *
|
435 | * // Get util
|
436 | * var util = require('@openveo/api').util;
|
437 | * var fileSystem = require('@openveo/api').fileSystem;
|
438 | *
|
439 | * // Validate parameters
|
440 | * var params = util.validateFiles({
|
441 | * myFirstFile: '/tmp/myFirstFile.mp4',
|
442 | * mySecondFile: '/tmp/mySecondFile.tar'
|
443 | * }, {
|
444 | * myFirstFile: {in: [fileSystem.FILE_TYPES.MP4]},
|
445 | * mySecondFile: {in: [fileSystem.FILE_TYPES.TAR]}
|
446 | * }, function(error, files) {
|
447 | * if (error) {
|
448 | * console.log('An error occurred during validation with message: ' + error.message);
|
449 | * }
|
450 | *
|
451 | * console.log('Is file valid ? ' + files.myFirstFile.isValid);
|
452 | * console.log('File type: ' + files.myFirstFile.type);
|
453 | * });
|
454 | *
|
455 | * console.log(params);
|
456 | *
|
457 | * @method validateFiles
|
458 | * @static
|
459 | * @async
|
460 | * @param {Object} filesToAnalyze Files to validate with keys as files identifiers and values as
|
461 | * files absolute paths
|
462 | * @param {Object} validationDescription The validation description object with keys as files identifiers
|
463 | * and values as validation objects
|
464 | * @param {Function} callback The function to call when done
|
465 | * - **Error** The error if an error occurred, null otherwise
|
466 | * - **Object** Files with keys as the files identifiers and values as Objects containing validation
|
467 | * information: isValid and type (from util.FILE_TYPES)
|
468 | */
|
469 | module.exports.validateFiles = function(filesToAnalyze, validationDescription, callback) {
|
470 | var files = {};
|
471 | var asyncFunctions = [];
|
472 |
|
473 | var getAsyncFunction = function(id, filePath) {
|
474 | return function(callback) {
|
475 | fileSystem.readFile(filePath, 0, 300, function(error, buffer) {
|
476 | if (error) return callback(error);
|
477 |
|
478 | var pathDescriptor = path.parse(filePath);
|
479 | var fileType = fileSystem.getFileTypeFromBuffer(buffer);
|
480 | files[id] = {isValid: false};
|
481 |
|
482 | if (fileType === fileSystem.FILE_TYPES.UNKNOWN && pathDescriptor.ext === '.tar')
|
483 | files[id].type = fileSystem.FILE_TYPES.TAR;
|
484 | else
|
485 | files[id].type = fileType;
|
486 |
|
487 | if (validationDescription[id].in.indexOf(files[id].type) > -1 &&
|
488 | (!validationDescription[id].validateExtension || pathDescriptor.ext.toLowerCase() === '.' + fileType))
|
489 | files[id].isValid = true;
|
490 |
|
491 | callback();
|
492 | });
|
493 | };
|
494 | };
|
495 |
|
496 | for (var id in filesToAnalyze) {
|
497 | if (filesToAnalyze[id] && validationDescription && validationDescription[id])
|
498 | asyncFunctions.push(getAsyncFunction(id, filesToAnalyze[id]));
|
499 | }
|
500 |
|
501 | if (!asyncFunctions.length)
|
502 | return callback(new Error('No files to analyze'));
|
503 |
|
504 | async.parallel(asyncFunctions, function(error) {
|
505 | if (error) return callback(error);
|
506 | callback(null, files);
|
507 | });
|
508 | };
|
509 |
|
510 | /**
|
511 | * Gets a specific property from an Array of Objects.
|
512 | *
|
513 | * @example
|
514 | *
|
515 | * // Get util
|
516 | * var util = require('@openveo/api').util;
|
517 | *
|
518 | * // Get property 'id' of each objects of the array
|
519 | * var params = util.getPropertyFromArray('id', [
|
520 | * {id: 0},
|
521 | * {id: 1},
|
522 | * {id: 2, items: [{id: 3}]}
|
523 | * ], 'items');
|
524 | *
|
525 | * // [0, 1, 2, 3]
|
526 | * console.log(params);
|
527 | *
|
528 | * @method getPropertyFromArray
|
529 | * @static
|
530 | * @param {String} property The name of the property to fetch
|
531 | * @param {Array} list The list of objects to look into
|
532 | * @param {String} [recursiveProperty] The name of the recursive property to look into
|
533 | * @return {Array} The list of values for the given property
|
534 | */
|
535 | module.exports.getPropertyFromArray = function(property, list, recursiveProperty) {
|
536 | var self = this;
|
537 | var values = [];
|
538 |
|
539 | if (!list || !list.length || !property)
|
540 | return values;
|
541 |
|
542 | list.forEach(function(item) {
|
543 | values.push(item[property]);
|
544 |
|
545 | if (recursiveProperty && item[recursiveProperty])
|
546 | values = values.concat(self.getPropertyFromArray(property, item[recursiveProperty], recursiveProperty));
|
547 | });
|
548 |
|
549 | return values;
|
550 | };
|
551 |
|
552 | /**
|
553 | * Evaluates a path of properties on an object.
|
554 | *
|
555 | * It does not use the JavaScript eval function.
|
556 | *
|
557 | * @example
|
558 | *
|
559 | * // Get util
|
560 | * var util = require('@openveo/api').util;
|
561 | *
|
562 | * // Get property 'my.deep.property' of the object
|
563 | * var value = util.evaluateDeepObjectProperties('my.deep.property', {
|
564 | * my {
|
565 | * deep {
|
566 | * property: 'My deep property value'
|
567 | * }
|
568 | * }
|
569 | * });
|
570 | *
|
571 | * // "My deep property value"
|
572 | * console.log(value);
|
573 | *
|
574 | * @method evaluateDeepObjectProperties
|
575 | * @static
|
576 | * @param {String} propertyPath The path of the property to retreive from the object
|
577 | * @param {Object} objectToAnalyze The object containing the requested property
|
578 | * @return {Mixed} The value of the property
|
579 | */
|
580 | module.exports.evaluateDeepObjectProperties = function(propertyPath, objectToAnalyze) {
|
581 | if (!propertyPath) return null;
|
582 |
|
583 | var propertyNames = propertyPath.split('.');
|
584 | var value = objectToAnalyze;
|
585 |
|
586 | for (var i = 0; i < propertyNames.length; i++) {
|
587 | if (!value[propertyNames[i]]) return null;
|
588 | value = value[propertyNames[i]];
|
589 | }
|
590 |
|
591 | return value;
|
592 | };
|