diff --git a/docs/adapter.js.html b/docs/adapter.js.html index 031d971..cd0cfef 100644 --- a/docs/adapter.js.html +++ b/docs/adapter.js.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -88,6 +92,15 @@ module.exports = { */ execute: function(sql, params, callback) { throw new Error("Correct adapter not defined for query execution"); + }, + + /** + * Close the connection that is open on the current adapter + * + * @return void + */ + close: function() { + throw new Error("Close method not defined for the current adapter"); } }; @@ -110,7 +123,7 @@ module.exports = { Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/driver.js.html b/docs/driver.js.html index 907121a..887d66c 100644 --- a/docs/driver.js.html +++ b/docs/driver.js.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -95,6 +99,8 @@ var d = { * @private */ _quote: function(str) { + //if (/[0-9]+|\'(.*?)\'/ig.test(str)) return str; + return (helpers.isString(str) && ! (str.startsWith(d.identifierChar) || str.endsWith(d.identifierChar))) ? d.identifierChar + str + d.identifierChar : str; @@ -146,6 +152,12 @@ var d = { return str.map(d.quoteIdentifiers); } +if ( ! helpers.isString(str)) +{ + console.error(str); + return str; +} + // Handle commas if (str.contains(',')) { @@ -252,7 +264,7 @@ module.exports = d; Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/helpers.js.html b/docs/helpers.js.html index f421591..f26c065 100644 --- a/docs/helpers.js.html +++ b/docs/helpers.js.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -213,7 +217,7 @@ module.exports = h; Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/index.html b/docs/index.html index e747466..fd27b9d 100644 --- a/docs/index.html +++ b/docs/index.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -153,7 +157,7 @@ query.select('foo') Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/module-State.html b/docs/module-State.html new file mode 100644 index 0000000..6d04ac2 --- /dev/null +++ b/docs/module-State.html @@ -0,0 +1,259 @@ + + + + + + DocStrap Module: State + + + + + + + + + +
    + + +
    + + +
    + +
    + + + +

    Module: State

    +
    + +
    +

    + State +

    + +
    + +
    +
    + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + +
    + + + + + + + +
    + + + + +
    + + + + + + + + + + + + + + + + + + +
    + +
    + + + + +
    + +
    +
    + + + + DocStrap Copyright © 2012-2014 The contributors to the JSDoc3 and DocStrap projects. + +
    + + + Documentation generated by JSDoc 3.3.0-alpha9 + on Fri Jan 23rd 2015 using the DocStrap template. + +
    +
    + + +
    +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/module-adapter.html b/docs/module-adapter.html index 3130cfd..ebea6d2 100644 --- a/docs/module-adapter.html +++ b/docs/module-adapter.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -148,6 +152,93 @@
    +
    +

    <static> close()

    + + +
    +
    + + +
    +

    Close the connection that is open on the current adapter

    +
    + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + +
    + + + + + + + +
    + + + + + + + + + + + +
    Returns:
    + + +
    +

    void

    +
    + + + + + + + +
    + + +

    <static> execute(sql, params, callback)

    @@ -354,7 +445,7 @@ Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/module-driver.html b/docs/module-driver.html index f1e285a..a2fd2e6 100644 --- a/docs/module-driver.html +++ b/docs/module-driver.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -170,7 +174,7 @@ Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/module-helpers.html b/docs/module-helpers.html index 1752581..ce19891 100644 --- a/docs/module-helpers.html +++ b/docs/module-helpers.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -1027,7 +1031,7 @@ in the passed array

    Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/module-node-query.html b/docs/module-node-query.html index e306a0f..30939f9 100644 --- a/docs/module-node-query.html +++ b/docs/module-node-query.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -485,7 +489,7 @@ Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/module-query-builder.html b/docs/module-query-builder.html index e742fbe..6892f73 100644 --- a/docs/module-query-builder.html +++ b/docs/module-query-builder.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -188,7 +192,7 @@ @@ -383,7 +387,94 @@ + + + + + + + + +
    + + + + + + + + + + + +
    Returns:
    + + +
    +

    void

    +
    + + + + + + + + + + + +
    +

    end()

    + + +
    +
    + + +
    +

    Closes the database connection for the current adapter

    +
    + + + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    +
    @@ -519,7 +610,7 @@
    @@ -764,7 +855,7 @@ @@ -955,7 +1046,7 @@ @@ -1154,7 +1245,7 @@ @@ -1355,7 +1446,7 @@ @@ -1546,7 +1637,7 @@ @@ -1641,7 +1732,7 @@ @@ -1788,7 +1879,7 @@ @@ -1875,7 +1966,7 @@ @@ -1962,7 +2053,7 @@ @@ -2147,7 +2238,7 @@ @@ -2357,7 +2448,7 @@ @@ -2539,7 +2630,7 @@ @@ -2771,7 +2862,7 @@ @@ -2997,7 +3088,7 @@ @@ -3176,7 +3267,7 @@ @@ -3402,7 +3493,7 @@ @@ -3593,7 +3684,7 @@ @@ -3681,7 +3772,7 @@ prefixed with 'OR'

    @@ -3866,7 +3957,7 @@ prefixed with 'OR'

    @@ -4092,7 +4183,7 @@ prefixed with 'OR'

    @@ -4180,7 +4271,7 @@ prefixed with 'OR NOT'

    @@ -4406,7 +4497,7 @@ prefixed with 'OR NOT'

    @@ -4591,7 +4682,7 @@ prefixed with 'OR NOT'

    @@ -4750,7 +4841,7 @@ prefixed with 'OR NOT'

    @@ -4886,7 +4977,7 @@ prefixed with 'OR NOT'

    @@ -5022,7 +5113,7 @@ prefixed with 'OR NOT'

    @@ -5181,7 +5272,7 @@ prefixed with 'OR NOT'

    @@ -5320,7 +5411,7 @@ prefixed with 'OR NOT'

    @@ -5502,7 +5593,7 @@ prefixed with 'OR NOT'

    @@ -5712,7 +5803,7 @@ prefixed with 'OR NOT'

    @@ -5897,7 +5988,7 @@ prefixed with 'OR NOT'

    @@ -6056,7 +6147,7 @@ prefixed with 'OR NOT'

    @@ -6192,7 +6283,7 @@ prefixed with 'OR NOT'

    @@ -6328,7 +6419,7 @@ prefixed with 'OR NOT'

    @@ -6487,7 +6578,7 @@ prefixed with 'OR NOT'

    @@ -6551,7 +6642,7 @@ prefixed with 'OR NOT'

    Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/module-query-parser.html b/docs/module-query-parser.html index cce42dd..143024d 100644 --- a/docs/module-query-parser.html +++ b/docs/module-query-parser.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -113,7 +117,7 @@ @@ -244,7 +248,7 @@ @@ -294,6 +298,154 @@ + + + + +
    +

    hasOperator(string) → {Array|null}

    + + +
    +
    + + +
    +

    Check if the string contains an operator, and if so, return the operator(s). +If there are no matches, return null

    +
    + + + + + + + +
    Parameters:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    string + + +String + + + +

    the string to check

    + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + +
    + + + + + + + +
    + + + + + + + + + + + +
    Returns:
    + + + + +
    +
    + Type +
    +
    + +Array +| + +null + + +
    +
    + + + + +
    @@ -394,7 +546,7 @@ @@ -438,6 +590,179 @@ + + + + +
    +

    parseWhere(driver, state) → {String}

    + + +
    +
    + + +
    +

    Parse a where clause to separate functions from values

    +
    + + + + + + + +
    Parameters:
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeDescription
    driver + + +Object + + + +
    state + + +State + + + +
    + + + + +
    + + + + + + + + + + + + + + + + + + + + + +
    Source:
    +
    + +
    + + + + + + + +
    + + + + + + + + + + + +
    Returns:
    + + +
    + +
    + + + +
    +
    + Type +
    +
    + +String + + +
    +
    + + + + +
    @@ -466,7 +791,7 @@ Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/modules.list.html b/docs/modules.list.html index 7e4b9f0..f0d840f 100644 --- a/docs/modules.list.html +++ b/docs/modules.list.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -158,7 +162,7 @@ Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/node-query.js.html b/docs/node-query.js.html index 32f214a..7c5a444 100644 --- a/docs/node-query.js.html +++ b/docs/node-query.js.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -151,7 +155,7 @@ module.exports = new NodeQuery(); Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/query-builder.js.html b/docs/query-builder.js.html index 11eda61..c9c607a 100644 --- a/docs/query-builder.js.html +++ b/docs/query-builder.js.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -77,14 +81,15 @@ /** @module query-builder */ var getArgs = require('getargs'), - helpers = require('./helpers'); + helpers = require('./helpers'), + State = require('./state'); /** * Variables controlling the sql building * * @private */ -var state = {}; +var state = new State(); /* * SQL generation object @@ -230,10 +235,17 @@ var QueryBuilder = function(driver, adapter) { var obj = {}; - if (helpers.isScalar(args.$key) && !helpers.isUndefined(args.$val) && !helpers.isNull(args.$val)) + + if (helpers.isScalar(args.$key) && !helpers.isUndefined(args.$val)) { + // Convert key/val pair to a simple object obj[args.$key] = args.$val; } + else if (helpers.isScalar(args.$key) && helpers.isUndefined(args.$val)) + { + // If just a string for the key, and no value, create a simple object with duplicate key/val + obj[args.$key] = args.$key; + } else { obj = args.$key; @@ -252,15 +264,17 @@ var QueryBuilder = function(driver, adapter) { } }); + return state[args.$varName]; }, whereMixedSet: function(/*key, val*/) { var args = getArgs('key:string|object, [val]', arguments); state.whereMap = []; + state.rawWhereValues = []; _p.mixedSet('whereMap', 'both', args.key, args.val); - _p.mixedSet('whereValues', 'value', args.key, args.val); + _p.mixedSet('rawWhereValues', 'value', args.key, args.val); }, fixConjunction: function(conj) { var lastItem = state.queryMap[state.queryMap.length - 1]; @@ -285,24 +299,16 @@ var QueryBuilder = function(driver, adapter) { // Normalize key and value and insert into state.whereMap _p.whereMixedSet(key, val); - Object.keys(state.whereMap).forEach(function(field) { - // Split each key by spaces, in case there - // is an operator such as >, <, !=, etc. - var fieldArray = field.trim().split(' ').map(helpers.stringTrim); + // Parse the where condition to account for operators, + // functions, identifiers, and literal values + state = parser.parseWhere(driver, state); - var item = driver.quoteIdentifiers(fieldArray[0]); - - // Simple key value, or an operator? - item += (fieldArray.length === 1 || fieldArray[1] === '') ? '=?' : " " + fieldArray[1] + " ?"; - - // Determine the correct conjunction + state.whereMap.forEach(function(clause) { var conj = _p.fixConjunction(defaultConj); - - _p.appendMap(conj, item, 'where'); - - // Clear the where Map - state.whereMap = {}; + _p.appendMap(conj, clause, 'where'); }); + + state.whereMap = {}; }, whereNull: function(field, stmt, conj) { field = driver.quoteIdentifiers(field); @@ -318,20 +324,15 @@ var QueryBuilder = function(driver, adapter) { // Normalize key/val and put in state.whereMap _p.whereMixedSet(args.key, args.val); - Object.keys(state.whereMap).forEach(function(field) { - // Split each key by spaces, in case there - // is an operator such as >, <, !=, etc. - var fieldArray = field.split(' ').map(helpers.stringTrim); - - var item = driver.quoteIdentifiers(fieldArray[0]); - - // Simple key value, or an operator? - item += (fieldArray.length === 1) ? '=?' : " " + fieldArray[1] + " ?"; + // Parse the having condition to account for operators, + // functions, identifiers, and literal values + state = parser.parseWhere(driver, state); + state.whereMap.forEach(function(clause) { // Put in the having map state.havingMap.push({ conjunction: (state.havingMap.length > 0) ? " " + args.conj + " " : ' HAVING ', - string: item + string: clause }); }); @@ -366,7 +367,7 @@ var QueryBuilder = function(driver, adapter) { vals = state.values.concat(state.whereValues); } -//console.log(state.queryMap); +//console.log(state); //console.log(sql); //console.log(vals); //console.log(callback); @@ -389,28 +390,7 @@ var QueryBuilder = function(driver, adapter) { return sql; }, resetState: function() { - state = { - // Arrays/Maps - queryMap: [], - values: [], - whereValues: [], - setArrayKeys: [], - orderArray: [], - groupArray: [], - havingMap: [], - whereMap: {}, - - // Partials - selectString: '', - fromString: '', - setString: '', - orderString: '', - groupString: '', - - // Other various values - limit: null, - offset: null - }; + state = new State(); } }; @@ -438,10 +418,14 @@ var QueryBuilder = function(driver, adapter) { return state; }; - // ------------------------------------------------------------------------ - - // Set up state object - this.resetQuery(); + /** + * Closes the database connection for the current adapter + * + * @return void + */ + this.end = function() { + adapter.close(); + }; // ------------------------------------------------------------------------ // ! Query Builder Methods @@ -1041,7 +1025,7 @@ module.exports = QueryBuilder; Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/query-parser.js.html b/docs/query-parser.js.html index 46d73ba..3589314 100644 --- a/docs/query-parser.js.html +++ b/docs/query-parser.js.html @@ -51,6 +51,10 @@ query-parser +
  • + State +
  • + @@ -78,21 +82,39 @@ var helpers = require('./helpers'); var matchPatterns = { - 'function': /([a-zA-Z0-9_]+\((.*?)\))/i, - identifier: /([a-zA-Z0-9_\-]+\.?)+/ig, - operator: /\=|AND|&&?|~|\|\|?|\^|\/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR/i + 'function': /([a-z0-9_]+\((.*)\))/i, + operator: /\!=?|\=|\+|&&?|~|\|\|?|\^|\/|<>|>=?|<=?|\-|%|OR|AND|NOT|XOR/ig, + literal: /([0-9]+)|'(.*?)'|true|false/ig }; +// Full pattern for identifiers +// Making sure that literals and functions aren't matched +matchPatterns.identifier = new RegExp( + '(' + + '(?!' + + matchPatterns['function'].source + '|' + + matchPatterns.literal.source + + ')' + + '([a-z_\-]+[0-9]*\\.?)' + + ')+' +, 'ig'); + // Full pattern for determining ordering of the pieces -matchPatterns.combined = new RegExp(matchPatterns['function'].source + "+|" +matchPatterns.joinCombined = new RegExp( + matchPatterns['function'].source + "+|" + + matchPatterns.literal.source + '+|' + matchPatterns.identifier.source - + '|(' + matchPatterns.operator.source + ')+', 'ig'); + + '|(' + matchPatterns.operator.source + ')+' +, 'ig'); + +var identifierBlacklist = ['true','false','null']; var filterMatches = function(array) { var output = []; // Return non-array matches - if (helpers.isScalar(array) || helpers.isNull(array) || helpers.isUndefined(array)) return output; + if (helpers.isNull(array)) return null; + if (helpers.isScalar(array) || helpers.isUndefined(array)) return output; array.forEach(function(item) { output.push(item); @@ -113,6 +135,17 @@ var QueryParser = function(driver) { // That 'new' keyword is annoying if ( ! (this instanceof QueryParser)) return new QueryParser(driver); + /** + * Check if the string contains an operator, and if so, return the operator(s). + * If there are no matches, return null + * + * @param {String} string - the string to check + * @return {Array|null} + */ + this.hasOperator = function(string) { + return filterMatches(string.match(matchPatterns.operator)); + } + /** * Tokenize the sql into parts for additional processing * @@ -124,12 +157,13 @@ var QueryParser = function(driver) { var output = {}; // Get clause components - matches['function'] = sql.match(matchPatterns['function']); + matches.functions = sql.match(new RegExp(matchPatterns['function'].source, 'ig')); matches.identifiers = sql.match(matchPatterns.identifier); matches.operators = sql.match(matchPatterns.operator); + matches.literals = sql.match(matchPatterns.literal); // Get everything at once for ordering - matches.combined = sql.match(matchPatterns.combined); + matches.combined = sql.match(matchPatterns.joinCombined); // Flatten the matches to increase relevance Object.keys(matches).forEach(function(key) { @@ -158,12 +192,150 @@ var QueryParser = function(driver) { } }); - return parts.combined.join(''); + return parts.combined.join(' '); + }; + + /** + * Parse a where clause to separate functions from values + * + * @param {Object} driver + * @param {State} state + * @return {String} - The parsed/escaped where condition + */ + this.parseWhere = function(driver, state) { + var whereMap = state.whereMap, + whereValues = state.rawWhereValues; + + var outputMap = []; + var outputValues = []; + var that = this; + + Object.keys(whereMap).forEach(function(key) { + // Combine fields, operators, functions and values into a full clause + // to have a common starting flow + var fullClause = ''; + + // Add an explicit = sign where one is inferred + if ( ! that.hasOperator(key)) + { + fullClause = key + ' = ' + whereMap[key]; + } + else if (whereMap[key] === key) + { + fullClause = key; + } + else + { + fullClause = key + ' ' + whereMap[key]; + } + + // Separate the clause into separate pieces + var parts = that.parseJoin(fullClause); + + // Filter explicit literals from lists of matches + if (whereValues.indexOf(whereMap[key]) !== -1) + { + var value = whereMap[key]; + var identIndex = (helpers.isArray(parts.identifiers)) ? parts.identifiers.indexOf(value) : -1; + var litIndex = (helpers.isArray(parts.literals)) ? parts.literals.indexOf(value) : -1; + var combIndex = (helpers.isArray(parts.combined)) ? parts.combined.indexOf(value) : -1; + var funcIndex = (helpers.isArray(parts.functions)) ? parts.functions.indexOf(value) : -1; + var inOutputArray = outputValues.indexOf(value) !== -1; + + // Remove the identifier in question, + // and add to the output values array + if (identIndex !== -1) + { + parts.identifiers.splice(identIndex, 1); + + if ( ! inOutputArray) + { + outputValues.push(value); + inOutputArray = true; + } + } + + // Remove the value from the literals list + // so it is not added twice + if (litIndex !== -1) + { + parts.literals.splice(litIndex, 1); + + if ( ! inOutputArray) + { + outputValues.push(value); + inOutputArray = true; + } + } + + // Remove the value from the combined list + // and replace it with a placeholder + if (combIndex !== -1) + { + // Make sure to skip functions when replacing values + if (funcIndex === -1) + { + parts.combined[combIndex] = '?'; + + if ( ! inOutputArray) + { + outputValues.push(value); + inOutputArray = true; + } + } + } + } + + // Filter false positive identifiers + parts.identifiers = parts.identifiers.filter(function(item) { + var isInCombinedMatches = parts.combined.indexOf(item) !== -1; + var isNotInBlackList = identifierBlacklist.indexOf(item.toLowerCase()) === -1; + + return isInCombinedMatches && isNotInBlackList; + }); + + // Quote identifiers + if (helpers.isArray(parts.identifiers)) + { + parts.identifiers.forEach(function(ident) { + var index = parts.combined.indexOf(ident); + if (index !== -1) + { + parts.combined[index] = driver.quoteIdentifiers(ident); + } + }); + } + + // Replace each literal with a placeholder in the map + // and add the literal to the values, + // This should only apply to literal values that are not + // explicitly mapped to values, but have to be parsed from + // a where condition, + if (helpers.isArray(parts.literals)) + { + parts.literals.forEach(function(lit) { + var litIndex = parts.combined.indexOf(lit); + + if (litIndex !== -1) + { + parts.combined[litIndex] = (helpers.isArray(parts.operators)) ? '?' : '= ?'; + outputValues.push(lit); + } + }); + } + + outputMap.push(parts.combined.join(' ')); + }); + + state.rawWhereValues = []; + state.whereValues = state.whereValues.concat(outputValues); + state.whereMap = outputMap; + + return state; }; }; -module.exports = QueryParser; - +module.exports = QueryParser; @@ -184,7 +356,7 @@ module.exports = QueryParser; Documentation generated by JSDoc 3.3.0-alpha9 - on Mon Dec 1st 2014 using the DocStrap template. diff --git a/docs/state.js.html b/docs/state.js.html new file mode 100644 index 0000000..eb1bee3 --- /dev/null +++ b/docs/state.js.html @@ -0,0 +1,209 @@ + + + + + + DocStrap Source: state.js + + + + + + + + + +
    + + +
    + + +
    + +
    + + + +

    Source: state.js

    + +
    +
    +
    'use strict';
    +
    +/** @module State */
    +module.exports = function State() {
    +	return {
    +		// Arrays/Maps
    +		queryMap: [],
    +		values: [],
    +		whereValues: [],
    +		setArrayKeys: [],
    +		orderArray: [],
    +		groupArray: [],
    +		havingMap: [],
    +		whereMap: {},
    +		rawWhereValues: [],
    +
    +		// Partials
    +		selectString: '',
    +		fromString: '',
    +		setString: '',
    +		orderString: '',
    +		groupString: '',
    +
    +		// Other various values
    +		limit: null,
    +		offset: null
    +	};
    +};
    +// End of module State
    +
    +
    + + + + + +
    + +
    +
    + + + + DocStrap Copyright © 2012-2014 The contributors to the JSDoc3 and DocStrap projects. + +
    + + + Documentation generated by JSDoc 3.3.0-alpha9 + on Fri Jan 23rd 2015 using the DocStrap template. + +
    +
    + + +
    +
    + +
    + + + + + + + + + + + + + + + + + +