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: we have to close Connections/Statements that way, because a dbcp PoolablePreparedStatement 145 // is not a DelegatedStatement; for that reason its not removed from the trace of the connection when it is closed. 146 // So, the connection tries to close it again when the connection is closed itself; 147 // as a result there is an error that forces the connection to be destroyed and not pooled 148 149 if (dbc == null) { 150 LOG.error(Messages.get().getBundle().key(Messages.LOG_NULL_DB_CONTEXT_0)); 151 } 152 153 try { 154 // first, close the result set 155 if (res != null) { 156 res.close(); 157 } 158 } catch (SQLException e) { 159 LOG.debug(e.getLocalizedMessage(), e); 160 } finally { 161 res = null; 162 } 163 164 try { 165 // close the statement 166 if (stmnt != null) { 167 stmnt.close(); 168 } 169 } catch (SQLException e) { 170 LOG.debug(e.getLocalizedMessage(), e); 171 } finally { 172 stmnt = null; 173 } 174 175 try { 176 // close the connection 177 if ((con != null) && !con.isClosed()) { 178 con.close(); 179 } 180 } catch (SQLException e) { 181 LOG.debug(e.getLocalizedMessage(), e); 182 } finally { 183 con = null; 184 } 185 186 } 187 188 /** 189 * Retrieves the value of the designated column in the current row of this ResultSet object as 190 * a byte array in the Java programming language.<p> 191 * 192 * The bytes represent the raw values returned by the driver. Overwrite this method if another 193 * database server requires a different handling of byte attributes in tables.<p> 194 * 195 * @param res the result set 196 * @param attributeName the name of the table attribute 197 * 198 * @return byte[] the column value; if the value is SQL NULL, the value returned is null 199 * 200 * @throws SQLException if a database access error occurs 201 */ 202 public byte[] getBytes(ResultSet res, String attributeName) throws SQLException { 203 204 return res.getBytes(attributeName); 205 } 206 207 /** 208 * Returns a JDBC connection from the connection pool.<p> 209 * 210 * Use this method to get a connection for reading/writing project independent data.<p> 211 * 212 * @param dbc the current database context 213 * 214 * @return a JDBC connection 215 * 216 * @throws SQLException if the project id is not supported 217 */ 218 public Connection getConnection(CmsDbContext dbc) throws SQLException { 219 220 if (dbc == null) { 221 LOG.error(Messages.get().getBundle().key(Messages.LOG_NULL_DB_CONTEXT_0)); 222 } 223 // match the ID to a JDBC pool URL of the OpenCms JDBC pools {online|offline|backup} 224 return getConnectionByUrl(m_poolUrl); 225 } 226 227 /** 228 * Returns a PreparedStatement for a JDBC connection specified by the key of a SQL query 229 * and the CmsProject.<p> 230 * 231 * @param con the JDBC connection 232 * @param project the specified CmsProject 233 * @param queryKey the key of the SQL query 234 * 235 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 236 * 237 * @throws SQLException if a database access error occurs 238 */ 239 public PreparedStatement getPreparedStatement(Connection con, CmsProject project, String queryKey) 240 throws SQLException { 241 242 return getPreparedStatement(con, project.getUuid(), queryKey); 243 } 244 245 /** 246 * Returns a PreparedStatement for a JDBC connection specified by the key of a SQL query 247 * and the project-ID.<p> 248 * 249 * @param con the JDBC connection 250 * @param projectId the ID of the specified CmsProject 251 * @param queryKey the key of the SQL query 252 * 253 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 254 * 255 * @throws SQLException if a database access error occurs 256 */ 257 public PreparedStatement getPreparedStatement(Connection con, CmsUUID projectId, String queryKey) 258 throws SQLException { 259 260 String rawSql = readQuery(projectId, queryKey); 261 return getPreparedStatementForSql(con, rawSql); 262 } 263 264 /** 265 * Returns a PreparedStatement for a JDBC connection specified by the key of a SQL query.<p> 266 * 267 * @param con the JDBC connection 268 * @param queryKey the key of the SQL query 269 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 270 * @throws SQLException if a database access error occurs 271 */ 272 public PreparedStatement getPreparedStatement(Connection con, String queryKey) throws SQLException { 273 274 String rawSql = readQuery(CmsUUID.getNullUUID(), queryKey); 275 return getPreparedStatementForSql(con, rawSql); 276 } 277 278 /** 279 * Returns a PreparedStatement for a JDBC connection specified by the SQL query.<p> 280 * 281 * @param con the JDBC connection 282 * @param query the SQL query 283 * @return PreparedStatement a new PreparedStatement containing the pre-compiled SQL statement 284 * @throws SQLException if a database access error occurs 285 */ 286 public PreparedStatement getPreparedStatementForSql(Connection con, String query) throws SQLException { 287 288 // unfortunately, this wrapper is essential, because some JDBC driver 289 // implementations don't accept the delegated objects of DBCP's connection pool. 290 return con.prepareStatement(query); 291 } 292 293 /** 294 * Initializes this SQL manager.<p> 295 * 296 * @param driverType the type ID of the driver (vfs,user,project or history) from where this SQL manager is referenced 297 * @param poolUrl the pool URL to get connections from the JDBC driver manager 298 */ 299 public void init(int driverType, String poolUrl) { 300 301 m_driverType = driverType; 302 m_poolUrl = poolUrl; 303 304 } 305 306 /** 307 * Searches for the SQL query with the specified key and CmsProject.<p> 308 * 309 * @param project the specified CmsProject 310 * @param queryKey the key of the SQL query 311 * @return the the SQL query in this property list with the specified key 312 */ 313 public String readQuery(CmsProject project, String queryKey) { 314 315 return readQuery(project.getUuid(), queryKey); 316 } 317 318 /** 319 * Searches for the SQL query with the specified key and project-ID.<p> 320 * 321 * For projectIds ≠ 0, the pattern {@link #QUERY_PROJECT_SEARCH_PATTERN} in table names of queries is 322 * replaced with "_ONLINE_" or "_OFFLINE_" to choose the right database 323 * tables for SQL queries that are project dependent! 324 * 325 * @param projectId the ID of the specified CmsProject 326 * @param queryKey the key of the SQL query 327 * @return the the SQL query in this property list with the specified key 328 */ 329 public String readQuery(CmsUUID projectId, String queryKey) { 330 331 String key; 332 if ((projectId != null) && !projectId.isNullUUID()) { 333 // id 0 is special, please see below 334 StringBuffer buffer = new StringBuffer(128); 335 buffer.append(queryKey); 336 if (projectId.equals(CmsProject.ONLINE_PROJECT_ID)) { 337 buffer.append("_ONLINE"); 338 } else { 339 buffer.append("_OFFLINE"); 340 } 341 key = buffer.toString(); 342 } else { 343 key = queryKey; 344 } 345 346 // look up the query in the cache 347 String query = m_cachedQueries.get(key); 348 349 if (query == null) { 350 // the query has not been cached yet 351 // get the SQL statement from the properties hash 352 query = readQuery(queryKey); 353 354 if (query == null) { 355 throw new CmsRuntimeException(Messages.get().container(Messages.ERR_QUERY_NOT_FOUND_1, queryKey)); 356 } 357 358 // replace control chars. 359 query = CmsStringUtil.substitute(query, "\t", " "); 360 query = CmsStringUtil.substitute(query, "\n", " "); 361 362 if ((projectId != null) && !projectId.isNullUUID()) { 363 // a project ID = 0 is an internal indicator that a project-independent 364 // query was requested - further regex operations are not required then 365 query = CmsSqlManager.replaceProjectPattern(projectId, query); 366 } 367 368 // to minimize costs, all statements with replaced expressions are cached in a map 369 m_cachedQueries.put(key, query); 370 } 371 372 return query; 373 } 374 375 /** 376 * Searches for the SQL query with the specified key.<p> 377 * 378 * @param queryKey the SQL query key 379 * @return the the SQL query in this property list with the specified key 380 */ 381 public String readQuery(String queryKey) { 382 383 String value = m_queries.get(queryKey); 384 if (value == null) { 385 if (LOG.isErrorEnabled()) { 386 LOG.error(Messages.get().getBundle().key(Messages.LOG_QUERY_NOT_FOUND_1, queryKey)); 387 } 388 } 389 return value; 390 } 391 392 /** 393 * Sets the designated parameter to the given Java array of bytes.<p> 394 * 395 * The driver converts this to an SQL VARBINARY or LONGVARBINARY (depending on the argument's 396 * size relative to the driver's limits on VARBINARY values) when it sends it to the database. 397 * 398 * @param statement the PreparedStatement where the content is set 399 * @param pos the first parameter is 1, the second is 2, ... 400 * @param content the parameter value 401 * @throws SQLException if a database access error occurs 402 */ 403 public void setBytes(PreparedStatement statement, int pos, byte[] content) throws SQLException { 404 405 if (content.length < 2000) { 406 statement.setBytes(pos, content); 407 } else { 408 statement.setBinaryStream(pos, new ByteArrayInputStream(content), content.length); 409 } 410 } 411 412 /** 413 * Replaces null or empty Strings with a String with one space character <code>" "</code>.<p> 414 * 415 * @param value the string to validate 416 * @return the validate string or a String with one space character if the validated string is null or empty 417 */ 418 public String validateEmpty(String value) { 419 420 if (CmsStringUtil.isNotEmpty(value)) { 421 return value; 422 } 423 424 return " "; 425 } 426 427 /** 428 * Loads a Java properties hash containing SQL queries.<p> 429 * 430 * @param propertyFilename the package/filename of the properties hash 431 */ 432 protected void loadQueryProperties(String propertyFilename) { 433 434 Properties properties = new Properties(); 435 436 try { 437 properties.load(getClass().getClassLoader().getResourceAsStream(propertyFilename)); 438 m_queries.putAll(CmsCollectionsGenericWrapper.<String, String> map(properties)); 439 replaceQuerySearchPatterns(); 440 } catch (Throwable t) { 441 if (LOG.isErrorEnabled()) { 442 LOG.error( 443 Messages.get().getBundle().key(Messages.LOG_LOAD_QUERY_PROP_FILE_FAILED_1, propertyFilename), 444 t); 445 } 446 447 properties = null; 448 } 449 } 450 451 /** 452 * Replaces patterns ${XXX} by another property value, if XXX is a property key with a value.<p> 453 */ 454 protected synchronized void replaceQuerySearchPatterns() { 455 456 String currentKey = null; 457 String currentValue = null; 458 int startIndex = 0; 459 int endIndex = 0; 460 int lastIndex = 0; 461 462 Iterator<String> allKeys = m_queries.keySet().iterator(); 463 while (allKeys.hasNext()) { 464 currentKey = allKeys.next(); 465 currentValue = m_queries.get(currentKey); 466 startIndex = 0; 467 endIndex = 0; 468 lastIndex = 0; 469 470 while ((startIndex = currentValue.indexOf("${", lastIndex)) != -1) { 471 endIndex = currentValue.indexOf('}', startIndex); 472 if ((endIndex != -1) && !currentValue.startsWith(QUERY_PROJECT_SEARCH_PATTERN, startIndex - 1)) { 473 474 String replaceKey = currentValue.substring(startIndex + 2, endIndex); 475 String searchPattern = currentValue.substring(startIndex, endIndex + 1); 476 String replacePattern = this.readQuery(replaceKey); 477 478 if (replacePattern != null) { 479 currentValue = CmsStringUtil.substitute(currentValue, searchPattern, replacePattern); 480 } 481 } 482 483 lastIndex = endIndex + 2; 484 } 485 m_queries.put(currentKey, currentValue); 486 } 487 } 488}