| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| var log = require('./log'); |
| var Int64 = require('node-int64'); |
| var InputBufferUnderrunError = require('./transport').InputBufferUnderrunError; |
| var Thrift = require('./thrift'); |
| var Type = Thrift.Type; |
| var util = require("util"); |
| |
| var Int64Util = require('./int64_util'); |
| var json_parse = require('./json_parse'); |
| |
| var InputBufferUnderrunError = require('./input_buffer_underrun_error'); |
| |
| module.exports = TJSONProtocol; |
| |
| /** |
| * Initializes a Thrift JSON protocol instance. |
| * @constructor |
| * @param {Thrift.Transport} trans - The transport to serialize to/from. |
| * @classdesc Apache Thrift Protocols perform serialization which enables cross |
| * language RPC. The Protocol type is the JavaScript browser implementation |
| * of the Apache Thrift TJSONProtocol. |
| * @example |
| * var protocol = new Thrift.Protocol(transport); |
| */ |
| function TJSONProtocol(trans) { |
| this.tstack = []; |
| this.tpos = []; |
| this.trans = trans; |
| }; |
| |
| /** |
| * Thrift IDL type Id to string mapping. |
| * @readonly |
| * @see {@link Thrift.Type} |
| */ |
| TJSONProtocol.Type = {}; |
| TJSONProtocol.Type[Type.BOOL] = '"tf"'; |
| TJSONProtocol.Type[Type.BYTE] = '"i8"'; |
| TJSONProtocol.Type[Type.I16] = '"i16"'; |
| TJSONProtocol.Type[Type.I32] = '"i32"'; |
| TJSONProtocol.Type[Type.I64] = '"i64"'; |
| TJSONProtocol.Type[Type.DOUBLE] = '"dbl"'; |
| TJSONProtocol.Type[Type.STRUCT] = '"rec"'; |
| TJSONProtocol.Type[Type.STRING] = '"str"'; |
| TJSONProtocol.Type[Type.MAP] = '"map"'; |
| TJSONProtocol.Type[Type.LIST] = '"lst"'; |
| TJSONProtocol.Type[Type.SET] = '"set"'; |
| |
| /** |
| * Thrift IDL type string to Id mapping. |
| * @readonly |
| * @see {@link Thrift.Type} |
| */ |
| TJSONProtocol.RType = {}; |
| TJSONProtocol.RType.tf = Type.BOOL; |
| TJSONProtocol.RType.i8 = Type.BYTE; |
| TJSONProtocol.RType.i16 = Type.I16; |
| TJSONProtocol.RType.i32 = Type.I32; |
| TJSONProtocol.RType.i64 = Type.I64; |
| TJSONProtocol.RType.dbl = Type.DOUBLE; |
| TJSONProtocol.RType.rec = Type.STRUCT; |
| TJSONProtocol.RType.str = Type.STRING; |
| TJSONProtocol.RType.map = Type.MAP; |
| TJSONProtocol.RType.lst = Type.LIST; |
| TJSONProtocol.RType.set = Type.SET; |
| |
| /** |
| * The TJSONProtocol version number. |
| * @readonly |
| * @const {number} Version |
| * @memberof Thrift.Protocol |
| */ |
| TJSONProtocol.Version = 1; |
| |
| TJSONProtocol.prototype.flush = function() { |
| this.writeToTransportIfStackIsFlushable(); |
| return this.trans.flush(); |
| }; |
| |
| TJSONProtocol.prototype.writeToTransportIfStackIsFlushable = function() { |
| if (this.tstack.length === 1) { |
| this.trans.write(this.tstack.pop()); |
| } |
| }; |
| |
| /** |
| * Serializes the beginning of a Thrift RPC message. |
| * @param {string} name - The service method to call. |
| * @param {Thrift.MessageType} messageType - The type of method call. |
| * @param {number} seqid - The sequence number of this call (always 0 in Apache Thrift). |
| */ |
| TJSONProtocol.prototype.writeMessageBegin = function(name, messageType, seqid) { |
| this.tstack.push([TJSONProtocol.Version, '"' + name + '"', messageType, seqid]); |
| }; |
| |
| /** |
| * Serializes the end of a Thrift RPC message. |
| */ |
| TJSONProtocol.prototype.writeMessageEnd = function() { |
| var obj = this.tstack.pop(); |
| |
| this.wobj = this.tstack.pop(); |
| this.wobj.push(obj); |
| |
| this.wbuf = '[' + this.wobj.join(',') + ']'; |
| |
| // we assume there is nothing more to come so we write |
| this.trans.write(this.wbuf); |
| }; |
| |
| /** |
| * Serializes the beginning of a struct. |
| * @param {string} name - The name of the struct. |
| */ |
| TJSONProtocol.prototype.writeStructBegin = function(name) { |
| this.tpos.push(this.tstack.length); |
| this.tstack.push({}); |
| }; |
| |
| /** |
| * Serializes the end of a struct. |
| */ |
| TJSONProtocol.prototype.writeStructEnd = function() { |
| var p = this.tpos.pop(); |
| var struct = this.tstack[p]; |
| var str = '{'; |
| var first = true; |
| for (var key in struct) { |
| if (first) { |
| first = false; |
| } else { |
| str += ','; |
| } |
| |
| str += key + ':' + struct[key]; |
| } |
| |
| str += '}'; |
| this.tstack[p] = str; |
| |
| this.writeToTransportIfStackIsFlushable(); |
| }; |
| |
| /** |
| * Serializes the beginning of a struct field. |
| * @param {string} name - The name of the field. |
| * @param {Thrift.Protocol.Type} fieldType - The data type of the field. |
| * @param {number} fieldId - The field's unique identifier. |
| */ |
| TJSONProtocol.prototype.writeFieldBegin = function(name, fieldType, fieldId) { |
| this.tpos.push(this.tstack.length); |
| this.tstack.push({ 'fieldId': '"' + |
| fieldId + '"', 'fieldType': TJSONProtocol.Type[fieldType] |
| }); |
| }; |
| |
| /** |
| * Serializes the end of a field. |
| */ |
| TJSONProtocol.prototype.writeFieldEnd = function() { |
| var value = this.tstack.pop(); |
| var fieldInfo = this.tstack.pop(); |
| |
| if (':' + value === ":[object Object]") { |
| this.tstack[this.tstack.length - 1][fieldInfo.fieldId] = '{' + |
| fieldInfo.fieldType + ':' + JSON.stringify(value) + '}'; |
| } else { |
| this.tstack[this.tstack.length - 1][fieldInfo.fieldId] = '{' + |
| fieldInfo.fieldType + ':' + value + '}'; |
| } |
| this.tpos.pop(); |
| |
| this.writeToTransportIfStackIsFlushable(); |
| }; |
| |
| /** |
| * Serializes the end of the set of fields for a struct. |
| */ |
| TJSONProtocol.prototype.writeFieldStop = function() { |
| }; |
| |
| /** |
| * Serializes the beginning of a map collection. |
| * @param {Thrift.Type} keyType - The data type of the key. |
| * @param {Thrift.Type} valType - The data type of the value. |
| * @param {number} [size] - The number of elements in the map (ignored). |
| */ |
| TJSONProtocol.prototype.writeMapBegin = function(keyType, valType, size) { |
| //size is invalid, we'll set it on end. |
| this.tpos.push(this.tstack.length); |
| this.tstack.push([TJSONProtocol.Type[keyType], TJSONProtocol.Type[valType], 0]); |
| }; |
| |
| /** |
| * Serializes the end of a map. |
| */ |
| TJSONProtocol.prototype.writeMapEnd = function() { |
| var p = this.tpos.pop(); |
| |
| if (p == this.tstack.length) { |
| return; |
| } |
| |
| if ((this.tstack.length - p - 1) % 2 !== 0) { |
| this.tstack.push(''); |
| } |
| |
| var size = (this.tstack.length - p - 1) / 2; |
| |
| this.tstack[p][this.tstack[p].length - 1] = size; |
| |
| var map = '}'; |
| var first = true; |
| while (this.tstack.length > p + 1) { |
| var v = this.tstack.pop(); |
| var k = this.tstack.pop(); |
| if (first) { |
| first = false; |
| } else { |
| map = ',' + map; |
| } |
| |
| if (! isNaN(k)) { k = '"' + k + '"'; } //json "keys" need to be strings |
| map = k + ':' + v + map; |
| } |
| map = '{' + map; |
| |
| this.tstack[p].push(map); |
| this.tstack[p] = '[' + this.tstack[p].join(',') + ']'; |
| |
| this.writeToTransportIfStackIsFlushable(); |
| }; |
| |
| /** |
| * Serializes the beginning of a list collection. |
| * @param {Thrift.Type} elemType - The data type of the elements. |
| * @param {number} size - The number of elements in the list. |
| */ |
| TJSONProtocol.prototype.writeListBegin = function(elemType, size) { |
| this.tpos.push(this.tstack.length); |
| this.tstack.push([TJSONProtocol.Type[elemType], size]); |
| }; |
| |
| /** |
| * Serializes the end of a list. |
| */ |
| TJSONProtocol.prototype.writeListEnd = function() { |
| var p = this.tpos.pop(); |
| |
| while (this.tstack.length > p + 1) { |
| var tmpVal = this.tstack[p + 1]; |
| this.tstack.splice(p + 1, 1); |
| this.tstack[p].push(tmpVal); |
| } |
| |
| this.tstack[p] = '[' + this.tstack[p].join(',') + ']'; |
| |
| this.writeToTransportIfStackIsFlushable(); |
| }; |
| |
| /** |
| * Serializes the beginning of a set collection. |
| * @param {Thrift.Type} elemType - The data type of the elements. |
| * @param {number} size - The number of elements in the list. |
| */ |
| TJSONProtocol.prototype.writeSetBegin = function(elemType, size) { |
| this.tpos.push(this.tstack.length); |
| this.tstack.push([TJSONProtocol.Type[elemType], size]); |
| }; |
| |
| /** |
| * Serializes the end of a set. |
| */ |
| TJSONProtocol.prototype.writeSetEnd = function() { |
| var p = this.tpos.pop(); |
| |
| while (this.tstack.length > p + 1) { |
| var tmpVal = this.tstack[p + 1]; |
| this.tstack.splice(p + 1, 1); |
| this.tstack[p].push(tmpVal); |
| } |
| |
| this.tstack[p] = '[' + this.tstack[p].join(',') + ']'; |
| |
| this.writeToTransportIfStackIsFlushable(); |
| }; |
| |
| /** Serializes a boolean */ |
| TJSONProtocol.prototype.writeBool = function(bool) { |
| this.tstack.push(bool ? 1 : 0); |
| }; |
| |
| /** Serializes a number */ |
| TJSONProtocol.prototype.writeByte = function(byte) { |
| this.tstack.push(byte); |
| }; |
| |
| /** Serializes a number */ |
| TJSONProtocol.prototype.writeI16 = function(i16) { |
| this.tstack.push(i16); |
| }; |
| |
| /** Serializes a number */ |
| TJSONProtocol.prototype.writeI32 = function(i32) { |
| this.tstack.push(i32); |
| }; |
| |
| /** Serializes a number */ |
| TJSONProtocol.prototype.writeI64 = function(i64) { |
| if (i64 instanceof Int64) { |
| this.tstack.push(Int64Util.toDecimalString(i64)); |
| } else { |
| this.tstack.push(i64); |
| } |
| }; |
| |
| /** Serializes a number */ |
| TJSONProtocol.prototype.writeDouble = function(dub) { |
| this.tstack.push(dub); |
| }; |
| |
| /** Serializes a string */ |
| TJSONProtocol.prototype.writeString = function(arg) { |
| // We do not encode uri components for wire transfer: |
| if (arg === null) { |
| this.tstack.push(null); |
| } else { |
| if (typeof arg === 'string') { |
| var str = arg; |
| } else if (arg instanceof Buffer) { |
| var str = arg.toString('utf8'); |
| } else { |
| throw new Error('writeString called without a string/Buffer argument: ' + arg); |
| } |
| |
| // concat may be slower than building a byte buffer |
| var escapedString = ''; |
| for (var i = 0; i < str.length; i++) { |
| var ch = str.charAt(i); // a single double quote: " |
| if (ch === '\"') { |
| escapedString += '\\\"'; // write out as: \" |
| } else if (ch === '\\') { // a single backslash: \ |
| escapedString += '\\\\'; // write out as: \\ |
| /* Currently escaped forward slashes break TJSONProtocol. |
| * As it stands, we can simply pass forward slashes into |
| * our strings across the wire without being escaped. |
| * I think this is the protocol's bug, not thrift.js |
| * } else if(ch === '/') { // a single forward slash: / |
| * escapedString += '\\/'; // write out as \/ |
| * } |
| */ |
| } else if (ch === '\b') { // a single backspace: invisible |
| escapedString += '\\b'; // write out as: \b" |
| } else if (ch === '\f') { // a single formfeed: invisible |
| escapedString += '\\f'; // write out as: \f" |
| } else if (ch === '\n') { // a single newline: invisible |
| escapedString += '\\n'; // write out as: \n" |
| } else if (ch === '\r') { // a single return: invisible |
| escapedString += '\\r'; // write out as: \r" |
| } else if (ch === '\t') { // a single tab: invisible |
| escapedString += '\\t'; // write out as: \t" |
| } else { |
| escapedString += ch; // Else it need not be escaped |
| } |
| } |
| this.tstack.push('"' + escapedString + '"'); |
| } |
| }; |
| |
| /** Serializes a string */ |
| TJSONProtocol.prototype.writeBinary = function(arg) { |
| if (typeof arg === 'string') { |
| var buf = new Buffer(arg, 'binary'); |
| } else if (arg instanceof Buffer || |
| Object.prototype.toString.call(arg) == '[object Uint8Array]') { |
| var buf = arg; |
| } else { |
| throw new Error('writeBinary called without a string/Buffer argument: ' + arg); |
| } |
| this.tstack.push('"' + buf.toString('base64') + '"'); |
| }; |
| |
| /** |
| * @class |
| * @name AnonReadMessageBeginReturn |
| * @property {string} fname - The name of the service method. |
| * @property {Thrift.MessageType} mtype - The type of message call. |
| * @property {number} rseqid - The sequence number of the message (0 in Thrift RPC). |
| */ |
| /** |
| * Deserializes the beginning of a message. |
| * @returns {AnonReadMessageBeginReturn} |
| */ |
| TJSONProtocol.prototype.readMessageBegin = function() { |
| this.rstack = []; |
| this.rpos = []; |
| |
| //Borrow the inbound transport buffer and ensure data is present/consistent |
| var transBuf = this.trans.borrow(); |
| if (transBuf.readIndex >= transBuf.writeIndex) { |
| throw new InputBufferUnderrunError(); |
| } |
| var cursor = transBuf.readIndex; |
| |
| if (transBuf.buf[cursor] !== 0x5B) { //[ |
| throw new Error("Malformed JSON input, no opening bracket"); |
| } |
| |
| //Parse a single message (there may be several in the buffer) |
| // TODO: Handle characters using multiple code units |
| cursor++; |
| var openBracketCount = 1; |
| var inString = false; |
| for (; cursor < transBuf.writeIndex; cursor++) { |
| var chr = transBuf.buf[cursor]; |
| //we use hexa charcode here because data[i] returns an int and not a char |
| if (inString) { |
| if (chr === 0x22) { //" |
| inString = false; |
| } else if (chr === 0x5C) { //\ |
| //escaped character, skip |
| cursor += 1; |
| } |
| } else { |
| if (chr === 0x5B) { //[ |
| openBracketCount += 1; |
| } else if (chr === 0x5D) { //] |
| openBracketCount -= 1; |
| if (openBracketCount === 0) { |
| //end of json message detected |
| break; |
| } |
| } else if (chr === 0x22) { //" |
| inString = true; |
| } |
| } |
| } |
| |
| if (openBracketCount !== 0) { |
| // Missing closing bracket. Can be buffer underrun. |
| throw new InputBufferUnderrunError(); |
| } |
| |
| //Reconstitute the JSON object and conume the necessary bytes |
| this.robj = json_parse(transBuf.buf.slice(transBuf.readIndex, cursor+1).toString()); |
| this.trans.consume(cursor + 1 - transBuf.readIndex); |
| |
| //Verify the protocol version |
| var version = this.robj.shift(); |
| if (version != TJSONProtocol.Version) { |
| throw new Error('Wrong thrift protocol version: ' + version); |
| } |
| |
| //Objectify the thrift message {name/type/sequence-number} for return |
| // and then save the JSON object in rstack |
| var r = {}; |
| r.fname = this.robj.shift(); |
| r.mtype = this.robj.shift(); |
| r.rseqid = this.robj.shift(); |
| this.rstack.push(this.robj.shift()); |
| return r; |
| }; |
| |
| /** Deserializes the end of a message. */ |
| TJSONProtocol.prototype.readMessageEnd = function() { |
| }; |
| |
| /** |
| * Deserializes the beginning of a struct. |
| * @param {string} [name] - The name of the struct (ignored) |
| * @returns {object} - An object with an empty string fname property |
| */ |
| TJSONProtocol.prototype.readStructBegin = function() { |
| var r = {}; |
| r.fname = ''; |
| |
| //incase this is an array of structs |
| if (this.rstack[this.rstack.length - 1] instanceof Array) { |
| this.rstack.push(this.rstack[this.rstack.length - 1].shift()); |
| } |
| |
| return r; |
| }; |
| |
| /** Deserializes the end of a struct. */ |
| TJSONProtocol.prototype.readStructEnd = function() { |
| this.rstack.pop(); |
| }; |
| |
| /** |
| * @class |
| * @name AnonReadFieldBeginReturn |
| * @property {string} fname - The name of the field (always ''). |
| * @property {Thrift.Type} ftype - The data type of the field. |
| * @property {number} fid - The unique identifier of the field. |
| */ |
| /** |
| * Deserializes the beginning of a field. |
| * @returns {AnonReadFieldBeginReturn} |
| */ |
| TJSONProtocol.prototype.readFieldBegin = function() { |
| var r = {}; |
| |
| var fid = -1; |
| var ftype = Type.STOP; |
| |
| //get a fieldId |
| for (var f in (this.rstack[this.rstack.length - 1])) { |
| if (f === null) { |
| continue; |
| } |
| |
| fid = parseInt(f, 10); |
| this.rpos.push(this.rstack.length); |
| |
| var field = this.rstack[this.rstack.length - 1][fid]; |
| |
| //remove so we don't see it again |
| delete this.rstack[this.rstack.length - 1][fid]; |
| |
| this.rstack.push(field); |
| |
| break; |
| } |
| |
| if (fid != -1) { |
| //should only be 1 of these but this is the only |
| //way to match a key |
| for (var i in (this.rstack[this.rstack.length - 1])) { |
| if (TJSONProtocol.RType[i] === null) { |
| continue; |
| } |
| |
| ftype = TJSONProtocol.RType[i]; |
| this.rstack[this.rstack.length - 1] = this.rstack[this.rstack.length - 1][i]; |
| } |
| } |
| |
| r.fname = ''; |
| r.ftype = ftype; |
| r.fid = fid; |
| |
| return r; |
| }; |
| |
| /** Deserializes the end of a field. */ |
| TJSONProtocol.prototype.readFieldEnd = function() { |
| var pos = this.rpos.pop(); |
| |
| //get back to the right place in the stack |
| while (this.rstack.length > pos) { |
| this.rstack.pop(); |
| } |
| }; |
| |
| /** |
| * @class |
| * @name AnonReadMapBeginReturn |
| * @property {Thrift.Type} ktype - The data type of the key. |
| * @property {Thrift.Type} vtype - The data type of the value. |
| * @property {number} size - The number of elements in the map. |
| */ |
| /** |
| * Deserializes the beginning of a map. |
| * @returns {AnonReadMapBeginReturn} |
| */ |
| TJSONProtocol.prototype.readMapBegin = function() { |
| var map = this.rstack.pop(); |
| var first = map.shift(); |
| if (first instanceof Array) { |
| this.rstack.push(map); |
| map = first; |
| first = map.shift(); |
| } |
| |
| var r = {}; |
| r.ktype = TJSONProtocol.RType[first]; |
| r.vtype = TJSONProtocol.RType[map.shift()]; |
| r.size = map.shift(); |
| |
| |
| this.rpos.push(this.rstack.length); |
| this.rstack.push(map.shift()); |
| |
| return r; |
| }; |
| |
| /** Deserializes the end of a map. */ |
| TJSONProtocol.prototype.readMapEnd = function() { |
| this.readFieldEnd(); |
| }; |
| |
| /** |
| * @class |
| * @name AnonReadColBeginReturn |
| * @property {Thrift.Type} etype - The data type of the element. |
| * @property {number} size - The number of elements in the collection. |
| */ |
| /** |
| * Deserializes the beginning of a list. |
| * @returns {AnonReadColBeginReturn} |
| */ |
| TJSONProtocol.prototype.readListBegin = function() { |
| var list = this.rstack[this.rstack.length - 1]; |
| |
| var r = {}; |
| r.etype = TJSONProtocol.RType[list.shift()]; |
| r.size = list.shift(); |
| |
| this.rpos.push(this.rstack.length); |
| this.rstack.push(list.shift()); |
| |
| return r; |
| }; |
| |
| /** Deserializes the end of a list. */ |
| TJSONProtocol.prototype.readListEnd = function() { |
| var pos = this.rpos.pop() - 2; |
| var st = this.rstack; |
| st.pop(); |
| if (st instanceof Array && st.length > pos && st[pos].length > 0) { |
| st.push(st[pos].shift()); |
| } |
| }; |
| |
| /** |
| * Deserializes the beginning of a set. |
| * @returns {AnonReadColBeginReturn} |
| */ |
| TJSONProtocol.prototype.readSetBegin = function() { |
| return this.readListBegin(); |
| }; |
| |
| /** Deserializes the end of a set. */ |
| TJSONProtocol.prototype.readSetEnd = function() { |
| return this.readListEnd(); |
| }; |
| |
| TJSONProtocol.prototype.readBool = function() { |
| return this.readValue() == '1'; |
| }; |
| |
| TJSONProtocol.prototype.readByte = function() { |
| return this.readI32(); |
| }; |
| |
| TJSONProtocol.prototype.readI16 = function() { |
| return this.readI32(); |
| }; |
| |
| TJSONProtocol.prototype.readI32 = function(f) { |
| return +this.readValue(); |
| } |
| |
| /** Returns the next value found in the protocol buffer */ |
| TJSONProtocol.prototype.readValue = function(f) { |
| if (f === undefined) { |
| f = this.rstack[this.rstack.length - 1]; |
| } |
| |
| var r = {}; |
| |
| if (f instanceof Array) { |
| if (f.length === 0) { |
| r.value = undefined; |
| } else { |
| r.value = f.shift(); |
| } |
| } else if (!(f instanceof Int64) && f instanceof Object) { |
| for (var i in f) { |
| if (i === null) { |
| continue; |
| } |
| this.rstack.push(f[i]); |
| delete f[i]; |
| |
| r.value = i; |
| break; |
| } |
| } else { |
| r.value = f; |
| this.rstack.pop(); |
| } |
| |
| return r.value; |
| }; |
| |
| TJSONProtocol.prototype.readI64 = function() { |
| var n = this.readValue() |
| if (typeof n === 'string') { |
| // Assuming no one is sending in 1.11111e+33 format |
| return Int64Util.fromDecimalString(n); |
| } else { |
| return new Int64(n); |
| } |
| }; |
| |
| TJSONProtocol.prototype.readDouble = function() { |
| return this.readI32(); |
| }; |
| |
| TJSONProtocol.prototype.readBinary = function() { |
| return new Buffer(this.readValue(), 'base64'); |
| }; |
| |
| TJSONProtocol.prototype.readString = function() { |
| return this.readValue(); |
| }; |
| |
| /** |
| * Returns the underlying transport. |
| * @readonly |
| * @returns {Thrift.Transport} The underlying transport. |
| */ |
| TJSONProtocol.prototype.getTransport = function() { |
| return this.trans; |
| }; |
| |
| /** |
| * Method to arbitrarily skip over data |
| */ |
| TJSONProtocol.prototype.skip = function(type) { |
| throw new Error('skip not supported yet'); |
| }; |