View Javadoc
1   /*
2    * Prometheus: Application Framework
3    * Copyright 2012-2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  package io.github.tonywasher.joceanus.prometheus.database;
18  
19  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
20  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogManager;
21  import io.github.tonywasher.joceanus.oceanus.logger.OceanusLogger;
22  import io.github.tonywasher.joceanus.oceanus.profile.OceanusProfile;
23  import io.github.tonywasher.joceanus.prometheus.data.PrometheusCryptographyDataType;
24  import io.github.tonywasher.joceanus.prometheus.data.PrometheusDataSet;
25  import io.github.tonywasher.joceanus.prometheus.exc.PrometheusIOException;
26  import io.github.tonywasher.joceanus.tethys.api.thread.TethysUIThreadStatusReport;
27  
28  import java.sql.Connection;
29  import java.sql.DriverManager;
30  import java.sql.PreparedStatement;
31  import java.sql.SQLException;
32  import java.util.ArrayList;
33  import java.util.List;
34  import java.util.ListIterator;
35  import java.util.Properties;
36  
37  /**
38   * Class that encapsulates a database connection.
39   */
40  public abstract class PrometheusDataStore
41          implements PrometheusDatabaseControl {
42      /**
43       * Number of update steps per table (INSERT/UPDATE/DELETE).
44       */
45      private static final int NUM_STEPS_PER_TABLE = 3;
46  
47      /**
48       * User property name.
49       */
50      private static final String PROPERTY_USER = "user";
51  
52      /**
53       * Password property name.
54       */
55      private static final String PROPERTY_PASS = "password";
56  
57      /**
58       * Instance property name.
59       */
60      private static final String PROPERTY_INSTANCE = "instance";
61  
62      /**
63       * Encrypt property name.
64       */
65      private static final String PROPERTY_ENCRYPT = "encrypt";
66  
67      /**
68       * Logger.
69       */
70      private static final OceanusLogger LOGGER = OceanusLogManager.getLogger(PrometheusDataStore.class);
71  
72      /**
73       * Database connection.
74       */
75      private Connection theConn;
76  
77      /**
78       * Database name.
79       */
80      private String theDatabase;
81  
82      /**
83       * Batch Size.
84       */
85      private final Integer theBatchSize;
86  
87      /**
88       * Database Driver.
89       */
90      private final PrometheusJDBCDriver theDriver;
91  
92      /**
93       * List of Database tables.
94       */
95      private final List<PrometheusTableInstance<?>> theTables;
96  
97      /**
98       * Construct a new Database class.
99       *
100      * @param pDatabase the database
101      * @param pConfig   the config
102      * @throws OceanusException on error
103      */
104     protected PrometheusDataStore(final String pDatabase,
105                                   final PrometheusDBConfig pConfig) throws OceanusException {
106         /* Create the connection */
107         try {
108             /* Access the batch size */
109             theBatchSize = pConfig.getBatchSize();
110 
111             /* Access the JDBC Driver */
112             theDriver = pConfig.getDriver();
113 
114             /* Store the name */
115             theDatabase = pDatabase;
116 
117             /* Obtain the connection */
118             final String myConnString = theDriver.getConnectionString(pDatabase, pConfig.getServer(), pConfig.getPort());
119 
120             /* Create the properties and record user */
121             final Properties myProperties = new Properties();
122             final String myUser = pConfig.getUser();
123             final char[] myPass = pConfig.getPassword();
124             myProperties.setProperty(PROPERTY_USER, myUser);
125             myProperties.setProperty(PROPERTY_PASS, new String(myPass));
126 
127             /* If we are using instance */
128             if (theDriver.useInstance()) {
129                 final String myInstance = pConfig.getInstance();
130                 myProperties.setProperty(PROPERTY_INSTANCE, myInstance);
131                 myProperties.setProperty(PROPERTY_ENCRYPT, "false");
132             }
133 
134             /* Connect using properties */
135             theConn = DriverManager.getConnection(myConnString, myProperties);
136 
137             /* Connect to the correct database */
138             theConn.setCatalog(pDatabase);
139 
140             /* Switch off autoCommit */
141             theConn.setAutoCommit(false);
142 
143             /* handle exceptions */
144         } catch (SQLException e) {
145             throw new PrometheusIOException("Failed to load driver", e);
146         }
147 
148         /* Create table list and add the tables to the list */
149         theTables = new ArrayList<>();
150 
151         /* Loop through the tables */
152         for (PrometheusCryptographyDataType myType : PrometheusCryptographyDataType.values()) {
153             /* Create the sheet */
154             theTables.add(newTable(myType));
155         }
156     }
157 
158     /**
159      * Construct a new Database class.
160      *
161      * @param pConfig the config
162      * @throws OceanusException on error
163      */
164     protected PrometheusDataStore(final PrometheusDBConfig pConfig) throws OceanusException {
165         /* Create the connection */
166         try {
167             /* Access the batch size */
168             theBatchSize = pConfig.getBatchSize();
169 
170             /* Access the JDBC Driver */
171             theDriver = pConfig.getDriver();
172 
173             /* Obtain the connection */
174             final String myConnString = theDriver.getConnectionString(pConfig.getServer(), pConfig.getPort());
175 
176             /* Create the properties and record user */
177             final Properties myProperties = new Properties();
178             final String myUser = pConfig.getUser();
179             final char[] myPass = pConfig.getPassword();
180             myProperties.setProperty(PROPERTY_USER, myUser);
181             myProperties.setProperty(PROPERTY_PASS, new String(myPass));
182 
183             /* If we are using instance */
184             if (theDriver.useInstance()) {
185                 final String myInstance = pConfig.getInstance();
186                 myProperties.setProperty(PROPERTY_INSTANCE, myInstance);
187                 myProperties.setProperty(PROPERTY_ENCRYPT, "false");
188             }
189 
190             /* Connect using properties */
191             theConn = DriverManager.getConnection(myConnString, myProperties);
192 
193             /* Switch off autoCommit */
194             theConn.setAutoCommit(false);
195 
196             /* handle exceptions */
197         } catch (SQLException e) {
198             throw new PrometheusIOException("Failed to load driver", e);
199         }
200 
201         /* Create table list and add the tables to the list */
202         theTables = new ArrayList<>();
203 
204         /* Loop through the tables */
205         for (PrometheusCryptographyDataType myType : PrometheusCryptographyDataType.values()) {
206             /* Create the sheet */
207             theTables.add(newTable(myType));
208         }
209     }
210 
211     /**
212      * Obtain the database name.
213      *
214      * @return the name
215      */
216     public String getName() {
217         return theDatabase;
218     }
219 
220     /**
221      * Execute the statement outside a transaction.
222      *
223      * @param pStatement the statement
224      * @throws OceanusException on error
225      */
226     void executeStatement(final String pStatement) throws OceanusException {
227         /* Protect the statement and execute without commit */
228         try (PreparedStatement myStmt = theConn.prepareStatement(pStatement)) {
229             theConn.setAutoCommit(true);
230             myStmt.execute();
231             theConn.setAutoCommit(false);
232 
233         } catch (SQLException e) {
234             throw new PrometheusIOException("Failed to execute statement", e);
235         }
236     }
237 
238     /**
239      * Create new sheet of required type.
240      *
241      * @param pListType the list type
242      * @return the new sheet
243      */
244     private PrometheusTableDataItem<?> newTable(final PrometheusCryptographyDataType pListType) {
245         /* Switch on list Type */
246         return switch (pListType) {
247             case CONTROLDATA -> new PrometheusTableControlData(this);
248             case CONTROLKEY -> new PrometheusTableControlKeys(this);
249             case CONTROLKEYSET -> new PrometheusTableControlKeySet(this);
250             case DATAKEYSET -> new PrometheusTableDataKeySet(this);
251             default -> throw new IllegalArgumentException(pListType.toString());
252         };
253     }
254 
255     @Override
256     public PrometheusJDBCDriver getDriver() {
257         return theDriver;
258     }
259 
260     @Override
261     public Connection getConn() {
262         return theConn;
263     }
264 
265     /**
266      * Add a table.
267      *
268      * @param pTable the Table to add
269      */
270     protected void addTable(final PrometheusTableInstance<?> pTable) {
271         pTable.resolveReferences(theTables);
272         theTables.add(pTable);
273     }
274 
275     /**
276      * Close the connection to the database rolling back any outstanding transaction.
277      */
278     public void close() {
279         /* Ignore if no connection */
280         if (theConn == null) {
281             return;
282         }
283 
284         /* Protect against exceptions */
285         try {
286             /* Roll-back any outstanding transaction */
287             if (!theConn.getAutoCommit()) {
288                 theConn.rollback();
289             }
290 
291             /* Loop through the tables */
292             for (PrometheusTableInstance<?> myInstance : theTables) {
293                 final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myInstance;
294 
295                 /* Close the Statement */
296                 myTable.closeStmt();
297             }
298 
299             /* Close the connection */
300             theConn.close();
301             theConn = null;
302 
303             /* Discard Exceptions */
304         } catch (SQLException e) {
305             LOGGER.error("Failed to close database connection", e);
306             theConn = null;
307         }
308     }
309 
310     /**
311      * Load data from the database.
312      *
313      * @param pReport the report
314      * @param pData   the new DataSet
315      * @throws OceanusException on error
316      */
317     public void loadDatabase(final TethysUIThreadStatusReport pReport,
318                              final PrometheusDataSet pData) throws OceanusException {
319         /* Initialise task */
320         pReport.initTask("loadDatabase");
321         pReport.setNumStages(theTables.size());
322 
323         /* Obtain the active profile */
324         OceanusProfile myTask = pReport.getActiveTask();
325         myTask = myTask.startTask("loadDatabase");
326 
327         /* Loop through the tables */
328         for (PrometheusTableInstance<?> myInstance : theTables) {
329             final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myInstance;
330 
331             /* Note the new step */
332             myTask.startTask(myTable.getTableName());
333 
334             /* Load the items */
335             myTable.loadItems(pReport, pData);
336         }
337 
338         /* Complete the task */
339         myTask.end();
340     }
341 
342     /**
343      * Update data into database.
344      *
345      * @param pReport the report
346      * @param pData   the data
347      * @throws OceanusException on error
348      */
349     public void updateDatabase(final TethysUIThreadStatusReport pReport,
350                                final PrometheusDataSet pData) throws OceanusException {
351         /* Set the number of stages */
352         final PrometheusBatchControl myBatch = new PrometheusBatchControl(theBatchSize);
353         pReport.setNumStages(NUM_STEPS_PER_TABLE * theTables.size());
354 
355         /* Obtain the active profile */
356         OceanusProfile myTask = pReport.getActiveTask();
357         myTask = myTask.startTask("updateDatabase");
358 
359         /* Loop through the tables */
360         OceanusProfile myStage = myTask.startTask("insertData");
361         for (PrometheusTableInstance<?> myInstance : theTables) {
362             final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myInstance;
363 
364             /* Note the new step */
365             myStage.startTask(myTable.getTableName());
366 
367             /* insert the items */
368             myTable.insertItems(pReport, pData, myBatch);
369         }
370 
371         /* Loop through the tables */
372         myStage = myTask.startTask("updateData");
373         final ListIterator<PrometheusTableInstance<?>> myListIterator = theTables.listIterator();
374         while (myListIterator.hasNext()) {
375             final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myListIterator.next();
376 
377             /* Note the new step */
378             myStage.startTask(myTable.getTableName());
379 
380             /* Load the items */
381             myTable.updateItems(pReport, myBatch);
382         }
383 
384         /* Loop through the tables in reverse order */
385         myStage = myTask.startTask("deleteData");
386         while (myListIterator.hasPrevious()) {
387             final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myListIterator.previous();
388 
389             /* Note the new step */
390             myStage.startTask(myTable.getTableName());
391 
392             /* Delete items from the table */
393             myTable.deleteItems(pReport, myBatch);
394         }
395 
396         /* If we have active work in the batch */
397         if (myBatch.isActive()) {
398             /* Commit the database */
399             try {
400                 theConn.commit();
401             } catch (SQLException e) {
402                 close();
403                 throw new PrometheusIOException("Failed to commit transaction", e);
404             }
405 
406             /* Commit the batch */
407             myBatch.commitItems();
408         }
409 
410         /* Complete the task */
411         myTask.end();
412     }
413 
414     /**
415      * Create database.
416      *
417      * @param pReport   the report
418      * @param pDatabase the database to create
419      * @throws OceanusException on error
420      */
421     public void createDatabase(final TethysUIThreadStatusReport pReport,
422                                final String pDatabase) throws OceanusException {
423         /* Set the number of stages */
424         pReport.setNumStages(2);
425 
426         /* Obtain the active profile */
427         OceanusProfile myTask = pReport.getActiveTask();
428         myTask = myTask.startTask("dropDatabase");
429         executeStatement("DROP DATABASE IF EXISTS " + pDatabase);
430 
431         /* Create database */
432         myTask = myTask.startTask("createDatabase");
433         executeStatement("CREATE DATABASE " + pDatabase);
434 
435         /* Complete the task */
436         myTask.end();
437     }
438 
439     /**
440      * Create tables.
441      *
442      * @param pReport the report
443      * @throws OceanusException on error
444      */
445     public void createTables(final TethysUIThreadStatusReport pReport) throws OceanusException {
446         /* Set the number of stages */
447         pReport.setNumStages(2);
448 
449         /* Drop any existing tables */
450         dropTables(pReport);
451 
452         /* Obtain the active profile */
453         OceanusProfile myTask = pReport.getActiveTask();
454         myTask = myTask.startTask("createTables");
455 
456         /* Loop through the tables */
457         for (PrometheusTableInstance<?> myInstance : theTables) {
458             final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myInstance;
459             /* Check for cancellation */
460             pReport.checkForCancellation();
461 
462             /* Note the new step */
463             myTask.startTask(myTable.getTableName());
464 
465             /* Create the table */
466             myTable.createTable();
467         }
468 
469         /* Complete the task */
470         myTask.end();
471     }
472 
473     /**
474      * Drop tables.
475      *
476      * @param pReport the report
477      * @throws OceanusException on error
478      */
479     private void dropTables(final TethysUIThreadStatusReport pReport) throws OceanusException {
480         /* Obtain the active profile */
481         OceanusProfile myTask = pReport.getActiveTask();
482         myTask = myTask.startTask("dropTables");
483 
484         /* Loop through the tables in reverse order */
485         final ListIterator<PrometheusTableInstance<?>> myIterator = theTables.listIterator(theTables.size());
486         while (myIterator.hasPrevious()) {
487             final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myIterator.previous();
488 
489             /* Check for cancellation */
490             pReport.checkForCancellation();
491 
492             /* Note the new step */
493             myTask.startTask(myTable.getTableName());
494 
495             /* Drop the table */
496             myTable.dropTable();
497         }
498 
499         /* Complete the task */
500         myTask.end();
501     }
502 
503     /**
504      * Purge tables.
505      *
506      * @param pReport the report
507      * @throws OceanusException on error
508      */
509     public void purgeTables(final TethysUIThreadStatusReport pReport) throws OceanusException {
510         /* Set the number of stages */
511         pReport.setNumStages(1);
512 
513         /* Obtain the active profile */
514         OceanusProfile myTask = pReport.getActiveTask();
515         myTask = myTask.startTask("purgeTables");
516 
517         /* Loop through the tables in reverse order */
518         final ListIterator<PrometheusTableInstance<?>> myIterator = theTables.listIterator(theTables.size());
519         while (myIterator.hasPrevious()) {
520             final PrometheusTableDataItem<?> myTable = (PrometheusTableDataItem<?>) myIterator.previous();
521 
522             /* Check for cancellation */
523             pReport.checkForCancellation();
524 
525             /* Note the new step */
526             myTask.startTask(myTable.getTableName());
527 
528             /* Purge the table */
529             myTable.purgeTable();
530         }
531 
532         /* Complete the task */
533         myTask.end();
534     }
535 }