View Javadoc
1   /*******************************************************************************
2    * Copyhacked (H) 2012-2025.
3    * This program and the accompanying materials
4    * are made available under no term at all, use it like
5    * you want, but share and discuss it
6    * every time possible with every body.
7    * 
8    * Contributors:
9    *      ron190 at ymail dot com - initial implementation
10   ******************************************************************************/
11  package com.jsql.model.accessible;
12  
13  import com.jsql.model.InjectionModel;
14  import com.jsql.model.bean.database.*;
15  import com.jsql.model.bean.util.Interaction;
16  import com.jsql.model.bean.util.Request;
17  import com.jsql.model.exception.AbstractSlidingException;
18  import com.jsql.model.exception.InjectionFailureException;
19  import com.jsql.model.exception.JSqlException;
20  import com.jsql.model.suspendable.SuspendableGetRows;
21  import com.jsql.util.I18nUtil;
22  import com.jsql.util.LogLevelUtil;
23  import org.apache.commons.lang3.StringUtils;
24  import org.apache.logging.log4j.LogManager;
25  import org.apache.logging.log4j.Logger;
26  
27  import java.util.ArrayList;
28  import java.util.List;
29  import java.util.regex.Pattern;
30  
31  /**
32   * Database resource object to read name of databases, tables, columns and values
33   * using most suited injection strategy.
34   */
35  public class DataAccess {
36      
37      private static final Logger LOGGER = LogManager.getRootLogger();
38      
39      /**
40       * Regex characters marking the end of the result of an injection.
41       * Process stops when this schema is encountered:
42       * <pre>SQLix01x03x03x07
43       */
44      public static final String TRAIL_RGX = "\\x01\\x03\\x03\\x07";
45      
46      /**
47       * Regex character used between each table cells.
48       * Expected schema of multiple table cells :
49       * <pre>
50       * x04[table cell]x05[number of occurrences]x04x06x04[table cell]x05[number of occurrences]x04
51       */
52      public static final String SEPARATOR_CELL_RGX = "\\x06";
53      
54      /**
55       * Regex character used between the table cell and the number of occurrence of the cell text.
56       * Expected schema of a table cell data is
57       * <pre>x04[table cell]x05[number of occurrences]x04
58       */
59      public static final String SEPARATOR_QTE_RGX = "\\x05";
60  
61      /**
62       * Regex character enclosing a table cell returned by injection.
63       * It allows to detect the correct end of a table cell data during parsing.
64       * Expected schema of a table cell data is
65       * <pre>x04[table cell]x05[number of occurrences]x04
66       */
67      public static final String ENCLOSE_VALUE_RGX = "\\x04";
68      
69      public static final String LEAD = "SqLi";
70      public static final String SHELL_LEAD = "${shell.lead}";
71      public static final String TRAIL = "iLQS";
72      public static final String SHELL_TRAIL = "${shell.trail}";
73      
74      /**
75       * Regex keywords corresponding to multiline and case-insensitive match.
76       */
77      public static final String MODE = "(?si)";
78      
79      /**
80       * Regex schema describing a table cell with firstly the cell content and secondly the number of occurrences
81       * of the cell text, separated by the reserved character x05 in hexadecimal.
82       * The range of characters from x01 to x1F are not printable ASCII characters used to parse the data and exclude
83       * printable characters during parsing.
84       * Expected schema of a table cell data is
85       * <pre>x04[table cell]x05[number of occurrences]x04
86       */
87      public static final String CELL_TABLE = "([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*)"+ DataAccess.SEPARATOR_QTE_RGX +"([^\\x01-\\x09\\x0B-\\x0C\\x0E-\\x1F]*)(\\x08)?";
88  
89      private final InjectionModel injectionModel;
90      
91      public DataAccess(InjectionModel injectionModel) {
92          this.injectionModel = injectionModel;
93      }
94      
95      /**
96       * Get general database information.<br>
97       * => version{%}database{%}user{%}CURRENT_USER
98       */
99      public void getDatabaseInfos() {
100         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, () -> I18nUtil.valueByKey("LOG_FETCHING_INFORMATIONS"));
101         
102         var sourcePage = new String[]{ StringUtils.EMPTY };
103 
104         var resultToParse = StringUtils.EMPTY;
105         try {
106             resultToParse = new SuspendableGetRows(this.injectionModel).run(
107                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlInfos(),
108                 sourcePage,
109                 false,
110                 0,
111                 MockElement.MOCK,
112                 "metadata"
113             );
114         } catch (AbstractSlidingException e) {
115             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
116         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
117             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
118         }
119 
120         if (StringUtils.isEmpty(resultToParse)) {
121             this.injectionModel.sendResponseFromSite("Incorrect metadata", sourcePage[0].trim());
122         }
123         
124         try {
125             String versionDatabase = resultToParse.split(DataAccess.ENCLOSE_VALUE_RGX)[0].replaceAll("\\s+", StringUtils.SPACE);
126             String nameDatabase = resultToParse.split(DataAccess.ENCLOSE_VALUE_RGX)[1];
127             String username = resultToParse.split(DataAccess.ENCLOSE_VALUE_RGX)[2];
128 
129             var infos = String.format(
130                 "Database [%s] on %s [%s] for user [%s]",
131                 nameDatabase,
132                 this.injectionModel.getMediatorVendor().getVendor(),
133                 versionDatabase,
134                 username
135             );
136             LOGGER.log(LogLevelUtil.CONSOLE_SUCCESS, infos);
137         } catch (ArrayIndexOutOfBoundsException e) {
138             LOGGER.log(
139                 LogLevelUtil.CONSOLE_ERROR,
140                 String.format("%s: %s", I18nUtil.valueByKey("LOG_DB_METADATA_INCORRECT"), resultToParse)
141             );
142             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, I18nUtil.valueByKey("LOG_DB_METADATA_WARN"));
143         }
144     }
145 
146     /**
147      * Get database names and table counts and send them to the view.<br>
148      * Use readable text (not hexa) and parse this pattern:<br>
149      * => hh[database name 1]jj[table count]hhgghh[database name 2]jj[table count]hhggh...hi<br>
150      * Data window can be cut before the end of the request but the process helps to obtain
151      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
152      * @return list of databases found
153      * @throws JSqlException when injection failure or stopped by user
154      */
155     public List<Database> listDatabases() throws JSqlException {
156         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, () -> I18nUtil.valueByKey("LOG_FETCHING_DATABASES"));
157         List<Database> databases = new ArrayList<>();
158         String resultToParse = StringUtils.EMPTY;
159         
160         try {
161             var sourcePage = new String[]{ StringUtils.EMPTY };
162             resultToParse = new SuspendableGetRows(this.injectionModel).run(
163                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlDatabases(),
164                 sourcePage,
165                 true,
166                 0,
167                 MockElement.MOCK,
168                 "databases"
169             );
170         } catch (AbstractSlidingException e) {
171             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
172         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
173             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
174         }
175 
176         // Parse all data we have retrieved
177         var regexSearch = Pattern.compile(
178                 DataAccess.MODE
179                 + DataAccess.ENCLOSE_VALUE_RGX
180                 + DataAccess.CELL_TABLE
181                 + DataAccess.ENCLOSE_VALUE_RGX
182             )
183             .matcher(resultToParse);
184         if (!regexSearch.find()) {
185             throw new InjectionFailureException("No match while injecting databases");
186         }
187         
188         regexSearch.reset();
189         // Build an array of Database objects from the data we have parsed
190         while (regexSearch.find()) {
191             String databaseName = regexSearch.group(1);
192             String tableCount = regexSearch.group(2);
193 
194             var newDatabase = new Database(databaseName, tableCount);
195             databases.add(newDatabase);
196         }
197 
198         var request = new Request();
199         request.setMessage(Interaction.ADD_DATABASES);
200         request.setParameters(databases);
201         this.injectionModel.sendToViews(request);
202         return databases;
203     }
204 
205     /**
206      * Get tables name and row count and send them to the view.<br>
207      * Use readable text (not hexa) and parse this pattern:<br>
208      * => hh[table name 1]jj[rows count]hhgghh[table name 2]jj[rows count]hhggh...hi<br>
209      * Data window can be cut before the end of the request but the process helps to obtain
210      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
211      * @param database which contains tables to find
212      * @return list of tables found
213      * @throws JSqlException when injection failure or stopped by user
214      */
215     public List<Table> listTables(Database database) throws JSqlException {
216         // Reset stoppedByUser if list of Databases is partial
217         // and some Tables are still reachable
218         this.injectionModel.setIsStoppedByUser(false);
219         
220         // Inform the view that database has just been used
221         var requestStartProgress = new Request();
222         requestStartProgress.setMessage(Interaction.START_PROGRESS);
223         requestStartProgress.setParameters(database);
224         this.injectionModel.sendToViews(requestStartProgress);
225 
226         var tableCount = Integer.toString(database.getChildCount());
227         
228         String resultToParse = StringUtils.EMPTY;
229         try {
230             var pageSource = new String[]{ StringUtils.EMPTY };
231             resultToParse = new SuspendableGetRows(this.injectionModel).run(
232                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlTables(database),
233                 pageSource,
234                 true,
235                 Integer.parseInt(tableCount),
236                 database,
237                 "tables"
238             );
239         } catch (AbstractSlidingException e) {
240             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
241         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
242             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
243         }
244 
245         // Parse all the data we have retrieved
246         var regexSearch = Pattern.compile(
247                 DataAccess.MODE
248                 + DataAccess.ENCLOSE_VALUE_RGX
249                 + DataAccess.CELL_TABLE
250                 + DataAccess.ENCLOSE_VALUE_RGX
251             )
252             .matcher(resultToParse);
253         
254         var requestEndProgress = new Request();
255         requestEndProgress.setMessage(Interaction.END_PROGRESS);
256         requestEndProgress.setParameters(database);
257         this.injectionModel.sendToViews(requestEndProgress);
258         
259         if (!regexSearch.find()) {
260             throw new InjectionFailureException("No match while injecting tables");
261         }
262         regexSearch.reset();
263         
264         List<Table> tables = new ArrayList<>();
265         
266         // Build an array of Table objects from the data we have parsed
267         while (regexSearch.find()) {
268             String tableName = regexSearch.group(1);
269             String rowCount = regexSearch.group(2);
270             
271             var newTable = new Table(tableName, rowCount, database);
272             tables.add(newTable);
273         }
274         
275         var requestAddTables = new Request();
276         requestAddTables.setMessage(Interaction.ADD_TABLES);
277         requestAddTables.setParameters(tables);
278         this.injectionModel.sendToViews(requestAddTables);
279         return tables;
280     }
281 
282     /**
283      * Get column names and send them to the view.<br>
284      * Use readable text (not hexa) and parse this pattern with 2nd member forced to 31 (1 in ascii):<br>
285      * => hh[column name 1]jj[31]hhgghh[column name 2]jj[31]hhggh...hi<br>
286      * Data window can be cut before the end of the request but the process helps to obtain
287      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
288      * @param table which contains columns to find
289      * @return list of columns found
290      * @throws JSqlException when injection failure or stopped by user
291      */
292     public List<Column> listColumns(Table table) throws JSqlException {
293         List<Column> columns = new ArrayList<>();
294         
295         // Inform the view that table has just been used
296         var requestStartProgress = new Request();
297         requestStartProgress.setMessage(Interaction.START_INDETERMINATE_PROGRESS);
298         requestStartProgress.setParameters(table);
299         this.injectionModel.sendToViews(requestStartProgress);
300 
301         String resultToParse = StringUtils.EMPTY;
302         
303         try {
304             var pageSource = new String[]{ StringUtils.EMPTY };
305             resultToParse = new SuspendableGetRows(this.injectionModel).run(
306                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlColumns(table),
307                 pageSource,
308                 true,
309                 0,
310                 table,
311                 "columns"
312             );
313         } catch (AbstractSlidingException e) {
314             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
315         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
316             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
317         }
318 
319         // Build SQLite columns
320         if (this.injectionModel.getMediatorVendor().isSqlite()) {
321             resultToParse = this.injectionModel.getMediatorVendor().getSqlite().transformSqlite(resultToParse);
322         }
323         
324         // Parse all the data we have retrieved
325         var regexSearch = Pattern.compile(
326                 DataAccess.MODE
327                 + DataAccess.ENCLOSE_VALUE_RGX
328                 + DataAccess.CELL_TABLE
329                 + DataAccess.ENCLOSE_VALUE_RGX
330             )
331             .matcher(resultToParse);
332 
333         var requestEndProgress = new Request();
334         requestEndProgress.setMessage(Interaction.END_INDETERMINATE_PROGRESS);
335         requestEndProgress.setParameters(table);
336         this.injectionModel.sendToViews(requestEndProgress);
337 
338         if (!regexSearch.find()) {
339             throw new InjectionFailureException("No match while injecting columns");
340         }
341 
342         regexSearch.reset();
343 
344         // Build an array of Column objects from the data we have parsed
345         while (regexSearch.find()) {
346             String nameColumn = regexSearch.group(1);
347             var column = new Column(nameColumn, table);
348             columns.add(column);
349         }
350 
351         var requestAddColumns = new Request();
352         requestAddColumns.setMessage(Interaction.ADD_COLUMNS);
353         requestAddColumns.setParameters(columns);
354         this.injectionModel.sendToViews(requestAddColumns);
355         return columns;
356     }
357 
358     /**
359      * Get table values and count each occurrence and send it to the view.<br>
360      * Values are on clear text (not hexa) and follows this window pattern<br>
361      * => hh[value 1]jj[count]hhgghh[value 2]jj[count]hhggh...hi<br>
362      * Data window can be cut before the end of the request but the process helps to obtain
363      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
364      * @param columnsBean choice by the user
365      * @return a 2x2 table containing values by columns
366      * @throws JSqlException when injection failure or stopped by user
367      */
368     public String[][] listValues(List<Column> columnsBean) throws JSqlException {
369         var databaseBean = (Database) columnsBean.get(0).getParent().getParent();
370         var tableBean = (Table) columnsBean.get(0).getParent();
371         int rowCount = columnsBean.get(0).getParent().getChildCount();
372 
373         // Inform the view that table has just been used
374         var request = new Request();
375         request.setMessage(Interaction.START_PROGRESS);
376         request.setParameters(tableBean);
377         this.injectionModel.sendToViews(request);
378 
379         // Build an array of column names
380         List<String> columnsName = new ArrayList<>();
381         for (AbstractElementDatabase columnBean: columnsBean) {
382             columnsName.add(columnBean.toString());
383         }
384 
385         // From that array, build the SQL fields nicely
386         // => col1{%}col2...
387         // ==> trim(ifnull(`col1`,0x00)),0x7f,trim(ifnull(`Col2`,0x00))...
388         String[] columns = columnsName.toArray(new String[0]);
389 
390         List<List<String>> listValues = this.getRows(databaseBean, tableBean, rowCount, columns);
391 
392         // Add the default title to the columns: row number, occurrence
393         columnsName.add(0, StringUtils.EMPTY);
394         columnsName.add(0, StringUtils.EMPTY);
395 
396         String[][] table = this.getTable(columnsName, listValues);
397 
398         columns = columnsName.toArray(new String[0]);
399         
400         // Group the columns names, values and Table object in one array
401         var objectData = new Object[]{ columns, table, tableBean };
402 
403         var requestCreateValuesTab = new Request();
404         requestCreateValuesTab.setMessage(Interaction.CREATE_VALUES_TAB);
405         requestCreateValuesTab.setParameters(objectData);
406         this.injectionModel.sendToViews(requestCreateValuesTab);
407 
408         var requestEndProgress = new Request();
409         requestEndProgress.setMessage(Interaction.END_PROGRESS);
410         requestEndProgress.setParameters(tableBean);
411         this.injectionModel.sendToViews(requestEndProgress);
412         return table;
413     }
414 
415     private List<List<String>> getRows(Database database, Table table, int rowCount, String[] columns) throws InjectionFailureException {
416         String resultToParse = StringUtils.EMPTY;
417         
418         try {
419             var pageSource = new String[]{ StringUtils.EMPTY };
420             resultToParse = new SuspendableGetRows(this.injectionModel).run(
421                 this.injectionModel.getMediatorVendor().getVendor().instance().sqlRows(columns, database, table),
422                 pageSource,
423                 true,
424                 rowCount,
425                 table,
426                 "rows"
427             );
428         } catch (AbstractSlidingException e) {  // Catch all exceptions but prevent detecting bug
429             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
430         } catch (Exception e) {
431             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
432         }
433         return SuspendableGetRows.parse(resultToParse);
434     }
435 
436     private static String getPartialResultAndLog(AbstractSlidingException e, String resultToParse) {
437         LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage());
438 
439         // Get pieces of data already retrieved instead of losing them
440         if (StringUtils.isNotEmpty(e.getSlidingWindowAllRows())) {
441             resultToParse = e.getSlidingWindowAllRows();
442         } else if (StringUtils.isNotEmpty(e.getSlidingWindowCurrentRows())) {
443             resultToParse = e.getSlidingWindowCurrentRows();
444         }
445         return resultToParse;
446     }
447 
448     private String[][] getTable(List<String> columnsName, List<List<String>> values) {
449         // Build a proper 2D array from the data
450         var table = new String[values.size()][columnsName.size()];
451         
452         for (var indexRow = 0 ; indexRow < values.size() ; indexRow++) {
453             var isIncomplete = false;
454             
455             for (var indexColumn = 0 ; indexColumn < columnsName.size() ; indexColumn++) {
456                 try {
457                     table[indexRow][indexColumn] = values.get(indexRow).get(indexColumn);
458                 } catch (IndexOutOfBoundsException e) {
459                     isIncomplete = true;
460                     LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, I18nUtil.valueByKey("LOG_LIST_VALUES_INCOMPLETE"));
461                     LOGGER.log(LogLevelUtil.IGNORE, e);
462                 }
463             }
464             
465             if (isIncomplete) {
466                 int logIndexRow = indexRow;
467                 LOGGER.log(
468                     LogLevelUtil.CONSOLE_ERROR,
469                     "{}{}: ",
470                     () -> I18nUtil.valueByKey("LOG_LIST_VALUES_TOO_LONG"),
471                     () -> logIndexRow + 1
472                 );
473 
474                 LOGGER.log(
475                     LogLevelUtil.CONSOLE_ERROR,
476                     () -> String.join(
477                         ", ",
478                         values.get(logIndexRow).toArray(new String[0])
479                     )
480                 );
481             }
482         }
483         return table;
484     }
485 }