001/*
002MIT License
003
004Copyright (c) 2020 FBSQL Team
005
006Permission is hereby granted, free of charge, to any person obtaining a copy
007of this software and associated documentation files (the "Software"), to deal
008in the Software without restriction, including without limitation the rights
009to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010copies of the Software, and to permit persons to whom the Software is
011furnished to do so, subject to the following conditions:
012
013The above copyright notice and this permission notice shall be included in all
014copies or substantial portions of the Software.
015
016THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022SOFTWARE.
023
024Home:   https://fbsql.github.io
025E-Mail: fbsql.team@gmail.com
026*/
027
028package org.fbsql.servlet;
029
030import static org.fbsql.servlet.StringUtils.q;
031
032import java.io.BufferedReader;
033import java.io.IOException;
034import java.io.OutputStream;
035import java.io.StringReader;
036import java.io.Writer;
037import java.lang.reflect.InvocationTargetException;
038import java.lang.reflect.Method;
039import java.nio.charset.StandardCharsets;
040import java.sql.Blob;
041import java.sql.CallableStatement;
042import java.sql.Clob;
043import java.sql.Date;
044import java.sql.ParameterMetaData;
045import java.sql.PreparedStatement;
046import java.sql.ResultSet;
047import java.sql.SQLException;
048import java.sql.Time;
049import java.sql.Timestamp;
050import java.sql.Types;
051import java.text.MessageFormat;
052import java.text.ParseException;
053import java.time.Instant;
054import java.util.ArrayList;
055import java.util.Arrays;
056import java.util.Base64.Decoder;
057import java.util.Calendar;
058import java.util.Collection;
059import java.util.Collections;
060import java.util.LinkedHashMap;
061import java.util.List;
062import java.util.Locale;
063import java.util.Map;
064import java.util.Queue;
065import java.util.TimeZone;
066
067import javax.servlet.AsyncContext;
068import javax.servlet.http.Cookie;
069import javax.servlet.http.HttpServletRequest;
070import javax.servlet.http.HttpServletResponse;
071import javax.servlet.http.HttpSession;
072
073import org.fbsql.antlr4.parser.ParseNativeStmt;
074import org.fbsql.antlr4.parser.ParseStmtConnectTo;
075import org.fbsql.antlr4.parser.ParseStmtConnectTo.StmtConnectTo;
076import org.fbsql.antlr4.parser.ParseStmtDeclareStatement.StmtDeclareStatement;
077import org.fbsql.connection_pool.ConnectionPoolManager;
078import org.fbsql.connection_pool.DbConnection;
079import org.fbsql.json.parser.JsonUtils;
080import org.mozilla.javascript.Context;
081import org.mozilla.javascript.Function;
082import org.mozilla.javascript.NativeArray;
083import org.mozilla.javascript.NativeJSON;
084import org.mozilla.javascript.NativeObject;
085import org.mozilla.javascript.Scriptable;
086
087/**
088 * <p><strong>DbRequestProcessor</strong> contains the processing logic that
089 * the @link(DbServlet) performs as it receives each servlet request
090 * from the container.</p>
091 */
092
093public class DbRequestProcessor implements Runnable {
094
095        /**
096         * SQL NULL constant
097         */
098        private static final String SQL_NULL = "NULL"; // SQL NULL constant
099
100        /**
101         * SQL FALSE constant
102         */
103        private static final String SQL_FALSE = "0"; // SQL FALSE constant
104
105        /**
106         * SQL TRUE constant
107         */
108        private static final String SQL_TRUE = "1"; // SQL TRUE constant
109
110        /**
111         * SQL quote character
112         */
113        private static final String SQL_QUOTE_CHAR = "'"; // SQL quote character
114
115        /**
116         * Execute type: query
117         * WARNING! This constant used also in JavaScript part
118         */
119        private static final String EXEC_TYPE_QUERY = "Q";
120
121        /**
122         * Execute type: update
123         * WARNING! This constant used also in JavaScript part
124         */
125        private static final String EXEC_TYPE_UPDATE = "U";
126
127        /*
128         * Built-in functions
129         */
130
131        /* functions with parameter */
132        private static final String FUT_IN_ROLE                               = "IN_ROLE(";
133        private static final String FUT_GET_COOKIE                            = "COOKIE(";
134        private static final String FUT_GET_HTTP_SESSION_ATTRIBUTE_AS_CHAR    = "GET_HTTP_SESSION_ATTRIBUTE_AS_CHAR(";
135        private static final String FUT_GET_HTTP_SESSION_ATTRIBUTE_AS_INTEGER = "GET_HTTP_SESSION_ATTRIBUTE_AS_INTEGER(";
136        private static final String FUT_GET_HTTP_HEADER_AS_CHAR               = "GET_HTTP_HEADER_AS_CHAR(";
137        private static final String FUT_GET_HTTP_HEADER_AS_DATE               = "GET_HTTP_HEADER_AS_DATE(";
138        private static final String FUT_GET_HTTP_HEADER_AS_INTEGER            = "GET_HTTP_HEADER_AS_INTEGER(";
139
140        /* functions without parameters */
141        private static final String FUN_REMOTE_USER                       = "REMOTE_USER()";
142        private static final String FUN_REMOTE_ROLE                       = "REMOTE_ROLE()";
143        private static final String FUN_REMOTE_SESSION_ID                 = "REMOTE_SESSION_ID()";
144        private static final String FUN_REMOTE_SESSION_CREATION_TIME      = "REMOTE_SESSION_CREATION_TIME()";
145        private static final String FUN_REMOTE_SESSION_LAST_ACCESSED_TIME = "REMOTE_SESSION_LAST_ACCESSED_TIME()";
146        private static final String FUN_REMOTE_USER_INFO                  = "REMOTE_USER_INFO()";
147
148        /* system constants (See JavaSctript class Constants in fbsql.js */
149        private static final String FBSQL_REMOTE_USER                       = "FBSQL_REMOTE_USER";
150        private static final String FBSQL_REMOTE_ROLE                       = "FBSQL_REMOTE_ROLE";
151        private static final String FBSQL_REMOTE_SESSION_ID                 = "FBSQL_REMOTE_SESSION_ID";
152        private static final String FBSQL_REMOTE_SESSION_CREATION_TIME      = "FBSQL_REMOTE_SESSION_CREATION_TIME";
153        private static final String FBSQL_REMOTE_SESSION_LAST_ACCESSED_TIME = "FBSQL_REMOTE_SESSION_LAST_ACCESSED_TIME";
154        private static final String FBSQL_REMOTE_USER_INFO                  = "FBSQL_REMOTE_USER_INFO";
155
156        private String        instanceName;
157        private String        instanceDirectory;
158        private StmtConnectTo stmtConnectTo;
159        private AsyncContext  asyncContext;
160
161        private boolean               debug;
162        private ConnectionPoolManager connectionPoolManager;
163
164        private Map<String /* SQL statement name */, StmtDeclareStatement>                declaredStatementsMap; // list of SQL statements
165        private Map<String /* stored procedure name */, NonNativeProcedure>               proceduresMap;
166        private Map<String /* js file name */, Scriptable>                                mapScopes;
167        private Map<String /* js file name */, Map<String /* function name */, Function>> mapFunctions;
168
169        private Map<StaticStatement, ReadyResult> mapJson;
170        private Queue<AsyncContext>               ongoingRequests;
171        private DbServlet.SharedCoder             sharedCoder;
172        private ParseNativeStmt                   parseNativeStmt;
173
174        /**
175         * Constructs DbRequestProcessor object
176         * 
177         * @param instanceName
178         * @param stmtConnectTo
179         * @param asyncCtx
180         * @param debug
181         * @param connectionPoolManager
182         * @param declaredStatementsMap
183         * @param proceduresMap
184         * @param mapScopes
185         * @param mapFunctions
186         * @param mapJson
187         * @param ongoingRequests
188         * @param sharedCoder
189         */
190        public DbRequestProcessor( //
191                        String instanceName, //
192                        String instanceDirectory, //
193                        StmtConnectTo stmtConnectTo, //
194                        AsyncContext asyncCtx, //
195                        boolean debug, //
196                        ConnectionPoolManager connectionPoolManager, //
197                        Map<String /* SQL statement name */, StmtDeclareStatement> declaredStatementsMap, // list of SQL statements
198                        Map<String /* stored procedure name */, NonNativeProcedure> proceduresMap, //
199                        Map<String /* js file name */, Scriptable> mapScopes, //
200                        Map<String /* js file name */, Map<String /* function name */, Function>> mapFunctions, //
201                        Map<StaticStatement, ReadyResult> mapJson, //
202                        Queue<AsyncContext> ongoingRequests, //
203                        ParseNativeStmt parseNativeStmt, //
204                        DbServlet.SharedCoder sharedCoder //
205        ) {
206                this.instanceName          = instanceName;
207                this.instanceDirectory     = instanceDirectory;
208                this.stmtConnectTo         = stmtConnectTo;
209                this.asyncContext          = asyncCtx;
210                this.debug                 = debug;
211                this.connectionPoolManager = connectionPoolManager;
212                this.declaredStatementsMap = declaredStatementsMap; // list of SQL statements
213                this.proceduresMap         = proceduresMap;
214                this.mapScopes             = mapScopes;
215                this.mapFunctions          = mapFunctions;
216                this.mapJson               = mapJson;
217                this.ongoingRequests       = ongoingRequests;
218                this.parseNativeStmt       = parseNativeStmt;
219                this.sharedCoder           = sharedCoder;
220        }
221
222        /**
223         * Implementation of Runnable.run() method
224         * 
225         * Overview of request processor logic
226         * - Parse request body
227         * - Check SQL statement against white list (list of allowed SQL statements)
228         * - Execute SQL statement if allowed
229         */
230        @Override
231        public void run() {
232                HttpServletRequest  request  = (HttpServletRequest) asyncContext.getRequest();
233                HttpServletResponse response = (HttpServletResponse) asyncContext.getResponse();
234
235                try {
236                        String etagFromClient = request.getHeader("If-None-Match");
237
238                        String   remoteUser = (String) request.getAttribute(DbServlet.REQUEST_ATTRIBUTE_USER);
239                        String   remoteRole = request.getHeader(DbServlet.CUSTOM_HTTP_HEADER_ROLE);
240                        Cookie[] cookies    = request.getCookies();
241                        //
242                        HttpSession session = request.getSession();
243
244                        boolean   reject        = false;
245                        String    rejectMessage = null;
246                        Throwable exception     = null;
247                        //
248                        String              clientInfoJson  = getClientInfo(request, sharedCoder.decoder);
249                        BufferedReader      br              = getCustomDataReader(request, sharedCoder.decoder);
250                        String              jsonLine        = br.readLine();
251                        Map<String, String> bodyMap         = JsonUtils.parseJsonObject(jsonLine);
252                        String              statementId     = (String) JsonUtils.parseJson(bodyMap.get("statementId"));
253                        String              execType        = (String) JsonUtils.parseJson(bodyMap.get("execType"));
254                        String              jsonStrFormat   = bodyMap.get("format");
255                        Integer             resultSetFormat = JsonUtils.parseJsonInt(jsonStrFormat);
256                        String              parameters      = bodyMap.get("parameters");
257                        CharSequence        sessionInfoJson = HttpRequestUtils.generateSessionInfoJson(request, sharedCoder.encoder);
258                        String              userInfoJson    = generateUserInfoJson(                                                  //
259                                        request,                                                                                             //
260                                        clientInfoJson,                                                                                      //
261                                        sessionInfoJson                                                                                      //
262                        );
263
264                        String updateResultJson = null;
265
266                        String  paramJsonArray;
267                        boolean batch;
268                        if (parameters.startsWith("[")) {
269                                batch          = true;
270                                paramJsonArray = parameters;
271                        } else {
272                                batch          = false;
273                                paramJsonArray = '[' + parameters + ']';
274                        }
275
276                        List<String> paramJsons = JsonUtils.parseJsonArray(paramJsonArray);
277
278                        boolean executeTypeQuery  = execType.equals(EXEC_TYPE_QUERY);
279                        boolean executeTypeUpdate = execType.equals(EXEC_TYPE_UPDATE);
280
281                        String                    unprocessedNamedPreparedStatement = null;
282                        String                    namedPreparedStatement            = null;
283                        List<Map<String, Object>> parametersListOfMaps              = null;
284                        StmtDeclareStatement      stmtDeclareStatement              = null;
285
286                        if (statementId == null) { // SQL statement provided
287                                StringBuilder unprocessedNamedPreparedStatementSb = new StringBuilder();
288                                while (true) {
289                                        String line = br.readLine();
290                                        if (line == null)
291                                                break;
292                                        unprocessedNamedPreparedStatementSb.append(' ' + line.trim() + '\n');
293                                }
294                                unprocessedNamedPreparedStatement = unprocessedNamedPreparedStatementSb.toString().trim();
295                                namedPreparedStatement            = SqlParseUtils.processStatement(unprocessedNamedPreparedStatement);
296                                for (StmtDeclareStatement curDeclareStatement : declaredStatementsMap.values()) {
297                                        if (curDeclareStatement.statement.equals(namedPreparedStatement)) {
298                                                statementId          = curDeclareStatement.alias;
299                                                stmtDeclareStatement = declaredStatementsMap.get(statementId);
300                                                break;
301                                        }
302                                }
303                                reject = stmtDeclareStatement == null && !stmtConnectTo.exposeUndeclaredStatements;
304                                if (reject)
305                                        rejectMessage = StringUtils.escapeJson(MessageFormat.format("Rejected. SQL statement \"{0}\" not exposed to frontend", namedPreparedStatement));
306                        } else { // SQL statement name provided
307                                reject = statementId.startsWith(ParseStmtConnectTo.NONEXPOSABLE_NAME_PREFIX);
308                                final String NAME_NOT_FOUND_MSG = StringUtils.escapeJson(MessageFormat.format("Rejected. SQL statement name: ''{0}'' not found", statementId));
309                                if (reject) // wrong name format
310                                        rejectMessage = NAME_NOT_FOUND_MSG;
311                                else {
312                                        stmtDeclareStatement = declaredStatementsMap.get(statementId);
313                                        if (stmtDeclareStatement == null) {
314                                                reject        = true;
315                                                rejectMessage = NAME_NOT_FOUND_MSG;
316                                        } else {
317                                                namedPreparedStatement            = stmtDeclareStatement.statement;
318                                                unprocessedNamedPreparedStatement = namedPreparedStatement;
319                                        }
320                                }
321                        }
322
323                        if (stmtDeclareStatement != null) {
324                                Collection<String> allowedRoles = stmtDeclareStatement.roles;
325                                if (!allowedRoles.isEmpty())
326                                        if (!allowedRoles.contains(remoteRole)) {
327                                                reject        = true;
328                                                rejectMessage = StringUtils.escapeJson(MessageFormat.format("Rejected. SQL statement \"{0}\" not allowed for role \"{1}\"", namedPreparedStatement, remoteRole));
329                                        }
330                        }
331
332                        //
333                        // validate
334                        //
335                        parametersListOfMaps = new ArrayList<>(paramJsons.size());
336                        for (String paramJson : paramJsons) {
337                                if (stmtDeclareStatement != null) {
338                                        String validatorStoredProcedureName = stmtDeclareStatement.trigger_before_procedure_name;
339                                        if (validatorStoredProcedureName != null) {
340                                                String statementInfoJson = generateStatementInfoJson( //
341                                                                instanceName, //
342                                                                statementId, //
343                                                                unprocessedNamedPreparedStatement, //
344                                                                paramJson //
345                                                );
346
347                                                DbConnection dbConnection0      = null;
348                                                String       modifiedParamsJson = null;
349                                                try {
350                                                        dbConnection0 = connectionPoolManager.getConnection();
351
352                                                        NonNativeProcedure nonNativeProcedure = proceduresMap.get(validatorStoredProcedureName);
353                                                        if (nonNativeProcedure != null) { // Java or JavaScript
354                                                                if (nonNativeProcedure.procedureType == ProcedureType.JAVA || nonNativeProcedure.procedureType == ProcedureType.JS) { // Java or JavaScript
355
356                                                                        List<Object> parameterValues = new ArrayList<>();
357                                                                        parameterValues.add(request);
358                                                                        parameterValues.add(response);
359                                                                        parameterValues.add(dbConnection0.getConnection());
360                                                                        parameterValues.add(instanceName);
361                                                                        parameterValues.add(userInfoJson);
362                                                                        parameterValues.add(statementInfoJson);
363                                                                        Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
364
365                                                                        Object obj = null;
366                                                                        if (nonNativeProcedure.procedureType == ProcedureType.JAVA) { // Java
367                                                                                Method method = CallUtils.getMethod(nonNativeProcedure.optionsJson);
368                                                                                try {
369                                                                                        obj = (String) method.invoke(null, parametersArray);
370                                                                                } catch (InvocationTargetException e) {
371                                                                                        exception = e.getTargetException();
372                                                                                }
373                                                                        } else if (nonNativeProcedure.procedureType == ProcedureType.JS) { // JavaScript
374                                                                                //
375                                                                                // initize Rhino
376                                                                                // https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino
377                                                                                //
378                                                                                Context ctx = Context.enter();
379                                                                                try {
380                                                                                        ctx.setLanguageVersion(Context.VERSION_1_7);
381                                                                                        ctx.setOptimizationLevel(9); // Rhino optimization: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Optimization
382                                                                                        JsFunction jsFunction = CallUtils.getFunction(instanceDirectory, nonNativeProcedure.optionsJson, mapScopes, mapFunctions);
383                                                                                        try {
384                                                                                                obj = jsFunction.function.call(ctx, jsFunction.scope, null, parametersArray);
385                                                                                        } catch (Exception e) {
386                                                                                                exception = e.getCause();
387                                                                                        }
388                                                                                        if (obj instanceof NativeObject)
389                                                                                                obj = (String) NativeJSON.stringify(ctx, jsFunction.scope, obj, null, null); // to string
390                                                                                } finally {
391                                                                                        ctx.exit();
392                                                                                }
393                                                                        }
394                                                                        if (obj instanceof ResultSet)
395                                                                                try (ResultSet rs = (ResultSet) obj) {
396                                                                                        if (rs.next())
397                                                                                                modifiedParamsJson = rs.getString(1);
398                                                                                } catch (SQLException e) {
399                                                                                        exception = e.getCause();
400                                                                                }
401                                                                        else if (obj instanceof CharSequence)
402                                                                                modifiedParamsJson = obj.toString();
403                                                                } else if (nonNativeProcedure.procedureType == ProcedureType.EXEC) { // OS
404                                                                        List<Object> parameterValues = new ArrayList<>();
405                                                                        parameterValues.add(instanceName);
406                                                                        parameterValues.add(userInfoJson);
407                                                                        parameterValues.add(statementInfoJson);
408                                                                        Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
409
410                                                                        modifiedParamsJson = CallUtils.executeOsProgramm(instanceDirectory, nonNativeProcedure.optionsJson, parametersArray);
411
412                                                                } else if (nonNativeProcedure.procedureType == ProcedureType.URL) { // URL
413                                                                        Map<String, Object> parametersMap = new LinkedHashMap<>();
414                                                                        parametersMap.put("instanceName", instanceName);
415                                                                        parametersMap.put("userInfoJson", userInfoJson);
416                                                                        parametersMap.put("statementInfoJson", statementInfoJson);
417
418                                                                        modifiedParamsJson = CallUtils.executeUrl(instanceDirectory, nonNativeProcedure.optionsJson, parametersMap);
419                                                                }
420                                                        } else { // Native
421                                                                CallableStatement cs = dbConnection0.getCallableStatement("{call " + validatorStoredProcedureName + "(?,?)}");
422                                                                cs.setString(1, instanceName);
423                                                                cs.setString(2, userInfoJson);
424                                                                cs.setString(3, statementInfoJson);
425
426                                                                try (ResultSet rs = cs.executeQuery()) {
427                                                                        if (rs.next())
428                                                                                modifiedParamsJson = rs.getString(1);
429                                                                }
430                                                        }
431                                                } finally {
432                                                        if (dbConnection0 != null)
433                                                                connectionPoolManager.releaseConnection(dbConnection0);
434                                                        if (modifiedParamsJson == null) { // reject if stored procedure return null
435                                                                reject = true;
436                                                                if (exception == null || exception.getMessage() == null)
437                                                                        rejectMessage = StringUtils.escapeJson(MessageFormat.format("Rejected. Message: \"Rejected by trigger\". Procedure: {0}. SQL statement: \"{1}\". Parameters: {2}.", validatorStoredProcedureName, namedPreparedStatement, paramJson));
438                                                                else
439                                                                        rejectMessage = exception.getMessage();
440                                                        } else
441                                                                paramJson = modifiedParamsJson.trim();
442                                                }
443                                        }
444                                }
445
446                                Map<String, String> parametersJsonStrs = JsonUtils.parseJsonObject(paramJson);
447                                Map<String, Object> parametersMap      = new LinkedHashMap<>(parametersJsonStrs.size());
448                                for (Map.Entry<String, String> parameterNameValueEntry : parametersJsonStrs.entrySet()) {
449                                        String pname      = parameterNameValueEntry.getKey();
450                                        String pvalueJson = parameterNameValueEntry.getValue();
451                                        Object pvalueObj  = JsonUtils.parseJson(pvalueJson);
452                                        parametersMap.put(pname, pvalueObj);
453                                }
454                                parametersListOfMaps.add(parametersMap);
455                        }
456                        if (reject) {
457                                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
458                                try (OutputStream os = response.getOutputStream()) {
459                                        os.write(('{' + q("message") + ':' + q(rejectMessage) + '}').getBytes(StandardCharsets.UTF_8));
460                                        os.flush();
461                                }
462                                asyncContext.complete();
463                                return;
464                        }
465
466                        StringBuilder                                     preparedStatementSb = new StringBuilder();
467                        Map<String /* name */, List<Integer /* index */>> mapParams           = SqlParseUtils.parseNamedPreparedStatement(namedPreparedStatement, preparedStatementSb);
468                        String                                            preparedStatement   = preparedStatementSb.toString();
469
470                        //
471                        // Replace built-in functions with values
472                        //
473                        preparedStatement = preparedStatement.replace(FUN_REMOTE_USER, remoteUser == null ? SQL_NULL : SQL_QUOTE_CHAR + remoteUser + SQL_QUOTE_CHAR);
474                        preparedStatement = preparedStatement.replace(FUN_REMOTE_ROLE, remoteRole == null ? SQL_NULL : SQL_QUOTE_CHAR + remoteRole + SQL_QUOTE_CHAR);
475                        preparedStatement = preparedStatement.replace(FUN_REMOTE_SESSION_ID, SQL_QUOTE_CHAR + session.getId() + SQL_QUOTE_CHAR);
476                        preparedStatement = preparedStatement.replace(FUN_REMOTE_SESSION_CREATION_TIME, Long.toString(session.getCreationTime()));
477                        preparedStatement = preparedStatement.replace(FUN_REMOTE_SESSION_LAST_ACCESSED_TIME, Long.toString(session.getLastAccessedTime()));
478                        preparedStatement = preparedStatement.replace(FUN_REMOTE_USER_INFO, userInfoJson == null ? SQL_NULL : SQL_QUOTE_CHAR + userInfoJson + SQL_QUOTE_CHAR);
479
480                        //
481                        // Replace IN_ROLE() built-in function with value
482                        //
483                        while (true) {
484                                int offset = SqlParseUtils.indexOf(preparedStatement, FUT_IN_ROLE);
485                                if (offset == -1)
486                                        break;
487                                int     pos1   = offset + FUT_IN_ROLE.length();
488                                char    quote  = preparedStatement.charAt(pos1);       // get single «'» or double «"» quote
489                                String  s      = preparedStatement.substring(pos1 + 1);
490                                int     pos2   = s.indexOf(quote);
491                                String  role   = s.substring(0, pos2);
492                                boolean inRole = request.isUserInRole(role);
493                                if (!inRole)
494                                        inRole = role.equals(remoteRole);
495                                preparedStatement = preparedStatement.substring(0, offset) + (inRole ? SQL_TRUE : SQL_FALSE) + preparedStatement.substring(pos1 + role.length() + 3);
496                        }
497
498                        //
499                        // Replace COOKIE() built-in function with value
500                        //
501                        while (true) {
502                                int offset = SqlParseUtils.indexOf(preparedStatement, FUT_GET_COOKIE);
503                                if (offset == -1)
504                                        break;
505                                int    pos1       = offset + FUT_GET_COOKIE.length();
506                                char   quote      = preparedStatement.charAt(pos1);       // get single «'» or double «"» quote
507                                String s          = preparedStatement.substring(pos1 + 1);
508                                int    pos2       = s.indexOf(quote);
509                                String cookieName = s.substring(0, pos2);
510                                String value      = null;
511                                for (Cookie cookie : cookies) {
512                                        if (cookieName.equals(cookie.getName())) {
513                                                value = cookie.getValue();
514                                                break;
515                                        }
516                                }
517                                preparedStatement = preparedStatement.substring(0, offset) + (value == null ? SQL_NULL : SQL_QUOTE_CHAR + value + SQL_QUOTE_CHAR) + preparedStatement.substring(pos1 + cookieName.length() + 3);
518                        }
519
520                        //
521                        // Replace GET_HTTP_SESSION_ATTRIBUTE_AS_CHAR() built-in function with value
522                        //
523                        while (true) {
524                                int offset = SqlParseUtils.indexOf(preparedStatement, FUT_GET_HTTP_SESSION_ATTRIBUTE_AS_CHAR);
525                                if (offset == -1)
526                                        break;
527                                int    pos1           = offset + FUT_GET_HTTP_SESSION_ATTRIBUTE_AS_CHAR.length();
528                                char   quote          = preparedStatement.charAt(pos1);                          // get single «'» or double «"» quote
529                                String s              = preparedStatement.substring(pos1 + 1);
530                                int    pos2           = s.indexOf(quote);
531                                String attributeName  = s.substring(0, pos2);
532                                Object attributeValue = session.getAttribute(attributeName);
533                                preparedStatement = preparedStatement.substring(0, offset) + (attributeValue == null ? SQL_NULL : SQL_QUOTE_CHAR + attributeValue.toString() + SQL_QUOTE_CHAR) + preparedStatement.substring(pos1 + attributeName.length() + 3);
534                        }
535
536                        //
537                        // Replace FUT_GET_HTTP_SESSION_ATTRIBUTE_AS_INTEGER() built-in function with value
538                        //
539                        while (true) {
540                                int offset = SqlParseUtils.indexOf(preparedStatement, FUT_GET_HTTP_SESSION_ATTRIBUTE_AS_INTEGER);
541                                if (offset == -1)
542                                        break;
543                                int    pos1           = offset + FUT_GET_HTTP_SESSION_ATTRIBUTE_AS_INTEGER.length();
544                                char   quote          = preparedStatement.charAt(pos1);                             // get single «'» or double «"» quote
545                                String s              = preparedStatement.substring(pos1 + 1);
546                                int    pos2           = s.indexOf(quote);
547                                String attributeName  = s.substring(0, pos2);
548                                Object attributeValue = session.getAttribute(attributeName);
549                                preparedStatement = preparedStatement.substring(0, offset) + (attributeValue == null ? SQL_NULL : attributeValue.toString()) + preparedStatement.substring(pos1 + attributeName.length() + 3);
550                        }
551
552                        //
553                        // Replace GET_HTTP_HEADER_AS_CHAR() built-in function with value
554                        //
555                        while (true) {
556                                int offset = SqlParseUtils.indexOf(preparedStatement, FUT_GET_HTTP_HEADER_AS_CHAR);
557                                if (offset == -1)
558                                        break;
559                                int    pos1       = offset + FUT_GET_HTTP_HEADER_AS_CHAR.length();
560                                char   quote      = preparedStatement.charAt(pos1);               // get single «'» or double «"» quote
561                                String s          = preparedStatement.substring(pos1 + 1);
562                                int    pos2       = s.indexOf(quote);
563                                String headerName = s.substring(0, pos2);
564                                String header     = request.getHeader(headerName);
565                                preparedStatement = preparedStatement.substring(0, offset) + (header == null ? SQL_NULL : SQL_QUOTE_CHAR + header + SQL_QUOTE_CHAR) + preparedStatement.substring(pos1 + headerName.length() + 3);
566                        }
567
568                        //
569                        // Replace GET_HTTP_HEADER_AS_INTEGER() built-in function with value
570                        //
571                        while (true) {
572                                int offset = SqlParseUtils.indexOf(preparedStatement, FUT_GET_HTTP_HEADER_AS_INTEGER);
573                                if (offset == -1)
574                                        break;
575                                int    pos1       = offset + FUT_GET_HTTP_HEADER_AS_INTEGER.length();
576                                char   quote      = preparedStatement.charAt(pos1);                  // get single «'» or double «"» quote
577                                String s          = preparedStatement.substring(pos1 + 1);
578                                int    pos2       = s.indexOf(quote);
579                                String headerName = s.substring(0, pos2);
580                                int    header     = request.getIntHeader(headerName);
581                                preparedStatement = preparedStatement.substring(0, offset) + (header == -1 ? SQL_NULL : header) + preparedStatement.substring(pos1 + headerName.length() + 3);
582                        }
583
584                        //
585                        // Replace GET_HTTP_HEADER_AS_DATE() built-in function with value
586                        //
587                        while (true) {
588                                int offset = SqlParseUtils.indexOf(preparedStatement, FUT_GET_HTTP_HEADER_AS_DATE);
589                                if (offset == -1)
590                                        break;
591                                int    pos1       = offset + FUT_GET_HTTP_HEADER_AS_DATE.length();
592                                char   quote      = preparedStatement.charAt(pos1);               // get single «'» or double «"» quote
593                                String s          = preparedStatement.substring(pos1 + 1);
594                                int    pos2       = s.indexOf(quote);
595                                String headerName = s.substring(0, pos2);
596                                long   header     = request.getDateHeader(headerName);
597                                preparedStatement = preparedStatement.substring(0, offset) + (header == -1 ? SQL_NULL : header) + preparedStatement.substring(pos1 + headerName.length() + 3);
598                        }
599
600                        ReadyResult readyResult = mapJson.get(new StaticStatement(preparedStatement, resultSetFormat));
601                        String      etag        = null;
602                        boolean     compressed  = false;
603                        byte[]      bs          = null;
604
605                        if (readyResult == null) {
606                                if (debug) {
607                                        System.out.println("Statement was delegated to underlying database:");
608                                        System.out.println(unprocessedNamedPreparedStatement);
609                                }
610                                DbConnection dbConnection = connectionPoolManager.getConnection();
611                                try {
612                                        NonNativeProcedure nonNativeProcedure = CallUtils.getCallStatementNonNativeProcedure(unprocessedNamedPreparedStatement, proceduresMap);
613
614                                        if (nonNativeProcedure != null) {
615                                                if (nonNativeProcedure.procedureType == ProcedureType.JAVA) { // Java
616                                                        Method method = CallUtils.getMethod(nonNativeProcedure.optionsJson);
617                                                        if (executeTypeUpdate) {
618                                                                int rowCount = 0;
619                                                                for (Map<String, Object> parametersMap : parametersListOfMaps) {
620                                                                        List<Object> parameterValues = new ArrayList<>();
621                                                                        parameterValues.add(request);
622                                                                        parameterValues.add(response);
623                                                                        parameterValues.add(dbConnection.getConnection());
624                                                                        parameterValues.add(instanceName);
625                                                                        CallUtils.getCallStatementParameterValues(parseNativeStmt, unprocessedNamedPreparedStatement, parametersMap, parameterValues);
626                                                                        Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
627
628                                                                        method.invoke(null, parametersArray);
629                                                                        rowCount++;
630                                                                }
631                                                                bs = simpleExecuteUpdateResultJson(rowCount).getBytes(StandardCharsets.UTF_8);
632                                                        } else if (executeTypeQuery) {
633                                                                Map<String, Object> parametersMap   = parametersListOfMaps.get(0);
634                                                                List<Object>        parameterValues = new ArrayList<>();
635                                                                parameterValues.add(request);
636                                                                parameterValues.add(response);
637                                                                parameterValues.add(dbConnection.getConnection());
638                                                                parameterValues.add(instanceName);
639                                                                CallUtils.getCallStatementParameterValues(parseNativeStmt, unprocessedNamedPreparedStatement, parametersMap, parameterValues);
640                                                                Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
641
642                                                                Object                                                       obj         = method.invoke(null, parametersArray);
643                                                                Integer                                                      compression = stmtDeclareStatement == null ? CompressionLevel.BEST_COMPRESSION : stmtDeclareStatement.compressionLevel;
644                                                                List<Map<String /* column name */, String /* JSON value */>> list        = null;
645                                                                if (obj instanceof ResultSet)
646                                                                        list = QueryUtils.resutlSetToListOfMapsJsonValues((ResultSet) obj, sharedCoder.encoder);
647                                                                else if (obj instanceof CharSequence)
648                                                                        list = QueryUtils.convertJsonArrayOfObjectsToListOfMaps(obj.toString());
649                                                                ReadyResult rr = QueryUtils.createReadyResult(list, resultSetFormat, compression, sharedCoder.encoder);
650                                                                bs         = rr.bs;
651                                                                etag       = rr.etag;
652                                                                compressed = rr.compressed;
653                                                        }
654                                                } else if (nonNativeProcedure.procedureType == ProcedureType.JS) { // JavaScript
655                                                        //
656                                                        // initize Rhino
657                                                        // https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino
658                                                        //
659                                                        Context ctx = Context.enter();
660
661                                                        try {
662                                                                ctx.setLanguageVersion(Context.VERSION_1_7);
663                                                                ctx.setOptimizationLevel(9); // Rhino optimization: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Optimization
664                                                                JsFunction jsFunction = CallUtils.getFunction(instanceDirectory, nonNativeProcedure.optionsJson, mapScopes, mapFunctions);
665
666                                                                if (executeTypeUpdate) {
667                                                                        int rowCount = 0;
668                                                                        for (Map<String, Object> parametersMap : parametersListOfMaps) {
669                                                                                List<Object> parameterValues = new ArrayList<>();
670                                                                                parameterValues.add(request);
671                                                                                parameterValues.add(response);
672                                                                                parameterValues.add(dbConnection.getConnection());
673                                                                                parameterValues.add(instanceName);
674                                                                                CallUtils.getCallStatementParameterValues(parseNativeStmt, unprocessedNamedPreparedStatement, parametersMap, parameterValues);
675                                                                                Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
676
677                                                                                jsFunction.function.call(ctx, jsFunction.scope, null, parametersArray);
678                                                                                rowCount++;
679                                                                        }
680                                                                        bs = simpleExecuteUpdateResultJson(rowCount).getBytes(StandardCharsets.UTF_8);
681                                                                } else if (executeTypeQuery) {
682                                                                        Map<String, Object> parametersMap   = parametersListOfMaps.get(0);
683                                                                        List<Object>        parameterValues = new ArrayList<>();
684                                                                        parameterValues.add(request);
685                                                                        parameterValues.add(response);
686                                                                        parameterValues.add(dbConnection.getConnection());
687                                                                        parameterValues.add(instanceName);
688                                                                        CallUtils.getCallStatementParameterValues(parseNativeStmt, unprocessedNamedPreparedStatement, parametersMap, parameterValues);
689                                                                        Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
690
691                                                                        Object                                                       obj         = jsFunction.function.call(ctx, jsFunction.scope, null, parametersArray);
692                                                                        Integer                                                      compression = stmtDeclareStatement == null ? CompressionLevel.BEST_COMPRESSION : stmtDeclareStatement.compressionLevel;
693                                                                        List<Map<String /* column name */, String /* JSON value */>> list        = null;
694                                                                        if (obj instanceof NativeObject || obj instanceof NativeArray) { // return JSON object that will sent to client
695                                                                                String json = (String) NativeJSON.stringify(ctx, jsFunction.scope, obj, null, null);
696                                                                                if (json.startsWith("{") && json.endsWith("}"))
697                                                                                        json = '[' + json + ']';
698                                                                                list = QueryUtils.convertJsonArrayOfObjectsToListOfMaps(json);
699                                                                        } else if (obj instanceof ResultSet)
700                                                                                list = QueryUtils.resutlSetToListOfMapsJsonValues((ResultSet) obj, sharedCoder.encoder);
701                                                                        else if (obj instanceof CharSequence)
702                                                                                list = QueryUtils.convertJsonArrayOfObjectsToListOfMaps(obj.toString());
703                                                                        ReadyResult rr = QueryUtils.createReadyResult(list, resultSetFormat, compression, sharedCoder.encoder);
704                                                                        bs         = rr.bs;
705                                                                        etag       = rr.etag;
706                                                                        compressed = rr.compressed;
707                                                                }
708                                                        } catch (Exception e) {
709                                                                e.printStackTrace();
710                                                        } finally {
711                                                                ctx.exit();
712                                                        }
713                                                } else if (nonNativeProcedure.procedureType == ProcedureType.EXEC) { // OS
714                                                        if (executeTypeUpdate) {
715                                                                int rowCount = 0;
716                                                                for (Map<String, Object> parametersMap : parametersListOfMaps) {
717                                                                        List<Object> parameterValues = new ArrayList<>();
718                                                                        parameterValues.add(instanceName);
719                                                                        CallUtils.getCallStatementParameterValues(parseNativeStmt, unprocessedNamedPreparedStatement, parametersMap, parameterValues);
720                                                                        Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
721
722                                                                        CallUtils.executeOsProgramm(instanceDirectory, nonNativeProcedure.optionsJson, parametersArray);
723                                                                        rowCount++;
724                                                                }
725                                                                bs = simpleExecuteUpdateResultJson(rowCount).getBytes(StandardCharsets.UTF_8);
726                                                        } else if (executeTypeQuery) {
727                                                                Map<String, Object> parametersMap   = parametersListOfMaps.get(0);
728                                                                List<Object>        parameterValues = new ArrayList<>();
729                                                                parameterValues.add(instanceName);
730                                                                CallUtils.getCallStatementParameterValues(parseNativeStmt, unprocessedNamedPreparedStatement, parametersMap, parameterValues);
731                                                                Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
732
733                                                                String jsonArray = CallUtils.executeOsProgramm(instanceDirectory, nonNativeProcedure.optionsJson, parametersArray);
734
735                                                                Integer                                                      compression = stmtDeclareStatement == null ? CompressionLevel.BEST_COMPRESSION : stmtDeclareStatement.compressionLevel;
736                                                                List<Map<String /* column name */, String /* JSON value */>> list        = QueryUtils.convertJsonArrayOfObjectsToListOfMaps(jsonArray);
737                                                                ReadyResult                                                  rr          = QueryUtils.createReadyResult(list, resultSetFormat, compression, sharedCoder.encoder);
738                                                                bs         = rr.bs;
739                                                                etag       = rr.etag;
740                                                                compressed = rr.compressed;
741                                                        }
742                                                } else if (nonNativeProcedure.procedureType == ProcedureType.URL) { // URL
743                                                        if (executeTypeUpdate) {
744                                                                int rowCount = 0;
745                                                                for (Map<String, Object> parametersMap : parametersListOfMaps) {
746                                                                        Map<String, Object> parametersMap0 = new LinkedHashMap<>();
747                                                                        parametersMap0.put("instanceName", instanceName);
748                                                                        parametersMap0.putAll(parametersMap);
749
750                                                                        CallUtils.executeUrl(instanceDirectory, nonNativeProcedure.optionsJson, parametersMap0);
751                                                                        rowCount++;
752                                                                }
753                                                                bs = simpleExecuteUpdateResultJson(rowCount).getBytes(StandardCharsets.UTF_8);
754                                                        } else if (executeTypeQuery) {
755                                                                Map<String, Object> parametersMap = parametersListOfMaps.get(0);
756
757                                                                Map<String, Object> parametersMap0 = new LinkedHashMap<>();
758                                                                parametersMap0.put("instanceName", instanceName);
759                                                                parametersMap0.putAll(parametersMap);
760
761                                                                String jsonArray = CallUtils.executeUrl(instanceDirectory, nonNativeProcedure.optionsJson, parametersMap0);
762
763                                                                Integer                                                      compression = stmtDeclareStatement == null ? CompressionLevel.BEST_COMPRESSION : stmtDeclareStatement.compressionLevel;
764                                                                List<Map<String /* column name */, String /* JSON value */>> list        = QueryUtils.convertJsonArrayOfObjectsToListOfMaps(jsonArray);
765                                                                ReadyResult                                                  rr          = QueryUtils.createReadyResult(list, resultSetFormat, compression, sharedCoder.encoder);
766                                                                bs         = rr.bs;
767                                                                etag       = rr.etag;
768                                                                compressed = rr.compressed;
769                                                        }
770                                                }
771                                        } else { // Native
772                                                PreparedStatement ps  = dbConnection.getPreparedStatement(preparedStatement);
773                                                ParameterMetaData pmd = ps.getParameterMetaData();
774                                                for (Map<String, Object> parametersMap : parametersListOfMaps) {
775                                                        for (Map.Entry<String, Object> parameterMapEntry : parametersMap.entrySet()) {
776                                                                String pname     = parameterMapEntry.getKey();
777                                                                Object pvalueObj = parameterMapEntry.getValue();
778                                                                String pvalue    = pvalueObj == null ? null : pvalueObj.toString();
779
780                                                                List<Integer /* index */> indexes = mapParams.get(pname);
781                                                                if (indexes != null)
782                                                                        for (int n : indexes) {
783                                                                                int parameterType = pmd.getParameterType(n);
784                                                                                if (pvalue == null) // JavaScript null
785                                                                                        ps.setNull(n, parameterType);
786                                                                                else {
787                                                                                        if (parameterType == Types.SMALLINT) // JavaScript Number
788                                                                                                ps.setShort(n, Short.parseShort(pvalue));
789                                                                                        else if (parameterType == Types.INTEGER) // JavaScript Number
790                                                                                                ps.setInt(n, Integer.parseInt(pvalue));
791                                                                                        else if (parameterType == Types.BIGINT) // JavaScript Number
792                                                                                                ps.setLong(n, Long.parseLong(pvalue));
793                                                                                        else if (parameterType == Types.FLOAT) // JavaScript Number
794                                                                                                ps.setFloat(n, Float.parseFloat(pvalue));
795                                                                                        else if (parameterType == Types.DOUBLE) // JavaScript Number
796                                                                                                ps.setDouble(n, Double.parseDouble(pvalue));
797                                                                                        else if (parameterType == Types.BOOLEAN || parameterType == Types.BIT) // JavaScript Boolean
798                                                                                                ps.setBoolean(n, Boolean.parseBoolean(pvalue));
799                                                                                        else if (parameterType == Types.DATE) {
800                                                                                                if (pvalue.contains("T")) { // JavaScript Date
801                                                                                                        Instant   instant = Instant.parse(pvalue);
802                                                                                                        Timestamp ts      = Timestamp.from(instant);
803                                                                                                        long      time    = ts.getTime();
804                                                                                                        ps.setDate(n, new Date(time));
805                                                                                                } else if (pvalue.contains("-")) // JavaScript String
806                                                                                                        ps.setString(n, pvalue); // JavaScript Number
807                                                                                                else if (pvalue.charAt(0) > '0' && pvalue.charAt(0) <= '9') // JavaScript Number
808                                                                                                        ps.setDate(n, new Date(Long.parseLong(pvalue)));
809                                                                                                else
810                                                                                                        ps.setString(n, pvalue);
811                                                                                        } else if (parameterType == Types.TIME) {
812                                                                                                if (pvalue.contains("T")) { // JavaScript Date
813                                                                                                        Instant   instant = Instant.parse(pvalue);
814                                                                                                        Timestamp ts      = Timestamp.from(instant);
815                                                                                                        ps.setTime(n, new Time(ts.getTime()));
816                                                                                                } else if (pvalue.contains(":")) // JavaScript String
817                                                                                                        ps.setString(n, pvalue);
818                                                                                                else if (pvalue.charAt(0) > '0' && pvalue.charAt(0) <= '9') // JavaScript Number
819                                                                                                        ps.setTime(n, new Time(Long.parseLong(pvalue)));
820                                                                                                else
821                                                                                                        ps.setString(n, pvalue);
822                                                                                        } else if (parameterType == Types.TIMESTAMP) {
823                                                                                                if (pvalue.contains("T")) { // JavaScript Date
824                                                                                                        Instant   instant = Instant.parse(pvalue);
825                                                                                                        Timestamp ts      = Timestamp.from(instant);
826                                                                                                        ps.setTimestamp(n, ts);
827                                                                                                } else if (pvalue.charAt(0) > '0' && pvalue.charAt(0) <= '9') // JavaScript Number
828                                                                                                        ps.setTime(n, new Time(Long.parseLong(pvalue)));
829                                                                                                else
830                                                                                                        ps.setString(n, pvalue);
831                                                                                        } else if (parameterType == Types.BINARY || parameterType == Types.VARBINARY || parameterType == Types.LONGVARBINARY) // JavaScript (ArrayBuffer, Blob, URL, String) converted to BASE64 on client side
832                                                                                                ps.setBytes(n, sharedCoder.decoder.decode(pvalue));
833                                                                                        else if (parameterType == Types.BLOB) { // JavaScript (ArrayBuffer, Blob, URL, String) converted to BASE64 on client side
834                                                                                                Blob blob = ps.getConnection().createBlob();
835                                                                                                blob.setBytes(1L, sharedCoder.decoder.decode(pvalue));
836                                                                                                ps.setBlob(n, blob);
837                                                                                        } else if (parameterType == Types.CLOB) { // JavaScript string stored in CLOB "AS IS" (as String)
838                                                                                                Clob clob = ps.getConnection().createClob();
839                                                                                                clob.setString(1L, pvalue);
840                                                                                                ps.setClob(n, clob);
841                                                                                        } else { // unknown
842                                                                                                if (FBSQL_REMOTE_USER.equals(pvalue))
843                                                                                                        ps.setString(n, remoteUser);
844                                                                                                else if (FBSQL_REMOTE_ROLE.equals(pvalue))
845                                                                                                        ps.setString(n, remoteRole);
846                                                                                                else if (FBSQL_REMOTE_SESSION_ID.equals(pvalue))
847                                                                                                        ps.setString(n, session.getId());
848                                                                                                else if (FBSQL_REMOTE_SESSION_CREATION_TIME.equals(pvalue))
849                                                                                                        ps.setLong(n, session.getCreationTime());
850                                                                                                else if (FBSQL_REMOTE_SESSION_LAST_ACCESSED_TIME.equals(pvalue))
851                                                                                                        ps.setLong(n, session.getLastAccessedTime());
852                                                                                                else if (FBSQL_REMOTE_USER_INFO.equals(pvalue))
853                                                                                                        ps.setString(n, userInfoJson);
854                                                                                                else
855                                                                                                        ps.setString(n, pvalue);
856                                                                                        }
857                                                                                }
858                                                                        }
859                                                        }
860                                                        if (executeTypeUpdate)
861                                                                ps.addBatch();
862                                                }
863
864                                                if (executeTypeQuery) { // Execute Query
865                                                        Integer                                                      compression = stmtDeclareStatement == null ? CompressionLevel.BEST_COMPRESSION : stmtDeclareStatement.compressionLevel;
866                                                        ResultSet                                                    rs          = ps.executeQuery();
867                                                        List<Map<String /* column name */, String /* JSON value */>> list        = QueryUtils.resutlSetToListOfMapsJsonValues(rs, sharedCoder.encoder);
868                                                        //                                                      try (ResultSet rs = ps.executeQuery()) {
869                                                        //                                                              List<Map<String /* column name */, Object /* column value */>> resultsListOfMaps = QueryUtils.resutlSetToListOfMaps(rs);
870                                                        //                                                              List<Map<String /* column name */, String /* JSON value */>>   list              = QueryUtils.listOfMapsToListOfMapsJsonValues(resultsListOfMaps, sharedCoder.encoder);
871
872                                                        ReadyResult rr = QueryUtils.createReadyResult(list, resultSetFormat, compression, sharedCoder.encoder);
873                                                        bs         = rr.bs;
874                                                        etag       = rr.etag;
875                                                        compressed = rr.compressed;
876                                                        //                                                      } finally {
877                                                        //                                                              if (dbConnection != null)
878                                                        //                                                                      connectionPoolManager.releaseConnection(dbConnection);
879                                                        //                                                      }
880
881                                                } else if (executeTypeUpdate) { // Execute Update
882                                                        List<Map<String /* column name */, Object /* JSON value */>> gkList;
883                                                        int[]                                                        rowCounts = ps.executeBatch();
884                                                        try (ResultSet rsgk = ps.getGeneratedKeys()) {
885                                                                if (rsgk == null)
886                                                                        gkList = Collections.emptyList();
887                                                                else
888                                                                        gkList = QueryUtils.resutlSetToListOfMaps(rsgk);
889                                                        } catch (Throwable e) {
890                                                                e.printStackTrace();
891                                                                gkList = Collections.emptyList();
892                                                                //                                                      } finally {
893                                                                //                                                              if (dbConnection != null)
894                                                                //                                                                      connectionPoolManager.releaseConnection(dbConnection);
895                                                        }
896
897                                                        List<Map<String /* column name */, String /* JSON value */>> generatedKeys = QueryUtils.listOfMapsToListOfMapsJsonValues(gkList, sharedCoder.encoder);
898
899                                                        //
900                                                        // Build executeUpdate result JSON
901                                                        //
902
903                                                        StringBuilder resultSb = new StringBuilder();
904                                                        resultSb.append('{');
905                                                        resultSb.append(q("rowCount"));
906                                                        resultSb.append(':');
907                                                        resultSb.append(batch ? Arrays.toString(rowCounts).replace(" ", "") : Integer.toString(rowCounts[0])); // JavaScript array of numbers (batch) or number
908                                                        resultSb.append(',');
909                                                        resultSb.append(q("generatedKeys"));
910                                                        resultSb.append(':');
911                                                        resultSb.append(QueryUtils.convertToJsonArray(generatedKeys, resultSetFormat));
912                                                        resultSb.append('}');
913
914                                                        updateResultJson = resultSb.toString();
915                                                        bs               = updateResultJson.getBytes(StandardCharsets.UTF_8);
916                                                } else
917                                                        throw new IllegalArgumentException(execType);
918                                        }
919                                } catch (SQLException e) {
920                                        e.printStackTrace();
921                                        try (OutputStream os = response.getOutputStream()) {
922                                                String message  = e.getMessage();
923                                                String sqlState = e.getSQLState();
924                                                String msg      = "{" + q("message") + ":" + (message == null ? "null" : q(message)) + "," + q("errorCode") + ":" + e.getErrorCode() + "," + q("SQLState") + ":" + (sqlState == null ? "null" : q(sqlState)) + "}";
925                                                os.write(msg.getBytes(StandardCharsets.UTF_8));
926                                                os.flush();
927                                        } catch (Exception e2) {
928                                                e2.printStackTrace();
929                                        }
930                                        asyncContext.complete();
931                                        return;
932                                } finally {
933                                        if (dbConnection != null)
934                                                connectionPoolManager.releaseConnection(dbConnection);
935                                }
936
937                                if (!ongoingRequests.isEmpty()) {
938                                        long timestamp = Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis();
939                                        if (stmtDeclareStatement != null) {
940                                                String notifierStoredProcedureName = stmtDeclareStatement.trigger_after_procedure_name;
941                                                for (AsyncContext ac : ongoingRequests) {
942                                                        try {
943                                                                HttpServletRequest  selfRequest  = (HttpServletRequest) ac.getRequest();
944                                                                HttpServletResponse selfResponce = (HttpServletResponse) ac.getResponse();
945
946                                                                if (notifierStoredProcedureName != null) {
947                                                                        String       selfClientInfoJson  = getClientInfo(selfRequest, sharedCoder.decoder);
948                                                                        CharSequence selfSessionInfoJson = HttpRequestUtils.generateSessionInfoJson(request, sharedCoder.encoder);
949
950                                                                        String selfUserInfoJson = generateUserInfoJson( //
951                                                                                        selfRequest, //
952                                                                                        selfClientInfoJson, //
953                                                                                        selfSessionInfoJson //
954                                                                        );
955
956                                                                        String statementInfoJson = generateStatementInfoJson( //
957                                                                                        instanceName, //
958                                                                                        statementId, //
959                                                                                        unprocessedNamedPreparedStatement, //
960                                                                                        paramJsonArray //
961                                                                        );
962
963                                                                        String executionResultJson = generateExecutionResultJson( //
964                                                                                        timestamp, //
965                                                                                        updateResultJson);
966
967                                                                        String       outEvent      = null; // JSON object that will sent to client
968                                                                        DbConnection dbConnection0 = null;
969                                                                        try {
970                                                                                dbConnection0 = connectionPoolManager.getConnection();
971                                                                                NonNativeProcedure nonNativeProcedure = proceduresMap.get(notifierStoredProcedureName);
972                                                                                if (nonNativeProcedure != null) {
973                                                                                        if (nonNativeProcedure.procedureType == ProcedureType.JAVA || nonNativeProcedure.procedureType == ProcedureType.JS) { // Java or JavaScript
974                                                                                                Object obj = null;
975
976                                                                                                List<Object> parameterValues = new ArrayList<>();
977                                                                                                parameterValues.add(selfRequest);
978                                                                                                parameterValues.add(selfResponce);
979                                                                                                parameterValues.add(dbConnection0.getConnection());
980                                                                                                parameterValues.add(instanceName);
981                                                                                                parameterValues.add(userInfoJson);
982                                                                                                parameterValues.add(selfUserInfoJson);
983                                                                                                parameterValues.add(statementInfoJson);
984                                                                                                parameterValues.add(executionResultJson);
985                                                                                                Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
986
987                                                                                                if (nonNativeProcedure.procedureType == ProcedureType.JAVA) { // Java
988                                                                                                        Method method = CallUtils.getMethod(nonNativeProcedure.optionsJson);
989                                                                                                        obj = method.invoke(null, parametersArray);
990                                                                                                } else if (nonNativeProcedure.procedureType == ProcedureType.JS) { // JavaScript
991                                                                                                        //
992                                                                                                        // initize Rhino
993                                                                                                        // https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino
994                                                                                                        //
995                                                                                                        Context ctx = Context.enter();
996                                                                                                        try {
997                                                                                                                ctx.setLanguageVersion(Context.VERSION_1_7);
998                                                                                                                ctx.setOptimizationLevel(9); // Rhino optimization: https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino/Optimization
999                                                                                                                JsFunction jsFunction = CallUtils.getFunction(instanceDirectory, nonNativeProcedure.optionsJson, mapScopes, mapFunctions);
1000                                                                                                                obj = jsFunction.function.call(ctx, jsFunction.scope, null, parametersArray);
1001                                                                                                                if (obj instanceof NativeObject)
1002                                                                                                                        obj = (String) NativeJSON.stringify(ctx, jsFunction.scope, obj, null, null);
1003                                                                                                        } catch (Exception e) {
1004                                                                                                                obj = null;
1005                                                                                                                e.printStackTrace();
1006                                                                                                        } finally {
1007                                                                                                                ctx.exit();
1008                                                                                                        }
1009                                                                                                }
1010                                                                                                if (obj instanceof ResultSet)
1011                                                                                                        try (ResultSet rs = (ResultSet) obj) {
1012                                                                                                                if (rs.next())
1013                                                                                                                        outEvent = rs.getString(1);
1014                                                                                                        }
1015                                                                                                else if (obj instanceof CharSequence)
1016                                                                                                        outEvent = obj.toString();
1017                                                                                        } else if (nonNativeProcedure.procedureType == ProcedureType.EXEC) { // OS
1018                                                                                                List<Object> parameterValues = new ArrayList<>();
1019                                                                                                parameterValues.add(instanceName);
1020                                                                                                parameterValues.add(userInfoJson);
1021                                                                                                parameterValues.add(selfUserInfoJson);
1022                                                                                                parameterValues.add(statementInfoJson);
1023                                                                                                parameterValues.add(executionResultJson);
1024                                                                                                Object[] parametersArray = parameterValues.toArray(new Object[parameterValues.size()]);
1025                                                                                                outEvent = CallUtils.executeOsProgramm(instanceDirectory, nonNativeProcedure.optionsJson, parametersArray);
1026
1027                                                                                        } else if (nonNativeProcedure.procedureType == ProcedureType.URL) { // URL
1028                                                                                                Map<String, Object> parametersMap = new LinkedHashMap<>();
1029                                                                                                parametersMap.put("instanceName", instanceName);
1030                                                                                                parametersMap.put("userInfoJson", userInfoJson);
1031                                                                                                parametersMap.put("selfUserInfoJson", selfUserInfoJson);
1032                                                                                                parametersMap.put("statementInfoJson", statementInfoJson);
1033                                                                                                parametersMap.put("executionResultJson", executionResultJson);
1034
1035                                                                                                outEvent = CallUtils.executeUrl(instanceDirectory, nonNativeProcedure.optionsJson, parametersMap);
1036                                                                                        }
1037                                                                                } else { // Native
1038                                                                                        CallableStatement cs = dbConnection0.getCallableStatement("{call " + notifierStoredProcedureName + "(?,?,?,?)}");
1039                                                                                        cs.setString(1, userInfoJson);
1040                                                                                        cs.setString(2, selfUserInfoJson);
1041                                                                                        cs.setString(3, statementInfoJson);
1042                                                                                        cs.setString(4, executionResultJson);
1043
1044                                                                                        try (ResultSet rs = cs.executeQuery()) {
1045                                                                                                if (rs.next())
1046                                                                                                        outEvent = rs.getString(1);
1047                                                                                        }
1048                                                                                }
1049
1050                                                                        } finally {
1051                                                                                if (dbConnection0 != null)
1052                                                                                        connectionPoolManager.releaseConnection(dbConnection0);
1053                                                                                if (outEvent != null) { // send if outEvent is not null
1054                                                                                        Writer writer = ac.getResponse().getWriter();
1055                                                                                        writer.append(outEvent + '\n');
1056                                                                                        writer.flush();
1057                                                                                        if (debug) {
1058                                                                                                System.out.println("Event was delivered to client:");
1059                                                                                                System.out.println(outEvent);
1060                                                                                        }
1061                                                                                }
1062                                                                        }
1063                                                                }
1064                                                        } catch (Exception e) {
1065                                                                e.printStackTrace();
1066                                                        }
1067                                                }
1068                                        }
1069                                }
1070                        } else {
1071                                bs         = readyResult.bs;
1072                                etag       = readyResult.etag;
1073                                compressed = readyResult.compressed;
1074                        }
1075                        if (executeTypeQuery) { // Execute Query
1076                                if (etag.equals(etagFromClient)) {
1077                                        response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); // setting HTTP 304 and returning with empty body
1078                                        asyncContext.complete();
1079                                        return;
1080                                } else {
1081                                        response.setContentType("application/json");
1082                                        response.setCharacterEncoding("UTF-8");
1083                                        response.setContentLength(bs.length);
1084                                        if (compressed)
1085                                                response.setHeader("Content-Encoding", "deflate");
1086                                        response.setHeader("Cache-Control", "no-cache");
1087                                        response.addHeader("ETag", etag);
1088                                }
1089                        } else if (executeTypeUpdate) { // Execute Update
1090                                response.setContentType("application/json");
1091                                response.setCharacterEncoding("UTF-8");
1092                                response.setContentLength(bs.length);
1093                        }
1094                        try (OutputStream os = response.getOutputStream()) {
1095                                os.write(bs);
1096                                os.flush();
1097                        }
1098                } catch (Throwable e) {
1099                        e.printStackTrace();
1100                        try (OutputStream os = response.getOutputStream()) {
1101                                os.write(("{" + q("message") + ":" + q(e.getMessage()) + "}").getBytes(StandardCharsets.UTF_8));
1102                                os.flush();
1103                        } catch (IOException e1) {
1104                                e1.printStackTrace();
1105                        }
1106                } finally {
1107                        try {
1108                                asyncContext.complete();
1109                        } catch (Throwable t) {
1110                                t.printStackTrace();
1111                        }
1112                }
1113        }
1114
1115        private static String getClientInfo(HttpServletRequest request, Decoder decoder) throws IOException {
1116                String base64 = request.getHeader(DbServlet.CUSTOM_HTTP_HEADER_CLIENT_INFO);
1117                byte[] bs     = decoder.decode(base64);
1118                return new String(bs, StandardCharsets.UTF_8);
1119        }
1120
1121        private static BufferedReader getCustomDataReader(HttpServletRequest request, Decoder decoder) throws IOException {
1122                String         method = request.getMethod().toUpperCase(Locale.ENGLISH);
1123                BufferedReader br;
1124                if (method.equals("GET")) {
1125                        String base64 = request.getHeader(DbServlet.CUSTOM_HTTP_HEADER_STATEMENT);
1126                        byte[] bs     = decoder.decode(base64);
1127                        String s      = new String(bs, StandardCharsets.UTF_8);
1128                        br = new BufferedReader(new StringReader(s));
1129                } else if (method.equals("POST"))
1130                        br = request.getReader();
1131                else
1132                        throw new IllegalArgumentException(method);
1133                return br;
1134        }
1135
1136        /**
1137         * Utility method generates ExecuteUpdate result JSON
1138         *
1139         * @param rowCount
1140         * @return
1141         */
1142        private static String simpleExecuteUpdateResultJson(int rowCount) {
1143                StringBuilder sb = new StringBuilder();
1144                sb.append('{');
1145                sb.append(q("rowCount") + ':' + Integer.toString(rowCount));
1146                sb.append(',');
1147                sb.append(q("generatedKeys") + ':' + "[]");
1148                sb.append('}');
1149                return sb.toString();
1150        }
1151
1152        /**
1153         * Generate User Info JSON-formatted string
1154         * 
1155         * @param request
1156         * @param clientInfoJson
1157         * @return
1158         * @throws IOException
1159         * @throws ParseException
1160         */
1161        private static String generateUserInfoJson( //
1162                        HttpServletRequest request, //
1163                        String clientInfoJson, //
1164                        CharSequence sessionInfoJson //
1165        ) throws IOException, ParseException {
1166                String       remoteAddr  = request.getRemoteAddr();
1167                String       remoteHost  = request.getRemoteHost();
1168                Integer      remotePort  = request.getRemotePort();
1169                String       remoteUser  = (String) request.getAttribute(DbServlet.REQUEST_ATTRIBUTE_USER);
1170                String       remoteRole  = request.getHeader(DbServlet.CUSTOM_HTTP_HEADER_ROLE);
1171                Cookie[]     cookies     = request.getCookies();
1172                CharSequence cookiesJson = HttpRequestUtils.getCookiesJson(cookies);
1173                CharSequence headersJson = HttpRequestUtils.getHttpHeadersJson(request);
1174
1175                final String  NULL  = "null";
1176                final char    COLON = ':';
1177                final char    COMMA = ',';
1178                StringBuilder sb    = new StringBuilder();
1179
1180                sb.append('{');
1181                sb.append(q("remoteUser")).append(COLON).append(remoteUser == null ? NULL : q(remoteUser)).append(COMMA);
1182                sb.append(q("remoteRole")).append(COLON).append(remoteRole == null ? NULL : q(remoteRole)).append(COMMA);
1183                sb.append(q("remoteAddr")).append(COLON).append(remoteAddr == null ? NULL : q(remoteAddr)).append(COMMA);
1184                sb.append(q("remoteHost")).append(COLON).append(remoteHost == null ? NULL : q(remoteHost)).append(COMMA);
1185                sb.append(q("remotePort")).append(COLON).append(remotePort == null ? NULL : Integer.toString(remotePort)).append(COMMA);
1186                sb.append(q("client")).append(COLON).append(clientInfoJson).append(COMMA);
1187                sb.append(q("headers")).append(COLON).append(headersJson).append(COMMA);
1188                sb.append(q("cookies")).append(COLON).append("[]".equals(cookiesJson) ? NULL : cookiesJson).append(COMMA);
1189                sb.append(q("httpSession")).append(COLON).append(sessionInfoJson);
1190                sb.append('}');
1191                return sb.toString();
1192        }
1193
1194        /**
1195         * Generate Statement Info JSON-formatted string
1196         *
1197         * @param instanceName
1198         * @param statementId
1199         * @param unprocessedNamedPreparedStatement
1200         * @param paramArrayJson
1201         * @return
1202         */
1203        private static String generateStatementInfoJson( //
1204                        String instanceName, //
1205                        String statementId, //
1206                        String unprocessedNamedPreparedStatement, //
1207                        String paramArrayJson) {
1208                final String  NULL  = "null";
1209                final char    COLON = ':';
1210                final char    COMMA = ',';
1211                StringBuilder sb    = new StringBuilder();
1212
1213                sb.append('{');
1214                sb.append(q("instanceName")).append(COLON).append(q(instanceName)).append(COMMA);
1215                sb.append(q("statementId")).append(COLON).append(statementId == null ? NULL : q(statementId)).append(COMMA);
1216                sb.append(q("statement")).append(COLON).append(q(unprocessedNamedPreparedStatement)).append(COMMA);
1217                sb.append(q("parameters")).append(COLON).append(paramArrayJson);
1218                sb.append('}');
1219                return sb.toString();
1220        }
1221
1222        /**
1223         * Generate Execution Result JSON-formatted string
1224         *
1225         * @param timestamp
1226         * @param updateResultJson
1227                 * @return
1228         */
1229        private static String generateExecutionResultJson( //
1230                        long timestamp, //
1231                        String updateResultJson //
1232        ) {
1233                final char    COLON = ':';
1234                final char    COMMA = ',';
1235                StringBuilder sb    = new StringBuilder();
1236
1237                sb.append('{');
1238                sb.append(q("timestamp")).append(COLON).append(timestamp).append(COMMA);
1239                sb.append(q("updateResult")).append(COLON).append(updateResultJson);
1240                sb.append('}');
1241                return sb.toString();
1242        }
1243}
1244
1245/*
1246Please contact FBSQL Team by E-Mail fbsql.team@gmail.com
1247or visit https://fbsql.github.io if you need additional
1248information or have any questions.
1249*/
1250
1251/* EOF */