]> git.basschouten.com Git - openhab-addons.git/blob
f5df1dbf8609cf0a7b66850c05c21a2b5c3803f7
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.persistence.jdbc.internal;
14
15 import java.lang.reflect.InvocationTargetException;
16 import java.util.Collections;
17 import java.util.Enumeration;
18 import java.util.Map;
19 import java.util.Objects;
20 import java.util.Properties;
21 import java.util.Set;
22 import java.util.regex.Matcher;
23 import java.util.regex.Pattern;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.persistence.jdbc.internal.db.JdbcBaseDAO;
28 import org.openhab.persistence.jdbc.internal.utils.MovingAverage;
29 import org.openhab.persistence.jdbc.internal.utils.StringUtilsExt;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
32
33 /**
34  * Configuration class
35  *
36  * @author Helmut Lehmeyer - Initial contribution
37  */
38 @NonNullByDefault
39 public class JdbcConfiguration {
40     private final Logger logger = LoggerFactory.getLogger(JdbcConfiguration.class);
41
42     private static final Pattern EXTRACT_CONFIG_PATTERN = Pattern.compile("^(.*?)\\.([0-9.a-zA-Z]+)$");
43     private static final String DB_DAO_PACKAGE = "org.openhab.persistence.jdbc.internal.db.Jdbc";
44
45     private Map<Object, Object> configuration;
46
47     private @NonNullByDefault({}) JdbcBaseDAO dBDAO;
48     private @Nullable String dbName;
49     boolean dbConnected = false;
50     boolean driverAvailable = false;
51
52     private @Nullable String serviceName;
53     private String name = "jdbc";
54     public final boolean valid;
55
56     // private String url;
57     // private String user;
58     // private String password;
59     private int numberDecimalcount = 3;
60     private boolean tableUseRealItemNames = false;
61     private boolean tableCaseSensitiveItemNames = false;
62     private String itemsManageTable = "items";
63     private String tableNamePrefix = "item";
64     private int tableIdDigitCount = 4;
65     private boolean rebuildTableNames = false;
66
67     private int errReconnectThreshold = 0;
68
69     public int timerCount = 0;
70     public int time1000Statements = 0;
71     public long timer1000 = 0;
72     public MovingAverage timeAverage50arr = new MovingAverage(50);
73     public MovingAverage timeAverage100arr = new MovingAverage(100);
74     public MovingAverage timeAverage200arr = new MovingAverage(200);
75     public boolean enableLogTime = false;
76
77     public JdbcConfiguration(Map<Object, Object> configuration) {
78         logger.debug("JDBC::JdbcConfiguration");
79         this.configuration = configuration;
80         valid = updateConfig();
81     }
82
83     private boolean updateConfig() {
84         logger.debug("JDBC::updateConfig: configuration size = {}", configuration.size());
85
86         String user = (String) configuration.get("user");
87         String password = (String) configuration.get("password");
88
89         // mandatory url
90         String url = (String) configuration.get("url");
91
92         if (url == null) {
93             logger.error("Mandatory url parameter is missing in configuration!");
94             return false;
95         }
96
97         Properties parsedURL = StringUtilsExt.parseJdbcURL(url);
98
99         if (user == null || user.isBlank()) {
100             logger.debug("No jdbc:user parameter defined in jdbc.cfg");
101         }
102         if (password == null || password.isBlank()) {
103             logger.debug("No jdbc:password parameter defined in jdbc.cfg.");
104         }
105
106         if (url.isBlank()) {
107             logger.debug(
108                     "JDBC url is missing - please configure in jdbc.cfg like 'jdbc:<service>:<host>[:<port>;<attributes>]'");
109             return false;
110         }
111
112         if ("false".equalsIgnoreCase(parsedURL.getProperty("parseValid"))) {
113             Enumeration<?> en = parsedURL.propertyNames();
114             String enstr = "";
115             for (Object key : Collections.list(en)) {
116                 enstr += key + " = " + parsedURL.getProperty("" + key) + "\n";
117             }
118             logger.warn(
119                     "JDBC url is not well formatted: {}\nPlease configure in openhab.cfg like 'jdbc:<service>:<host>[:<port>;<attributes>]'",
120                     enstr);
121             return false;
122         }
123
124         logger.debug("JDBC::updateConfig: user={}", user);
125         logger.debug("JDBC::updateConfig: password exists? {}", password != null && !password.isBlank());
126         logger.debug("JDBC::updateConfig: url={}", url);
127
128         // set database type and database type class
129         setDBDAOClass(Objects.requireNonNull(parsedURL.getProperty("dbShortcut"))); // derby, h2, hsqldb, mariadb,
130                                                                                     // mysql, postgresql,
131         // sqlite, timescaledb
132         // set user
133         if (user != null && !user.isBlank()) {
134             dBDAO.databaseProps.setProperty("dataSource.user", user);
135         }
136
137         // set password
138         if (password != null && !password.isBlank()) {
139             dBDAO.databaseProps.setProperty("dataSource.password", password);
140         }
141
142         // set sql-types from external config
143         setSqlTypes();
144
145         final Pattern isNumericPattern = Pattern.compile("\\d+(\\.\\d+)?");
146         String et = (String) configuration.get("reconnectCnt");
147         if (et != null && !et.isBlank() && isNumericPattern.matcher(et).matches()) {
148             errReconnectThreshold = Integer.parseInt(et);
149             logger.debug("JDBC::updateConfig: errReconnectThreshold={}", errReconnectThreshold);
150         }
151
152         String mt = (String) configuration.get("itemsManageTable");
153         if (mt != null && !mt.isBlank()) {
154             itemsManageTable = mt;
155             logger.debug("JDBC::updateConfig: itemsManageTable={}", itemsManageTable);
156         }
157
158         String np = (String) configuration.get("tableNamePrefix");
159         if (np != null && !np.isBlank()) {
160             tableNamePrefix = np;
161             logger.debug("JDBC::updateConfig: tableNamePrefix={}", tableNamePrefix);
162         }
163
164         String dd = (String) configuration.get("numberDecimalcount");
165         if (dd != null && !dd.isBlank() && isNumericPattern.matcher(dd).matches()) {
166             numberDecimalcount = Integer.parseInt(dd);
167             logger.debug("JDBC::updateConfig: numberDecimalcount={}", numberDecimalcount);
168         }
169
170         String rn = (String) configuration.get("tableUseRealItemNames");
171         if (rn != null && !rn.isBlank()) {
172             tableUseRealItemNames = "true".equals(rn) ? Boolean.parseBoolean(rn) : false;
173             logger.debug("JDBC::updateConfig: tableUseRealItemNames={}", tableUseRealItemNames);
174         }
175
176         String lc = (String) configuration.get("tableCaseSensitiveItemNames");
177         if (lc != null && !lc.isBlank()) {
178             tableCaseSensitiveItemNames = Boolean.parseBoolean(lc);
179             logger.debug("JDBC::updateConfig: tableCaseSensitiveItemNames={}", tableCaseSensitiveItemNames);
180         }
181
182         String td = (String) configuration.get("tableIdDigitCount");
183         if (td != null && !td.isBlank() && isNumericPattern.matcher(td).matches()) {
184             tableIdDigitCount = Integer.parseInt(td);
185             logger.debug("JDBC::updateConfig: tableIdDigitCount={}", tableIdDigitCount);
186         }
187
188         String rt = (String) configuration.get("rebuildTableNames");
189         if (rt != null && !rt.isBlank()) {
190             rebuildTableNames = Boolean.parseBoolean(rt);
191             logger.debug("JDBC::updateConfig: rebuildTableNames={}", rebuildTableNames);
192         }
193
194         // undocumented
195         String ac = (String) configuration.get("maximumPoolSize");
196         if (ac != null && !ac.isBlank()) {
197             dBDAO.databaseProps.setProperty("maximumPoolSize", ac);
198         }
199
200         // undocumented
201         String ic = (String) configuration.get("minimumIdle");
202         if (ic != null && !ic.isBlank()) {
203             dBDAO.databaseProps.setProperty("minimumIdle", ic);
204         }
205
206         // undocumented
207         String it = (String) configuration.get("idleTimeout");
208         if (it != null && !it.isBlank()) {
209             dBDAO.databaseProps.setProperty("idleTimeout", it);
210         }
211         // undocumented
212         String ent = (String) configuration.get("enableLogTime");
213         if (ent != null && !ent.isBlank()) {
214             enableLogTime = "true".equals(ent) ? Boolean.parseBoolean(ent) : false;
215         }
216         logger.debug("JDBC::updateConfig: enableLogTime {}", enableLogTime);
217
218         // undocumented
219         String fd = (String) configuration.get("driverClassName");
220         if (fd != null && !fd.isBlank()) {
221             dBDAO.databaseProps.setProperty("driverClassName", fd);
222         }
223
224         // undocumented
225         String ds = (String) configuration.get("dataSourceClassName");
226         if (ds != null && !ds.isBlank()) {
227             dBDAO.databaseProps.setProperty("dataSourceClassName", ds);
228         }
229
230         // undocumented
231         String dn = dBDAO.databaseProps.getProperty("driverClassName");
232         if (dn == null) {
233             dn = dBDAO.databaseProps.getProperty("dataSourceClassName");
234         } else {
235             dBDAO.databaseProps.setProperty("jdbcUrl", url);
236         }
237
238         // test if JDBC driver bundle is available
239         testJDBCDriver(Objects.requireNonNull(dn));
240
241         logger.debug("JDBC::updateConfig: configuration complete. service={}", getName());
242
243         return true;
244     }
245
246     private void setDBDAOClass(String sn) {
247         String serviceName;
248
249         // set database type
250         if (sn.isBlank() || sn.length() < 2) {
251             logger.error(
252                     "JDBC::updateConfig: Required database url like 'jdbc:<service>:<host>[:<port>;<attributes>]' - please configure the jdbc:url parameter in openhab.cfg");
253             serviceName = "none";
254         } else {
255             serviceName = sn;
256         }
257         this.serviceName = serviceName;
258         logger.debug("JDBC::updateConfig: found serviceName = '{}'", serviceName);
259
260         // set class for database type
261         String ddp = DB_DAO_PACKAGE + serviceName.toUpperCase().charAt(0) + serviceName.toLowerCase().substring(1)
262                 + "DAO";
263
264         logger.debug("JDBC::updateConfig: Init Data Access Object Class: '{}'", ddp);
265         try {
266             dBDAO = (JdbcBaseDAO) Class.forName(ddp).getConstructor().newInstance();
267             logger.debug("JDBC::updateConfig: dBDAO ClassName={}", dBDAO.getClass().getName());
268         } catch (IllegalAccessException | InstantiationException | InvocationTargetException
269                 | NoSuchMethodException e) {
270             logger.error("JDBC::updateConfig: Exception: {}", e.getMessage());
271         } catch (ClassNotFoundException e) {
272             logger.warn("JDBC::updateConfig: no Configuration for serviceName '{}' found. ClassNotFoundException: {}",
273                     serviceName, e.getMessage());
274             logger.debug("JDBC::updateConfig: using default Database Configuration: JdbcBaseDAO !!");
275             dBDAO = new JdbcBaseDAO();
276             logger.debug("JDBC::updateConfig: dBConfig done");
277         }
278     }
279
280     private void setSqlTypes() {
281         Set<Object> keys = configuration.keySet();
282
283         for (Object k : keys) {
284             String key = (String) k;
285             Matcher matcher = EXTRACT_CONFIG_PATTERN.matcher(key);
286             if (!matcher.matches()) {
287                 continue;
288             }
289             matcher.reset();
290             matcher.find();
291             if (!"sqltype".equals(matcher.group(1))) {
292                 continue;
293             }
294             String itemType = matcher.group(2);
295             if (!itemType.startsWith("table")) {
296                 itemType = itemType.toUpperCase() + "ITEM";
297             }
298             String value = (String) configuration.get(key);
299             logger.debug("JDBC::updateConfig: set sqlTypes: itemType={} value={}", itemType, value);
300             if (value != null) {
301                 dBDAO.sqlTypes.put(itemType, value);
302             }
303         }
304     }
305
306     private void testJDBCDriver(String driver) {
307         driverAvailable = true;
308         try {
309             Class.forName(driver);
310             logger.debug("JDBC::updateConfig: load JDBC-driverClass was successful: '{}'", driver);
311         } catch (ClassNotFoundException e) {
312             driverAvailable = false;
313             logger.error(
314                     "JDBC::updateConfig: could NOT load JDBC-driverClassName or JDBC-dataSourceClassName. ClassNotFoundException: '{}'",
315                     e.getMessage());
316             String warn = ""
317                     + "\n\n\t!!!\n\tTo avoid this error, place an appropriate JDBC driver file for serviceName '{}' in addons directory.\n"
318                     + "\tCopy missing JDBC-Driver-jar to your openHab/addons Folder.\n\t!!!\n" + "\tDOWNLOAD: \n";
319             String serviceName = this.serviceName;
320             if (serviceName != null) {
321                 switch (serviceName) {
322                     case "derby":
323                         warn += "\tDerby:     version >= 10.14.2.0 from          https://mvnrepository.com/artifact/org.apache.derby/derby\n";
324                         break;
325                     case "h2":
326                         warn += "\tH2:        version >= 2.2.224 from            https://mvnrepository.com/artifact/com.h2database/h2\n";
327                         break;
328                     case "hsqldb":
329                         warn += "\tHSQLDB:    version >= 2.3.3 from              https://mvnrepository.com/artifact/org.hsqldb/hsqldb\n";
330                         break;
331                     case "mariadb":
332                         warn += "\tMariaDB:   version >= 3.0.8 from              https://mvnrepository.com/artifact/org.mariadb.jdbc/mariadb-java-client\n";
333                         break;
334                     case "mysql":
335                         warn += "\tMySQL:     version >= 8.2.0 from              https://mvnrepository.com/artifact/com.mysql/mysql-connector-j\n";
336                         break;
337                     case "postgresql":
338                         warn += "\tPostgreSQL:version >= 42.4.4 from             https://mvnrepository.com/artifact/org.postgresql/postgresql\n";
339                         break;
340                     case "sqlite":
341                         warn += "\tSQLite:    version >= 3.42.0.0 from           https://mvnrepository.com/artifact/org.xerial/sqlite-jdbc\n";
342                         break;
343                 }
344             }
345             logger.warn(warn, serviceName);
346         }
347     }
348
349     public Properties getHikariConfiguration() {
350         return dBDAO.getConnectionProperties();
351     }
352
353     public String getName() {
354         // return serviceName;
355         return name;
356     }
357
358     public @Nullable String getServiceName() {
359         return serviceName;
360     }
361
362     public String getItemsManageTable() {
363         return itemsManageTable;
364     }
365
366     public String getTableNamePrefix() {
367         return tableNamePrefix;
368     }
369
370     public int getErrReconnectThreshold() {
371         return errReconnectThreshold;
372     }
373
374     public boolean getRebuildTableNames() {
375         return rebuildTableNames;
376     }
377
378     public int getNumberDecimalcount() {
379         return numberDecimalcount;
380     }
381
382     public boolean getTableUseRealItemNames() {
383         return tableUseRealItemNames;
384     }
385
386     public boolean getTableCaseSensitiveItemNames() {
387         return tableCaseSensitiveItemNames;
388     }
389
390     /**
391      * Checks if real item names (without number suffix) is enabled.
392      *
393      * @return true if both tableUseRealItemNames and tableCaseSensitiveItemNames are enabled.
394      */
395     public boolean getTableUseRealCaseSensitiveItemNames() {
396         return tableUseRealItemNames && tableCaseSensitiveItemNames;
397     }
398
399     public int getTableIdDigitCount() {
400         return tableIdDigitCount;
401     }
402
403     public JdbcBaseDAO getDBDAO() {
404         return dBDAO;
405     }
406
407     public @Nullable String getDbName() {
408         return dbName;
409     }
410
411     public void setDbName(String dbName) {
412         this.dbName = dbName;
413     }
414
415     public boolean isDbConnected() {
416         return dbConnected;
417     }
418
419     public void setDbConnected(boolean dbConnected) {
420         logger.debug("JDBC::setDbConnected {}", dbConnected);
421         // Initializing step, after db is connected.
422         // Initialize sqlTypes, depending on DB version for example
423         dBDAO.initAfterFirstDbConnection();
424         // Running once again to prior external configured SqlTypes!
425         setSqlTypes();
426         this.dbConnected = dbConnected;
427     }
428
429     public boolean isDriverAvailable() {
430         return driverAvailable;
431     }
432 }