001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (https://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software GmbH & Co. KG, please see the 018 * company website: https://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: https://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.db.generic; 029 030import org.opencms.db.CmsDbContext; 031import org.opencms.file.CmsProject; 032import org.opencms.main.CmsLog; 033import org.opencms.main.CmsRuntimeException; 034import org.opencms.util.CmsCollectionsGenericWrapper; 035import org.opencms.util.CmsStringUtil; 036import org.opencms.util.CmsUUID; 037 038import java.io.ByteArrayInputStream; 039import java.sql.Connection; 040import java.sql.PreparedStatement; 041import java.sql.ResultSet; 042import java.sql.SQLException; 043import java.sql.Statement; 044import java.util.Collections; 045import java.util.HashMap; 046import java.util.Iterator; 047import java.util.List; 048import java.util.Map; 049import java.util.Properties; 050import java.util.concurrent.ConcurrentHashMap; 051 052import org.apache.commons.logging.Log; 053 054/** 055 * Generic (ANSI-SQL) implementation of the SQL manager.<p> 056 * 057 * @since 6.0.0 058 */ 059public class CmsSqlManager extends org.opencms.db.CmsSqlManager { 060 061 /** A pattern being replaced in SQL queries to generate SQL queries to access online/offline tables. */ 062 protected static final String QUERY_PROJECT_SEARCH_PATTERN = "_${PROJECT}_"; 063 064 /** The log object for this class. */ 065 private static final Log LOG = CmsLog.getLog(CmsSqlManager.class); 066 067 /** The filename/path of the SQL query properties. */ 068 private static final String QUERY_PROPERTIES = "org/opencms/db/generic/query.properties"; 069 070 /** A map to cache queries with replaced search patterns. */ 071 protected ConcurrentHashMap<String, String> m_cachedQueries; 072 073 /** The type ID of the driver (vfs, user, project or history) from where this SQL manager is referenced. */ 074 protected int m_driverType; 075 076 /** The pool URL to get connections from the JDBC driver manager, including DBCP's pool URL prefix. */ 077 protected String m_poolUrl; 078 079 /** A map holding all SQL queries. */ 080 protected Map<String, String> m_queries; 081 082 /** 083 * Creates a new, empty SQL manager.<p> 084 */ 085 public CmsSqlManager() { 086 087 this(Collections.<String> emptyList()); 088 } 089 090 /** 091 * Creates a new SQL manager and loads additional SQL query property files after the generic queries.<p> 092 * 093 * @param additionalQueryProperties the additional SQL query property files to load 094 */ 095 public CmsSqlManager(List<String> additionalQueryProperties) { 096 097 m_cachedQueries = new ConcurrentHashMap<String, String>(); 098 m_queries = new HashMap<String, String>(); 099 loadQueryProperties(QUERY_PROPERTIES); 100 loadAdditionalQueryProperties(additionalQueryProperties); 101 } 102 103 /** 104 * Creates a new instance of a SQL manager.<p> 105 * 106 * @param classname the classname of the SQL manager 107 * 108 * @return a new instance of the SQL manager 109 */ 110 public static org.opencms.db.generic.CmsSqlManager getInstance(String classname) { 111 112 return getInstance(classname, Collections.<String> emptyList()); 113 } 114 115 /** 116 * Creates a new instance of a SQL manager and loads additional SQL query property files.<p> 117 * 118 * @param classname the classname of the SQL manager 119 * @param additionalQueryProperties the additional SQL query property files to load 120 * 121 * @return a new instance of the SQL manager 122 */ 123 public static org.opencms.db.generic.CmsSqlManager getInstance( 124 String classname, 125 List<String> additionalQueryProperties) { 126 127 org.opencms.db.generic.CmsSqlManager sqlManager; 128 129 try { 130 Class<?> sqlManagerClass = Class.forName(classname); 131 Object objectInstance; 132 try { 133 objectInstance = sqlManagerClass.getConstructor(List.class).newInstance(additionalQueryProperties); 134 } catch (NoSuchMethodException e) { 135 if ((additionalQueryProperties == null) || additionalQueryProperties.isEmpty()) { 136 objectInstance = sqlManagerClass.newInstance(); 137 } else { 138 throw e; 139 } 140 } 141 sqlManager = (org.opencms.db.generic.CmsSqlManager)objectInstance; 142 } catch (Throwable t) { 143 LOG.error(Messages.get().getBundle().key(Messages.LOG_SQL_MANAGER_INIT_FAILED_1, classname), t); 144 sqlManager = null; 145 } 146 147 if (CmsLog.INIT.isInfoEnabled()) { 148 CmsLog.INIT.info(Messages.get().getBundle().key(Messages.INIT_DRIVER_SQL_MANAGER_1, classname)); 149 } 150 151 return sqlManager; 152 153 } 154 155 /** 156 * Replaces the project search pattern in SQL queries by the pattern _ONLINE_ or _OFFLINE_ depending on the 157 * specified project ID.<p> 158 * 159 * @param projectId the ID of the current project 160 * @param query the SQL query 161 * @return String the SQL query with the table key search pattern replaced 162 */ 163 protected static String replaceProjectPattern(CmsUUID projectId, String query) { 164 165 // make the statement project dependent 166 String replacePattern = ((projectId == null) || projectId.equals(CmsProject.ONLINE_PROJECT_ID)) 167 ? "_ONLINE_" 168 : "_OFFLINE_"; 169 return CmsStringUtil.substitute(query, QUERY_PROJECT_SEARCH_PATTERN, replacePattern); 170 } 171 172 /** 173 * Attempts to close the connection, statement and result set after a statement has been executed.<p> 174 * 175 * @param dbc the current database context 176 * @param con the JDBC connection 177 * @param stmnt the statement 178 * @param res the result set 179 */ 180 public void closeAll(CmsDbContext dbc, Connection con, Statement stmnt, ResultSet res) { 181 182 // NOTE: The following note is outdated, since we are now using HikariCP as a connection pool, 183 // which doesn't have prepared statement pooling. Closing the connection in a finally block or using 184 // the try-with-resources syntax is sufficient. 185 186 // NOTE: (outdated) we have to close Connections/Statements that way, because a dbcp PoolablePreparedStatement 187 // is not a DelegatedStatement; for that reason its not removed from the trace of the connection when it is closed. 188 // So, the connection tries to close it again when the connection is closed itself; 189 // as a result there is an error that forces the connection to be destroyed and not pooled 190 191 if (dbc == null) { 192 LOG.error(Messages.get().getBundle().key(Messages.LOG_NULL_DB_CONTEXT_0)); 193 } 194 195 try { 196 // first, close the result set 197 if (res != null) { 198 res.close(); 199 } 200 } catch (SQLException e) { 201 LOG.debug(e.getLocalizedMessage(), e); 202 } finally { 203 res = null; 204 } 205 206 try { 207 // close the statement 208 if (stmnt != null) { 209 stmnt.close(); 210 } 211 } catch (SQLException e) { 212 LOG.debug(e.getLocalizedMessage(), e); 213 } finally { 214 stmnt = null; 215 } 216 217 try { 218 // close the connection 219 if ((con != null) && !con.isClosed()) { 220 con.close(); 221 } 222 } catch (SQLException e) { 223 LOG.debug(e.getLocalizedMessage(), e); 224 } finally { 225 con = null; 226 } 227 228 } 229 230 /** 231 * Retrieves the value of the designated column in the current row of this ResultSet object as 232 * a byte array in the Java programming language.<p> 233 * 234 * The bytes represent the raw values returned by the driver. Overwrite this method if another 235 * database server requires a different handling of byte attributes in tables.<p> 236 * 237 * @param res the result set 238 * @param attributeName the name of the table attribute 239 * 240 * @return byte[] the column value; if the value is SQL NULL, the value returned is null 241 * 242 * @throws SQLException if a database access error occurs 243 */ 244 public byte[] getBytes(ResultSet res, String attributeName) throws SQLException { 245 246 return res.getBytes(attributeName); 247 } 248 249 /** 250 * Returns a JDBC connection from the connection pool.<p> 251 * 252 * Use this method to get a connection for reading/writing project independent data.<p> 253 * 254 * @param dbc the current database context 255 * 256 * @return a JDBC connection 257 * 258 * @throws SQLException if the project id is not supported 259 */ 260 public Connection getConnection(CmsDbContext dbc) throws SQLException { 261 262 if (dbc == null) { 263 LOG.error(Messages.get().getBundle().key(Messages.LOG_NULL_DB_CONTEXT_0)); 264 } 265 // match the ID to a JDBC pool URL of the OpenCms JDBC pools {online|offline|backup} 266 return getConnectionByUrl(m_poolUrl); 267 } 268 269 /** 270 * Returns a PreparedStatement for a JDBC connection specified by the key of a SQL query 271 * and the CmsProject.<p> 272 * 273 * @param con the JDBC connection 274 * @param project the specified CmsProject 275 * @param queryKey the key of the SQL query 276 * 277 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 278 * 279 * @throws SQLException if a database access error occurs 280 */ 281 public PreparedStatement getPreparedStatement(Connection con, CmsProject project, String queryKey) 282 throws SQLException { 283 284 return getPreparedStatement(con, project.getUuid(), queryKey); 285 } 286 287 /** 288 * Returns a PreparedStatement for a JDBC connection specified by the key of a SQL query 289 * and the project-ID.<p> 290 * 291 * @param con the JDBC connection 292 * @param projectId the ID of the specified CmsProject 293 * @param queryKey the key of the SQL query 294 * 295 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 296 * 297 * @throws SQLException if a database access error occurs 298 */ 299 public PreparedStatement getPreparedStatement(Connection con, CmsUUID projectId, String queryKey) 300 throws SQLException { 301 302 String rawSql = readQuery(projectId, queryKey); 303 return getPreparedStatementForSql(con, rawSql); 304 } 305 306 /** 307 * Returns a PreparedStatement for a JDBC connection specified by the key of a SQL query.<p> 308 * 309 * @param con the JDBC connection 310 * @param queryKey the key of the SQL query 311 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 312 * @throws SQLException if a database access error occurs 313 */ 314 public PreparedStatement getPreparedStatement(Connection con, String queryKey) throws SQLException { 315 316 String rawSql = readQuery(CmsUUID.getNullUUID(), queryKey); 317 return getPreparedStatementForSql(con, rawSql); 318 } 319 320 /** 321 * Returns a PreparedStatement for a JDBC connection specified by the SQL query.<p> 322 * 323 * @param con the JDBC connection 324 * @param query the SQL query 325 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 326 * @throws SQLException if a database access error occurs 327 */ 328 public PreparedStatement getPreparedStatementForSql(Connection con, String query) throws SQLException { 329 330 // unfortunately, this wrapper is essential, because some JDBC driver 331 // implementations don't accept the delegated objects of DBCP's connection pool. 332 return con.prepareStatement(query); 333 } 334 335 /** 336 * Initializes this SQL manager.<p> 337 * 338 * @param driverType the type ID of the driver (vfs,user,project or history) from where this SQL manager is referenced 339 * @param poolUrl the pool URL to get connections from the JDBC driver manager 340 */ 341 public void init(int driverType, String poolUrl) { 342 343 m_driverType = driverType; 344 m_poolUrl = poolUrl; 345 346 } 347 348 /** 349 * Searches for the SQL query with the specified key and CmsProject.<p> 350 * 351 * @param project the specified CmsProject 352 * @param queryKey the key of the SQL query 353 * @return the the SQL query in this property list with the specified key 354 */ 355 public String readQuery(CmsProject project, String queryKey) { 356 357 return readQuery(project.getUuid(), queryKey); 358 } 359 360 /** 361 * Searches for the SQL query with the specified key and project-ID.<p> 362 * 363 * For projectIds ≠ 0, the pattern {@link #QUERY_PROJECT_SEARCH_PATTERN} in table names of queries is 364 * replaced with "_ONLINE_" or "_OFFLINE_" to choose the right database 365 * tables for SQL queries that are project dependent! 366 * 367 * @param projectId the ID of the specified CmsProject 368 * @param queryKey the key of the SQL query 369 * @return the the SQL query in this property list with the specified key 370 */ 371 public String readQuery(CmsUUID projectId, String queryKey) { 372 373 String key; 374 if ((projectId != null) && !projectId.isNullUUID()) { 375 // id 0 is special, please see below 376 StringBuffer buffer = new StringBuffer(128); 377 buffer.append(queryKey); 378 if (projectId.equals(CmsProject.ONLINE_PROJECT_ID)) { 379 buffer.append("_ONLINE"); 380 } else { 381 buffer.append("_OFFLINE"); 382 } 383 key = buffer.toString(); 384 } else { 385 key = queryKey; 386 } 387 388 // look up the query in the cache 389 String query = m_cachedQueries.get(key); 390 391 if (query == null) { 392 // the query has not been cached yet 393 // get the SQL statement from the properties hash 394 query = readQuery(queryKey); 395 396 if (query == null) { 397 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_QUERY_NOT_FOUND_1, queryKey)); 398 } 399 400 // replace control chars. 401 query = CmsStringUtil.substitute(query, "\t", " "); 402 query = CmsStringUtil.substitute(query, "\n", " "); 403 404 if ((projectId != null) && !projectId.isNullUUID()) { 405 // a project ID = 0 is an internal indicator that a project-independent 406 // query was requested - further regex operations are not required then 407 query = CmsSqlManager.replaceProjectPattern(projectId, query); 408 } 409 410 // to minimize costs, all statements with replaced expressions are cached in a map 411 m_cachedQueries.put(key, query); 412 } 413 414 return query; 415 } 416 417 /** 418 * Searches for the SQL query with the specified key.<p> 419 * 420 * @param queryKey the SQL query key 421 * @return the the SQL query in this property list with the specified key 422 */ 423 public String readQuery(String queryKey) { 424 425 String value = m_queries.get(queryKey); 426 if (value == null) { 427 if (LOG.isErrorEnabled()) { 428 LOG.error(Messages.get().getBundle().key(Messages.LOG_QUERY_NOT_FOUND_1, queryKey)); 429 } 430 } 431 return value; 432 } 433 434 /** 435 * Sets the designated parameter to the given Java array of bytes.<p> 436 * 437 * The driver converts this to an SQL VARBINARY or LONGVARBINARY (depending on the argument's 438 * size relative to the driver's limits on VARBINARY values) when it sends it to the database. 439 * 440 * @param statement the PreparedStatement where the content is set 441 * @param pos the first parameter is 1, the second is 2, ... 442 * @param content the parameter value 443 * @throws SQLException if a database access error occurs 444 */ 445 public void setBytes(PreparedStatement statement, int pos, byte[] content) throws SQLException { 446 447 if (content.length < 2000) { 448 statement.setBytes(pos, content); 449 } else { 450 statement.setBinaryStream(pos, new ByteArrayInputStream(content), content.length); 451 } 452 } 453 454 /** 455 * Replaces null or empty Strings with a String with one space character <code>" "</code>.<p> 456 * 457 * @param value the string to validate 458 * @return the validate string or a String with one space character if the validated string is null or empty 459 */ 460 public String validateEmpty(String value) { 461 462 if (CmsStringUtil.isNotEmpty(value)) { 463 return value; 464 } 465 466 return " "; 467 } 468 469 /** 470 * Loads additional Java properties hashes containing SQL queries.<p> 471 * 472 * @param additionalQueryProperties the additional SQL query property files to load 473 */ 474 protected void loadAdditionalQueryProperties(List<String> additionalQueryProperties) { 475 476 if (additionalQueryProperties == null) { 477 return; 478 } 479 for (String propertyFilename : additionalQueryProperties) { 480 if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(propertyFilename)) { 481 loadQueryProperties(propertyFilename); 482 } 483 } 484 } 485 486 /** 487 * Loads a Java properties hash containing SQL queries.<p> 488 * 489 * @param propertyFilename the package/filename of the properties hash 490 */ 491 protected void loadQueryProperties(String propertyFilename) { 492 493 Properties properties = new Properties(); 494 495 try { 496 properties.load(getClass().getClassLoader().getResourceAsStream(propertyFilename)); 497 m_queries.putAll(CmsCollectionsGenericWrapper.<String, String> map(properties)); 498 replaceQuerySearchPatterns(); 499 } catch (Throwable t) { 500 if (LOG.isErrorEnabled()) { 501 LOG.error( 502 Messages.get().getBundle().key(Messages.LOG_LOAD_QUERY_PROP_FILE_FAILED_1, propertyFilename), 503 t); 504 } 505 506 properties = null; 507 } 508 } 509 510 /** 511 * Replaces patterns ${XXX} by another property value, if XXX is a property key with a value.<p> 512 */ 513 protected synchronized void replaceQuerySearchPatterns() { 514 515 String currentKey = null; 516 String currentValue = null; 517 int startIndex = 0; 518 int endIndex = 0; 519 int lastIndex = 0; 520 521 Iterator<String> allKeys = m_queries.keySet().iterator(); 522 while (allKeys.hasNext()) { 523 currentKey = allKeys.next(); 524 currentValue = m_queries.get(currentKey); 525 startIndex = 0; 526 endIndex = 0; 527 lastIndex = 0; 528 529 while ((startIndex = currentValue.indexOf("${", lastIndex)) != -1) { 530 endIndex = currentValue.indexOf('}', startIndex); 531 if ((endIndex != -1) && !currentValue.startsWith(QUERY_PROJECT_SEARCH_PATTERN, startIndex - 1)) { 532 533 String replaceKey = currentValue.substring(startIndex + 2, endIndex); 534 String searchPattern = currentValue.substring(startIndex, endIndex + 1); 535 String replacePattern = this.readQuery(replaceKey); 536 537 if (replacePattern != null) { 538 currentValue = CmsStringUtil.substitute(currentValue, searchPattern, replacePattern); 539 } 540 } 541 542 lastIndex = endIndex + 2; 543 } 544 m_queries.put(currentKey, currentValue); 545 } 546 } 547}