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.suspendable.Input;
16  import com.jsql.view.subscriber.Seal;
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(new Input(
107                 this.injectionModel.getMediatorEngine().getEngine().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             LOGGER.log(
130                 LogLevelUtil.CONSOLE_SUCCESS,
131                 "Database [{}] on {} [{}] for user [{}]",
132                 nameDatabase,
133                 this.injectionModel.getMediatorEngine().getEngine(),
134                 versionDatabase,
135                 username
136             );
137         } catch (ArrayIndexOutOfBoundsException e) {
138             LOGGER.log(LogLevelUtil.CONSOLE_ERROR,"{}: {}", I18nUtil.valueByKey("LOG_DB_METADATA_INCORRECT"), resultToParse);
139             LOGGER.log(LogLevelUtil.CONSOLE_INFORM, I18nUtil.valueByKey("LOG_DB_METADATA_WARN"));
140         }
141     }
142 
143     /**
144      * Get database names and table counts and send them to the view.<br>
145      * Use readable text (not hexa) and parse this pattern:<br>
146      * => hh[database name 1]jj[table count]hhgghh[database name 2]jj[table count]hhggh...hi<br>
147      * Data window can be cut before the end of the request but the process helps to obtain
148      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
149      * @return list of databases found
150      * @throws JSqlException when injection failure or stopped by user
151      */
152     public List<Database> listDatabases() throws JSqlException {
153         LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, () -> I18nUtil.valueByKey("LOG_FETCHING_DATABASES"));
154         List<Database> databases = new ArrayList<>();
155         String resultToParse = StringUtils.EMPTY;
156         
157         try {
158             var sourcePage = new String[]{ StringUtils.EMPTY };
159             resultToParse = new SuspendableGetRows(this.injectionModel).run(new Input(
160                 this.injectionModel.getMediatorEngine().getEngine().instance().sqlDatabases(),
161                 sourcePage,
162                 true,
163                 0,
164                 MockElement.MOCK,
165                 "databases"
166             ));
167         } catch (AbstractSlidingException e) {
168             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
169         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
170             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
171         }
172 
173         // Parse all data we have retrieved
174         var regexSearch = Pattern.compile(
175                 DataAccess.MODE
176                 + DataAccess.ENCLOSE_VALUE_RGX
177                 + DataAccess.CELL_TABLE
178                 + DataAccess.ENCLOSE_VALUE_RGX
179             )
180             .matcher(resultToParse);
181         if (!regexSearch.find()) {
182             throw new InjectionFailureException("No match while injecting databases");
183         }
184         
185         regexSearch.reset();
186         // Build an array of Database objects from the data we have parsed
187         while (regexSearch.find()) {
188             String databaseName = regexSearch.group(1);
189             String tableCount = regexSearch.group(2);
190 
191             var newDatabase = new Database(databaseName, tableCount);
192             databases.add(newDatabase);
193         }
194 
195         this.injectionModel.sendToViews(new Seal.AddDatabases(databases));
196         return databases;
197     }
198 
199     /**
200      * Get tables name and row count and send them to the view.<br>
201      * Use readable text (not hexa) and parse this pattern:<br>
202      * => hh[table name 1]jj[rows count]hhgghh[table name 2]jj[rows count]hhggh...hi<br>
203      * Data window can be cut before the end of the request but the process helps to obtain
204      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
205      * @param database which contains tables to find
206      * @return list of tables found
207      * @throws JSqlException when injection failure or stopped by user
208      */
209     public List<Table> listTables(Database database) throws JSqlException {
210         // Reset stoppedByUser if list of Databases is partial
211         // and some Tables are still reachable
212         this.injectionModel.setIsStoppedByUser(false);
213         
214         // Inform the view that database has just been used
215         this.injectionModel.sendToViews(new Seal.StartProgress(database));
216 
217         var tableCount = Integer.toString(database.getChildCount());
218         
219         String resultToParse = StringUtils.EMPTY;
220         try {
221             var pageSource = new String[]{ StringUtils.EMPTY };
222             resultToParse = new SuspendableGetRows(this.injectionModel).run(new Input(
223                 this.injectionModel.getMediatorEngine().getEngine().instance().sqlTables(database),
224                 pageSource,
225                 true,
226                 Integer.parseInt(tableCount),
227                 database,
228                 "tables"
229             ));
230         } catch (AbstractSlidingException e) {
231             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
232         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
233             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
234         }
235 
236         // Parse all the data we have retrieved
237         var regexSearch = Pattern.compile(
238                 DataAccess.MODE
239                 + DataAccess.ENCLOSE_VALUE_RGX
240                 + DataAccess.CELL_TABLE
241                 + DataAccess.ENCLOSE_VALUE_RGX
242             )
243             .matcher(resultToParse);
244         
245         this.injectionModel.sendToViews(new Seal.EndProgress(database));
246 
247         if (!regexSearch.find()) {
248             throw new InjectionFailureException("No match while injecting tables");
249         }
250         regexSearch.reset();
251         
252         List<Table> tables = new ArrayList<>();
253         
254         // Build an array of Table objects from the data we have parsed
255         while (regexSearch.find()) {
256             String tableName = regexSearch.group(1);
257             String rowCount = regexSearch.group(2);
258             
259             var newTable = new Table(tableName, rowCount, database);
260             tables.add(newTable);
261         }
262 
263         this.injectionModel.sendToViews(new Seal.AddTables(tables));
264         return tables;
265     }
266 
267     /**
268      * Get column names and send them to the view.<br>
269      * Use readable text (not hexa) and parse this pattern with 2nd member forced to 31 (1 in ascii):<br>
270      * => hh[column name 1]jj[31]hhgghh[column name 2]jj[31]hhggh...hi<br>
271      * Data window can be cut before the end of the request but the process helps to obtain
272      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
273      * @param table which contains columns to find
274      * @return list of columns found
275      * @throws JSqlException when injection failure or stopped by user
276      */
277     public List<Column> listColumns(Table table) throws JSqlException {
278         List<Column> columns = new ArrayList<>();
279         
280         // Inform the view that table has just been used
281         this.injectionModel.sendToViews(new Seal.StartIndeterminateProgress(table));
282 
283         String resultToParse = StringUtils.EMPTY;
284         
285         try {
286             var pageSource = new String[]{ StringUtils.EMPTY };
287             resultToParse = new SuspendableGetRows(this.injectionModel).run(new Input(
288                 this.injectionModel.getMediatorEngine().getEngine().instance().sqlColumns(table),
289                 pageSource,
290                 true,
291                 0,
292                 table,
293                 "columns"
294             ));
295         } catch (AbstractSlidingException e) {
296             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
297         } catch (Exception e) {  // Catch all exceptions but prevent detecting bug
298             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
299         }
300 
301         // Build SQLite columns
302         if (this.injectionModel.getMediatorEngine().isSqlite()) {
303             resultToParse = this.injectionModel.getMediatorEngine().getSqlite().transformSqlite(resultToParse);
304         }
305         
306         // Parse all the data we have retrieved
307         var regexSearch = Pattern.compile(
308                 DataAccess.MODE
309                 + DataAccess.ENCLOSE_VALUE_RGX
310                 + DataAccess.CELL_TABLE
311                 + DataAccess.ENCLOSE_VALUE_RGX
312             )
313             .matcher(resultToParse);
314 
315         this.injectionModel.sendToViews(new Seal.EndIndeterminateProgress(table));
316 
317         if (!regexSearch.find()) {
318             throw new InjectionFailureException("No match while injecting columns");
319         }
320 
321         regexSearch.reset();
322 
323         // Build an array of Column objects from the data we have parsed
324         while (regexSearch.find()) {
325             String nameColumn = regexSearch.group(1);
326             var column = new Column(nameColumn, table);
327             columns.add(column);
328         }
329 
330         this.injectionModel.sendToViews(new Seal.AddColumns(columns));
331         return columns;
332     }
333 
334     /**
335      * Get table values and count each occurrence and send it to the view.<br>
336      * Values are on clear text (not hexa) and follows this window pattern<br>
337      * => hh[value 1]jj[count]hhgghh[value 2]jj[count]hhggh...hi<br>
338      * Data window can be cut before the end of the request but the process helps to obtain
339      * the rest of the unreachable data. The process can be interrupted by the user (stop/pause).
340      * @param columnsBean choice by the user
341      * @return a 2x2 table containing values by columns
342      * @throws JSqlException when injection failure or stopped by user
343      */
344     public String[][] listValues(List<Column> columnsBean) throws JSqlException {
345         var databaseBean = (Database) columnsBean.getFirst().getParent().getParent();
346         var tableBean = (Table) columnsBean.getFirst().getParent();
347         int rowCount = columnsBean.getFirst().getParent().getChildCount();
348 
349         // Inform the view that table has just been used
350         this.injectionModel.sendToViews(new Seal.StartProgress(tableBean));
351 
352         // Build an array of column names
353         List<String> columnsName = new ArrayList<>();
354         for (AbstractElementDatabase columnBean: columnsBean) {
355             columnsName.add(columnBean.toString());
356         }
357 
358         // From that array, build the SQL fields nicely
359         // => col1{%}col2...
360         // ==> trim(ifnull(`col1`,0x00)),0x7f,trim(ifnull(`Col2`,0x00))...
361         String[] columns = columnsName.toArray(new String[0]);
362 
363         List<List<String>> listValues = this.getRows(databaseBean, tableBean, rowCount, columns);
364 
365         // Add the default title to the columns: row number, occurrence
366         columnsName.addFirst(StringUtils.EMPTY);
367         columnsName.addFirst(StringUtils.EMPTY);
368 
369         String[][] table = this.getTable(columnsName, listValues);
370 
371         columns = columnsName.toArray(new String[0]);
372         
373         this.injectionModel.sendToViews(new Seal.CreateValuesTab(columns, table, tableBean));
374         this.injectionModel.sendToViews(new Seal.EndProgress(tableBean));
375         return table;
376     }
377 
378     private List<List<String>> getRows(Database database, Table table, int rowCount, String[] columns) throws InjectionFailureException {
379         String resultToParse = StringUtils.EMPTY;
380         
381         try {
382             var pageSource = new String[]{ StringUtils.EMPTY };
383             resultToParse = new SuspendableGetRows(this.injectionModel).run(new Input(
384                 this.injectionModel.getMediatorEngine().getEngine().instance().sqlRows(columns, database, table),
385                 pageSource,
386                 true,
387                 rowCount,
388                 table,
389                 "rows"
390             ));
391         } catch (AbstractSlidingException e) {  // Catch all exceptions but prevent detecting bug
392             resultToParse = DataAccess.getPartialResultAndLog(e, resultToParse);
393         } catch (Exception e) {
394             LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage(), e);
395         }
396         return SuspendableGetRows.parse(resultToParse);
397     }
398 
399     private static String getPartialResultAndLog(AbstractSlidingException e, String resultToParse) {
400         LOGGER.log(LogLevelUtil.CONSOLE_ERROR, e.getMessage());
401 
402         // Get pieces of data already retrieved instead of losing them
403         String resultFromWindow = resultToParse;
404         if (StringUtils.isNotEmpty(e.getSlidingWindowAllRows())) {
405             resultFromWindow = e.getSlidingWindowAllRows();
406         } else if (StringUtils.isNotEmpty(e.getSlidingWindowCurrentRows())) {
407             resultFromWindow = e.getSlidingWindowCurrentRows();
408         }
409         return resultFromWindow;
410     }
411 
412     private String[][] getTable(List<String> columnsName, List<List<String>> values) {
413         // Build a proper 2D array from the data
414         var table = new String[values.size()][columnsName.size()];
415         
416         for (var indexRow = 0 ; indexRow < values.size() ; indexRow++) {
417             var isIncomplete = false;
418             
419             for (var indexColumn = 0 ; indexColumn < columnsName.size() ; indexColumn++) {
420                 try {
421                     table[indexRow][indexColumn] = values.get(indexRow).get(indexColumn);
422                 } catch (IndexOutOfBoundsException e) {
423                     isIncomplete = true;
424                     LOGGER.log(LogLevelUtil.CONSOLE_DEFAULT, I18nUtil.valueByKey("LOG_LIST_VALUES_INCOMPLETE"));
425                     LOGGER.log(LogLevelUtil.IGNORE, e);
426                 }
427             }
428             
429             if (isIncomplete) {
430                 int logIndexRow = indexRow;
431                 LOGGER.log(
432                     LogLevelUtil.CONSOLE_ERROR,
433                     "{}{}: ",
434                     () -> I18nUtil.valueByKey("LOG_LIST_VALUES_TOO_LONG"),
435                     () -> logIndexRow + 1
436                 );
437 
438                 LOGGER.log(
439                     LogLevelUtil.CONSOLE_ERROR,
440                     () -> String.join(
441                         ", ",
442                         values.get(logIndexRow).toArray(new String[0])
443                     )
444                 );
445             }
446         }
447         return table;
448     }
449 }