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