This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit a9dd4976a83505bed470dc5708545bc0d68649e6 Author: opencode <[email protected]> AuthorDate: Mon Nov 2 19:33:15 2026 +0100 Harden the JNDI factory's attribute parsing and data source routing The JNDI object factory used to read a resource Reference leniently and report poorly: unparsed attribute values were silently dropped or misread as defaults, a cleared attribute could configure a property to its empty value instead of leaving it off, and the two routes by which a data source can back the pool - the object-valued dataSource attribute and the dataSourceJNDI lookup - each rejected or misused valid and invalid configurations with messages that named neither the attribute nor the offending value. A full review campaign (original defect report plus five clean-room review rounds) produced the changes now folded into this one, grouped here by subject. Parsing is now strict and self-describing. An attribute whose value is blank means "not configured" uniformly - booleans, numerics, the transaction isolation level, the catalog, driverClassName, jdbcInterceptors, validationQuery, object_name, dataSource and dataSourceJNDI all leave their property at the PoolProperties default when the value carries nothing but whitespace. A value the property cannot take fails the resource naming the attribute and quoting the value, instead of being silently ignored: unparseable booleans and numbers, numbers outside an int's range, unknown isolation levels (whose java.sql.Connection numeric form is now accepted alongside the names). Numbers, keywords, class names and JNDI names are applied trimmed; identifiers - catalog, validation query, pool name - are stored verbatim, since whitespace can be meaningful in them. The backing data source is validated on both routes, with context. The dataSource attribute now carries a live javax.sql.DataSource, XADataSource or ConnectionPoolDataSource object through to the pool instead of stringifying it into a rejection; a genuinely text-valued attribute fails with guidance pointing at the two routes that do work. A pool cannot wrap another pool: that case is diagnosed where the factory accepts the object, naming the attribute and the class, and on the lookup route as the declared NamingException rather than an unchecked failure from deep inside pool start. The object a dataSourceJNDI name resolves to is checked to be a data source the connect path can use, blank names mean unset, and the pool's own XA class name is accepted at the reference type gate. Around the factory: the interceptor list parser treats a redundant separator as cosmetic in every position - leading and doubled ones used to abort pool start with a message-less SQLException, while the same separator trailing was dropped - and PoolProperties keeps its credential fields authoritative across a connectionProperties reconfigure. Diagnostics were localized, leveled, and deduplicated; the unknown-attribute sweep can no longer desynchronize from an ALL_PROPERTIES a subclass extends; the PROP_USE_CON_LOCK constant was named for its property. The documentation was made true: the class javadoc no longer promises a DBCP BasicDataSource and now states the blank/strict/trim policy and the method contracts, and webapps/docs/jdbc-pool.xml no longer points operators at a dataSource-as-text route the factory rejects, documents the isolation attribute's numeric form, adds the previously undocumented useLock and object_name attributes, and distinguishes object_name from the SlowQueryReportJmx objectName attribute. jdbc-pool suite: 734 green. --- modules/jdbc-pool/doc/jdbc-pool.xml | 59 ++- .../apache/tomcat/jdbc/pool/DataSourceFactory.java | 454 ++++++++++++++++----- .../apache/tomcat/jdbc/pool/PoolProperties.java | 57 ++- .../tomcat/jdbc/pool/util/LocalStrings.properties | 11 +- .../naming/TestDataSourceFactoryJndiLookup.java | 97 +++++ .../TestDataSourceFactoryNoOpPropertyWarnings.java | 17 +- .../TestDataSourceFactoryRefAddrContent.java | 119 +++++- ...stDataSourceFactoryUnknownPropertyWarnings.java | 2 +- .../tomcat/jdbc/naming/TestNamingFactory.java | 311 ++++++++++++++ .../pool/TestJdbcInterceptorConfigParsing.java | 56 ++- .../tomcat/jdbc/pool/TestPoolProperties.java | 59 +++ 11 files changed, 1095 insertions(+), 147 deletions(-) diff --git a/modules/jdbc-pool/doc/jdbc-pool.xml b/modules/jdbc-pool/doc/jdbc-pool.xml index 307c478d5e..005955e12f 100644 --- a/modules/jdbc-pool/doc/jdbc-pool.xml +++ b/modules/jdbc-pool/doc/jdbc-pool.xml @@ -190,6 +190,15 @@ <section name="Attributes"> <p>To provide a very simple switch to and from commons-dbcp and tomcat-jdbc-pool, Most attributes are the same and have the same meaning.</p> + <p>When the pool is configured through the JNDI resource factory, an attribute whose + value is blank leaves its property at the default: clearing an attribute in a + resource definition switches the property off rather than configuring it to the + empty value. A value the property cannot take - an unparseable number, an unknown + boolean or transaction isolation level - fails the creation of the pool with an + error naming the attribute, rather than being silently ignored. Numeric, boolean + and keyword values, class names and JNDI names are read ignoring surrounding + whitespace; names, queries and URLs are stored verbatim, since a space can be + part of such a value.</p> <subsection name="JNDI Factory and Type"> <attributes> <attribute name="factory" required="true"> @@ -199,8 +208,12 @@ <p>Type should always be <code>javax.sql.DataSource</code> or <code>javax.sql.XADataSource</code></p> <p>Depending on the type a <code>org.apache.tomcat.jdbc.pool.DataSource</code> or a <code>org.apache.tomcat.jdbc.pool.XADataSource</code> will be created.</p> <p>Note that the <code>javax.sql.XADataSource</code> type only provides XA connections if the pool is backed - by an XA data source: the <code>dataSource</code> or <code>dataSourceJNDI</code> attribute must name a - <code>javax.sql.XADataSource</code> provided by the JDBC driver. A pool configured with + by an XA data source: the data source the pool takes its connections from - supplied through the + <code>dataSourceJNDI</code> or <code>dataSource</code> attribute - must implement + <code>javax.sql.XADataSource</code>. In an XML resource definition an attribute can only carry text, + while the <code>dataSource</code> attribute takes the data source object itself, so the XML route to a + backing data source is <code>dataSourceJNDI</code>, naming a <code>javax.sql.XADataSource</code> + provided by the JDBC driver and bound in JNDI. A pool configured with <code>driverClassName</code> and <code>url</code> pools plain JDBC connections, and a request for an XA connection from such a pool fails with an explicit error.</p> </attribute> @@ -247,7 +260,11 @@ <li><code>REPEATABLE_READ</code></li> <li><code>SERIALIZABLE</code></li> </ul> - <p>If not set, the method will not be called and it defaults to the JDBC driver.</p> + <p>The numeric transaction isolation level of <code>java.sql.Connection</code>, for example + <code>2</code> for <code>READ_COMMITTED</code>, is accepted in place of the name. The name or number + is matched ignoring surrounding whitespace, and any value other than the names above, their numbers + or a blank value fails the creation of the pool with an error naming the attribute.</p> + <p>If not set, or set to a blank value, the method will not be called and it defaults to the JDBC driver.</p> </attribute> <attribute name="defaultCatalog" required="false"> @@ -344,7 +361,9 @@ <p>(String) The SQL query that will be used to validate connections from this pool before returning them to the caller. If specified, this query does not have to return any data, it just can't throw an <code>SQLException</code>. The default value is <code>null</code>. - If not specified, connections will be validation by the isValid() method. + If not specified, or set to a blank value, connections will be validated by the + <code>isValid()</code> method of <code>java.sql.Connection</code>: a blank query is not sent to the + database as an empty statement. Example values are <code>SELECT 1</code>(mysql), <code>select 1 from dual</code>(oracle), <code>SELECT 1</code>(MS Sql Server) </p> </attribute> @@ -475,6 +494,16 @@ The default value is <code>true</code>.</p> </attribute> + <attribute name="object_name" required="false"> + <p>(String) The pool's name, used as the <code>name</code> key property of the JMX registrations of the + pool's MBean and of interceptors that register with the pool, such as + <code>SlowQueryReportJmx</code>. When the attribute is set through the JNDI resource factory, + surrounding whitespace is not stripped - the name is stored as it is written - and a blank value + leaves the pool with its generated name. Note that this attribute of the pool is spelled with an + underscore, unlike the <code>objectName</code> attribute of the <code>SlowQueryReportJmx</code> + interceptor described below, which overrides only that interceptor's registration.</p> + </attribute> + <attribute name="fairQueue" required="false"> <p>(boolean) Set to true if you wish that calls to getConnection should be treated fairly in a true FIFO fashion. This uses the <code>org.apache.tomcat.jdbc.pool.FairBlockingQueue</code> @@ -524,6 +553,15 @@ The default value is <code>true</code>. </p> </attribute> + <attribute name="useLock" required="false"> + <p>(boolean) Set to true if a lock should be used when operations are performed on the connection object. + Should be set to <code>false</code> unless you plan to have a background thread of your own doing idle + and abandon checking, such as a JMX client. If the pool sweeper is enabled - the pool's own background + maintenance thread, started by <code>timeBetweenEvictionRunsMillis</code> together with one of the + checks it runs - the lock is used regardless of this setting. + The default value is <code>false</code>. + </p> + </attribute> <attribute name="suspectTimeout" required="false"> <p>(int) Timeout value in seconds. Default value is <code>0</code>.<br/> Similar to to the <code>removeAbandonedTimeout</code> value but instead of treating the connection @@ -568,9 +606,16 @@ <p>(javax.sql.DataSource) Inject a data source to the connection pool, and the pool will use the data source to retrieve connections instead of establishing them using the <code>java.sql.Driver</code> interface. This is useful when you wish to pool XA connections or connections established using a data source instead of a connection string. Default value is <code>null</code> </p> + <p>When the pool is configured through the JNDI resource factory, this attribute carries the + data source object itself, and the object must be a <code>javax.sql.DataSource</code>, a + <code>javax.sql.XADataSource</code> or a <code>javax.sql.ConnectionPoolDataSource</code> provided by + the JDBC driver - and not another connection pool, which the pool refuses to wrap. A resource + definition can only give attributes as text, and text cannot carry a live object; a text value here + fails the creation of the pool. In a resource definition, use this attribute's JNDI counterpart, + <code>dataSourceJNDI</code>, to back the pool with a data source.</p> </attribute> <attribute name="dataSourceJNDI" required="false"> - <p>(String) The JNDI name for a data source to be looked up in JNDI and then used to establish connections to the database. See the <code>dataSource</code> attribute. Default value is <code>null</code>. If the name cannot be resolved when the pool is created, the creation fails with an error naming the unresolved name rather than producing a pool that cannot provide connections. + <p>(String) The JNDI name for a data source to be looked up in JNDI and then used to establish connections to the database. See the <code>dataSource</code> attribute. Default value is <code>null</code>. Surrounding whitespace around the name is ignored. If the name cannot be resolved when the pool is created, or resolves to an object the pool cannot establish connections from, the creation fails with an error naming the unresolved name rather than producing a pool that cannot provi [...] </p> </attribute> <attribute name="useDisposableConnectionFacade" required="false"> @@ -835,6 +880,10 @@ <p>(String) Define a valid <code>javax.management.ObjectName</code> string that will be used to register this object with the platform mbean server The default value is <code>null</code> and the object will be registered using tomcat.jdbc:type=org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReportJmx,name=the-name-of-the-pool + Note this interceptor attribute is spelled <code>objectName</code>, unlike the pool's own + <code>object_name</code> attribute, which names the pool in the registrations; a + <code>objectName</code> attribute on the pool itself is not a pool property and will be reported + as ignored. </p> </attribute> </attributes> diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java index 767a9e34ad..e3b1d5d3bb 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/DataSourceFactory.java @@ -42,22 +42,20 @@ import org.apache.tomcat.jdbc.pool.util.MessageFactory; /** * <p> - * JNDI object factory that creates an instance of <code>BasicDataSource</code> that has been configured based on the - * <code>RefAddr</code> values of the specified <code>Reference</code>, which must match the names and data types of the - * <code>BasicDataSource</code> bean properties. + * JNDI object factory that creates an instance of + * {@link org.apache.tomcat.jdbc.pool.DataSource} - or {@link XADataSource}, when the + * reference names a type to be built as XA - configured from the {@code RefAddr} + * values of the specified {@code Reference}. Address names are matched against the + * pool properties listed in {@link #ALL_PROPERTIES}; they are the attributes of the + * pool documented with {@link PoolConfiguration}'s setters, not the property set of + * any other pool implementation. + * </p> + * <p> + * The factory applies a strict reading of the reference: an attribute whose value is + * blank leaves its property at the {@link PoolProperties} default, and a value the + * property cannot take fails the lookup with an exception naming the attribute + * instead of being silently ignored. * </p> - * <br> - * Properties available for configuration:<br> - * <a href="https://commons.apache.org/dbcp/configuration.html">Commons DBCP properties</a><br> - * <ol> - * <li>initSQL - A query that gets executed once, right after the connection is established.</li> - * <li>testOnConnect - run validationQuery after connection has been established.</li> - * <li>validationInterval - avoid excess validation, only run validation at most at this frequency - time in - * milliseconds.</li> - * <li>jdbcInterceptors - a semicolon separated list of classnames extending {@link JdbcInterceptor} class.</li> - * <li>jmxEnabled - true of false, whether to register the pool with JMX.</li> - * <li>fairQueue - true of false, whether the pool should sacrifice a little bit of performance for true fairness.</li> - * </ol> */ public class DataSourceFactory implements ObjectFactory { /** @@ -261,7 +259,7 @@ public class DataSourceFactory implements ObjectFactory { /** * Property name for useLock. */ - protected static final String PROP_USE_CON_LOCK = "useLock"; + protected static final String PROP_USE_LOCK = "useLock"; /** * Property name for dataSource. @@ -343,7 +341,7 @@ public class DataSourceFactory implements ObjectFactory { PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED, PROP_REMOVEABANDONED, PROP_REMOVEABANDONEDTIMEOUT, PROP_LOGABANDONED, PROP_POOLPREPAREDSTATEMENTS, PROP_MAXOPENPREPAREDSTATEMENTS, PROP_CONNECTIONPROPERTIES, PROP_INITSQL, PROP_INTERCEPTORS, PROP_JMX_ENABLED, PROP_FAIR_QUEUE, PROP_USE_EQUALS, OBJECT_NAME, - PROP_ABANDONWHENPERCENTAGEFULL, PROP_MAXAGE, PROP_USE_CON_LOCK, PROP_DATASOURCE, PROP_DATASOURCE_JNDI, + PROP_ABANDONWHENPERCENTAGEFULL, PROP_MAXAGE, PROP_USE_LOCK, PROP_DATASOURCE, PROP_DATASOURCE_JNDI, PROP_SUSPECT_TIMEOUT, PROP_ALTERNATE_USERNAME_ALLOWED, PROP_COMMITONRETURN, PROP_ROLLBACKONRETURN, PROP_USEDISPOSABLECONNECTIONFACADE, PROP_LOGVALIDATIONERRORS, PROP_PROPAGATEINTERRUPTSTATE, PROP_IGNOREEXCEPTIONONPRELOAD, PROP_USESTATEMENTFACADE }; @@ -373,20 +371,14 @@ public class DataSourceFactory implements ObjectFactory { private static final Set<String> RESERVED_JNDI_ADDRESS_NAMES = Collections.unmodifiableSet( new HashSet<>(Arrays.asList("factory", "singleton", "scope", "auth", "description", "forceString"))); - /* - * The recognized property names as a set, for the sweep over the - * reference's addresses that reports everything the lookup loop above - * never read. - */ - private static final Set<String> ALL_PROPERTY_NAMES = Collections.unmodifiableSet( - new HashSet<>(Arrays.asList(ALL_PROPERTIES))); - // -------------------------------------------------- ObjectFactory Methods /** * <p> - * Create and return a new <code>BasicDataSource</code> instance. If no instance can be created, return - * <code>null</code> instead. + * Creates and returns the pooled data source that the given {@code Reference} + * describes: an {@link org.apache.tomcat.jdbc.pool.DataSource}, or an + * {@link XADataSource} when the reference names {@code javax.sql.XADataSource} + * or the pool's own XA class. * </p> * * @param obj The possibly null object containing location or reference information that can be used in @@ -396,14 +388,26 @@ public class DataSourceFactory implements ObjectFactory { * <code>null</code> if <code>name</code> is relative to the default initial context * @param environment The possibly null environment that is used in creating this object * + * @return the configured data source, or <code>null</code> if <code>obj</code> is not a + * <code>Reference</code> or names a class this factory does not build (the + * latter is logged) + * + * @throws IllegalArgumentException if an attribute carries a value its property cannot + * take: an unusable boolean, numeric or transaction-isolation value, or a + * {@code dataSource} attribute holding a String or another connection pool + * @throws NamingException if the name given by the {@code dataSourceJNDI} attribute does + * not resolve to a data source that can back the pool + * * @exception Exception if an exception occurs creating the instance */ @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { - // We only know how to deal with <code>javax.naming.Reference</code>s - // that specify a class name of "javax.sql.DataSource" + // The reference must name one of the types this factory builds: the + // two documented interface names and the pool's own concrete classes + // (the non-XA one has always been accepted; the XA one is exactly + // what the factory news for an XA reference). if ((obj == null) || !(obj instanceof Reference)) { return null; } @@ -420,30 +424,51 @@ public class DataSourceFactory implements ObjectFactory { if (org.apache.tomcat.jdbc.pool.DataSource.class.getName().equals(ref.getClassName())) { ok = true; } + if (org.apache.tomcat.jdbc.pool.XADataSource.class.getName().equals(ref.getClassName())) { + ok = true; + XA = true; + } if (!ok) { log.warn(MessageFactory.get("dataSourceFactory.invalidClassName", ref.getClassName())); return null; } - Properties properties = new Properties(); - DataSource dataSourceObject = null; + Object dataSourceObject = null; for (int i = 0; i < ALL_PROPERTIES.length; i++) { String propertyName = ALL_PROPERTIES[i]; RefAddr ra = ref.get(propertyName); if (ra != null) { Object content = ra.getContent(); if (content == null) { - // RefAddr.getContent() is nullable, and a property with no - // content simply has no value to apply + // RefAddr.getContent() is nullable: a property carrying no + // content has no value to apply. The name is recognised, + // so note the skip where an operator wondering why a + // configured attribute did not take effect will look. + if (log.isDebugEnabled()) { + log.debug(MessageFactory.get("dataSourceFactory.nullContentProperty", propertyName)); + } continue; } - if (PROP_DATASOURCE.equals(propertyName) && content instanceof DataSource) { - // A live data source cannot travel through Properties as a - // string, so keep the object and hand it to the pool below - dataSourceObject = (DataSource) content; - continue; + if (PROP_DATASOURCE.equals(propertyName)) { + if (content instanceof DataSourceProxy) { + // A pool cannot wrap another pool; reported with the + // attribute and the object's class, the context the + // layered-pool rejection deeper down cannot supply - + // the lookup route already diagnoses this binding with + // the same details. + throw new IllegalArgumentException(MessageFactory.get( + "dataSourceFactory.cannotSetDataSourceLayered", + content.getClass().getName())); + } + if (isSupportedDataSourceObject(content)) { + // A live data source cannot travel through Properties + // as a string, so keep the object and hand it to the + // pool below + dataSourceObject = content; + continue; + } } properties.setProperty(propertyName, content.toString()); } @@ -452,11 +477,16 @@ public class DataSourceFactory implements ObjectFactory { // Whatever the lookup above did not recognize is never read, so the // operator hears about it here instead of watching the pool come up // with defaults. Names the JNDI container itself uses to build the - // reference are not pool properties and stay unmentioned. + // reference are not pool properties and stay unmentioned. The set is + // read from ALL_PROPERTIES at lookup time rather than snapshotted at + // class-initialisation: the array is protected so a subclass can + // extend it, and a snapshot would leave the loop reading one list + // while this sweep judged against another. + Set<String> knownPropertyNames = new HashSet<>(Arrays.asList(ALL_PROPERTIES)); Enumeration<RefAddr> addresses = ref.getAll(); while (addresses.hasMoreElements()) { String addressName = addresses.nextElement().getType(); - if (!ALL_PROPERTY_NAMES.contains(addressName) + if (!knownPropertyNames.contains(addressName) && !RESERVED_JNDI_ADDRESS_NAMES.contains(addressName)) { log.warn(MessageFactory.get("dataSourceFactory.invalidProperty", addressName)); } @@ -471,12 +501,56 @@ public class DataSourceFactory implements ObjectFactory { return createDataSource(properties, nameCtx, XA, dataSourceObject); } + /** + * Whether content carried under the {@code dataSource} attribute is an + * object the pool can build its connections from. The pool accepts the + * three JDBC data source types its connect path understands + * ({@code javax.sql.DataSource}, {@code javax.sql.XADataSource} and + * {@code javax.sql.ConnectionPoolDataSource}, see + * {@link PooledConnection#connectUsingDataSource()}); only the first is a + * {@code javax.sql.DataSource}, so testing for that one type alone would + * stringify an XA or pooled data source and then reject the string. + * + * @param content the {@code RefAddr} content of a {@code dataSource} attribute + * + * @return {@code true} if the object can back the pool + */ + private static boolean isSupportedDataSourceObject(Object content) { + return content instanceof DataSource || content instanceof javax.sql.XADataSource + || content instanceof javax.sql.ConnectionPoolDataSource; + } + /** * Parse the given properties into a PoolConfiguration. * + * <p> + * A blank attribute value - empty or whitespace only, the shape a cleared + * template attribute yields - leaves the property at its + * {@link PoolProperties} default. A value the property cannot take fails the + * parse naming the attribute, rather than being silently dropped the way + * unparsed values used to be. Numeric, boolean and transaction-isolation + * attributes apply their trimmed value, and so does the lookup of the name + * given by {@code dataSourceJNDI}: whitespace around a number, a keyword or a + * class or JNDI name is always noise. Other string values are stored verbatim + * - a catalog, a validation query or a URL can legitimately carry + * surrounding whitespace. + * </p> + * + * <p> + * Two attributes are not plain property assignments: {@code connectionProperties} + * is resolved into the driver properties through + * {@link PoolProperties#setConnectionProperties(String)}, and + * {@code username}/{@code password} are merged into that same driver property + * set afterwards, so credentials configured as their own attributes override a + * {@code user} or {@code password} pair inside {@code connectionProperties}. + * </p> + * * @param properties The properties to parse * * @return The configured PoolConfiguration + * + * @throws IllegalArgumentException if an attribute carries a value its property + * cannot take */ public static PoolConfiguration parsePoolProperties(Properties properties) { PoolConfiguration poolProperties = new PoolProperties(); @@ -500,63 +574,95 @@ public class DataSourceFactory implements ObjectFactory { value = properties.getProperty(PROP_DEFAULTTRANSACTIONISOLATION); if (value != null) { - int level = UNKNOWN_TRANSACTIONISOLATION; - if ("NONE".equalsIgnoreCase(value)) { - level = Connection.TRANSACTION_NONE; - } else if ("READ_COMMITTED".equalsIgnoreCase(value)) { - level = Connection.TRANSACTION_READ_COMMITTED; - } else if ("READ_UNCOMMITTED".equalsIgnoreCase(value)) { - level = Connection.TRANSACTION_READ_UNCOMMITTED; - } else if ("REPEATABLE_READ".equalsIgnoreCase(value)) { - level = Connection.TRANSACTION_REPEATABLE_READ; - } else if ("SERIALIZABLE".equalsIgnoreCase(value)) { - level = Connection.TRANSACTION_SERIALIZABLE; - } else { - try { - level = Integer.parseInt(value); - } catch (NumberFormatException e) { - System.err.println("Could not parse defaultTransactionIsolation: " + value); - System.err.println("WARNING: defaultTransactionIsolation not set"); - System.err.println("using default value of database driver"); - level = UNKNOWN_TRANSACTIONISOLATION; + String trimmed = value.trim(); + if (!trimmed.isEmpty()) { + // Unset means the pool stays in the "use the driver default" + // state it comes with. + int level; + if ("NONE".equalsIgnoreCase(trimmed)) { + level = Connection.TRANSACTION_NONE; + } else if ("READ_COMMITTED".equalsIgnoreCase(trimmed)) { + level = Connection.TRANSACTION_READ_COMMITTED; + } else if ("READ_UNCOMMITTED".equalsIgnoreCase(trimmed)) { + level = Connection.TRANSACTION_READ_UNCOMMITTED; + } else if ("REPEATABLE_READ".equalsIgnoreCase(trimmed)) { + level = Connection.TRANSACTION_REPEATABLE_READ; + } else if ("SERIALIZABLE".equalsIgnoreCase(trimmed)) { + level = Connection.TRANSACTION_SERIALIZABLE; + } else { + try { + level = Integer.parseInt(trimmed); + } catch (NumberFormatException e) { + // An unusable value fails the resource naming the + // property, instead of silently dropping the + // configured isolation level. + throw new IllegalArgumentException(MessageFactory.get( + "dataSourceFactory.invalidTransactionIsolation", trimmed, + PROP_DEFAULTTRANSACTIONISOLATION), e); + } } + poolProperties.setDefaultTransactionIsolation(level); } - poolProperties.setDefaultTransactionIsolation(level); } value = properties.getProperty(PROP_DEFAULTCATALOG); - if (value != null) { + if (value != null && !value.trim().isEmpty()) { + // A stored empty value would be pushed into every physical + // connection as setCatalog("") and whether that is inert or fails + // is driver-dependent. The value itself is kept untrimmed - a + // catalog name is a database identifier, which may carry + // whitespace the way a JNDI name cannot. poolProperties.setDefaultCatalog(value); } value = properties.getProperty(PROP_DRIVERCLASSNAME); - if (value != null) { - poolProperties.setDriverClassName(value); + if (value != null && !value.trim().isEmpty()) { + // The connect path's DriverManager fallback is keyed on the + // property being null, so a stored empty class name would defeat + // the fallback and abort the pool loading the empty class. The + // trimmed value is applied: a Java class name can never carry + // meaningful surrounding whitespace. + poolProperties.setDriverClassName(value.trim()); } value = properties.getProperty(PROP_MAXACTIVE); if (value != null) { - poolProperties.setMaxActive(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_MAXACTIVE, value); + if (parsed != null) { + poolProperties.setMaxActive(parsed.intValue()); + } } value = properties.getProperty(PROP_MAXIDLE); if (value != null) { - poolProperties.setMaxIdle(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_MAXIDLE, value); + if (parsed != null) { + poolProperties.setMaxIdle(parsed.intValue()); + } } value = properties.getProperty(PROP_MINIDLE); if (value != null) { - poolProperties.setMinIdle(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_MINIDLE, value); + if (parsed != null) { + poolProperties.setMinIdle(parsed.intValue()); + } } value = properties.getProperty(PROP_INITIALSIZE); if (value != null) { - poolProperties.setInitialSize(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_INITIALSIZE, value); + if (parsed != null) { + poolProperties.setInitialSize(parsed.intValue()); + } } value = properties.getProperty(PROP_MAXWAIT); if (value != null) { - poolProperties.setMaxWait(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_MAXWAIT, value); + if (parsed != null) { + poolProperties.setMaxWait(parsed.intValue()); + } } value = properties.getProperty(PROP_TESTONBORROW); @@ -585,18 +691,27 @@ public class DataSourceFactory implements ObjectFactory { value = properties.getProperty(PROP_TIMEBETWEENEVICTIONRUNSMILLIS); if (value != null) { - poolProperties.setTimeBetweenEvictionRunsMillis(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_TIMEBETWEENEVICTIONRUNSMILLIS, value); + if (parsed != null) { + poolProperties.setTimeBetweenEvictionRunsMillis(parsed.intValue()); + } } value = properties.getProperty(PROP_NUMTESTSPEREVICTIONRUN); if (value != null) { log.warn(MessageFactory.get("dataSourceFactory.invalidSetting", PROP_NUMTESTSPEREVICTIONRUN)); - poolProperties.setNumTestsPerEvictionRun(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_NUMTESTSPEREVICTIONRUN, value); + if (parsed != null) { + poolProperties.setNumTestsPerEvictionRun(parsed.intValue()); + } } value = properties.getProperty(PROP_MINEVICTABLEIDLETIMEMILLIS); if (value != null) { - poolProperties.setMinEvictableIdleTimeMillis(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_MINEVICTABLEIDLETIMEMILLIS, value); + if (parsed != null) { + poolProperties.setMinEvictableIdleTimeMillis(parsed.intValue()); + } } value = properties.getProperty(PROP_TESTWHILEIDLE); @@ -623,13 +738,23 @@ public class DataSourceFactory implements ObjectFactory { } value = properties.getProperty(PROP_VALIDATIONQUERY); - if (value != null) { + if (value != null && !value.trim().isEmpty()) { + // The way to express "no validation query" is the property being + // unset, which routes validation to Connection.isValid(); a + // stored empty string instead runs the empty string as SQL on + // every validation - a no-op on forgiving drivers and a failed + // validation, session discard and reconnect on stricter ones. The + // query itself is kept untrimmed; whitespace around real SQL is + // the driver's to chew. poolProperties.setValidationQuery(value); } value = properties.getProperty(PROP_VALIDATIONQUERY_TIMEOUT); if (value != null) { - poolProperties.setValidationQueryTimeout(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_VALIDATIONQUERY_TIMEOUT, value); + if (parsed != null) { + poolProperties.setValidationQueryTimeout(parsed.intValue()); + } } value = properties.getProperty(PROP_VALIDATOR_CLASS_NAME); @@ -639,7 +764,10 @@ public class DataSourceFactory implements ObjectFactory { value = properties.getProperty(PROP_VALIDATIONINTERVAL); if (value != null) { - poolProperties.setValidationInterval(Long.parseLong(value)); + Long parsed = parseNumber(PROP_VALIDATIONINTERVAL, value); + if (parsed != null) { + poolProperties.setValidationInterval(parsed.longValue()); + } } value = properties.getProperty(PROP_ACCESSTOUNDERLYINGCONNECTIONALLOWED); @@ -661,7 +789,10 @@ public class DataSourceFactory implements ObjectFactory { value = properties.getProperty(PROP_REMOVEABANDONEDTIMEOUT); if (value != null) { - poolProperties.setRemoveAbandonedTimeout(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_REMOVEABANDONEDTIMEOUT, value); + if (parsed != null) { + poolProperties.setRemoveAbandonedTimeout(parsed.intValue()); + } } value = properties.getProperty(PROP_LOGABANDONED); @@ -684,10 +815,9 @@ public class DataSourceFactory implements ObjectFactory { value = properties.getProperty(PROP_CONNECTIONPROPERTIES); if (value != null) { - // Use the canonical setter so that the configured string also - // round-trips through getConnectionProperties(); it parses the - // pairs into getDbProperties() with the very same parser that - // getProperties(value) below used to apply. + // The canonical setter parses the pairs into the driver + // properties and keeps the string round-tripping through + // getConnectionProperties(). poolProperties.setConnectionProperties(value); } else { poolProperties.setDbProperties(new Properties()); @@ -706,7 +836,10 @@ public class DataSourceFactory implements ObjectFactory { } value = properties.getProperty(PROP_INTERCEPTORS); - if (value != null) { + if (value != null && !value.trim().isEmpty()) { + // Storing the empty string would leave the property non-null, and + // the pool treats that as a configured chain whose only segment is + // the empty one. poolProperties.setJdbcInterceptors(value); } @@ -735,46 +868,67 @@ public class DataSourceFactory implements ObjectFactory { } value = properties.getProperty(OBJECT_NAME); - if (value != null) { + if (value != null && !value.trim().isEmpty()) { + // An empty attribute would be quoted into an empty pool name, + // giving the JMX object name an empty name component where the + // property being unset lets the pool name itself. The configured + // name is stored verbatim - it is an identifier and can carry + // meaningful whitespace, which is also why it is quoted. poolProperties.setName(ObjectName.quote(value)); } value = properties.getProperty(PROP_ABANDONWHENPERCENTAGEFULL); if (value != null) { - poolProperties.setAbandonWhenPercentageFull(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_ABANDONWHENPERCENTAGEFULL, value); + if (parsed != null) { + poolProperties.setAbandonWhenPercentageFull(parsed.intValue()); + } } value = properties.getProperty(PROP_MAXAGE); if (value != null) { - poolProperties.setMaxAge(Long.parseLong(value)); + Long parsed = parseNumber(PROP_MAXAGE, value); + if (parsed != null) { + poolProperties.setMaxAge(parsed.longValue()); + } } - value = properties.getProperty(PROP_USE_CON_LOCK); + value = properties.getProperty(PROP_USE_LOCK); if (value != null) { - Boolean b = parseBoolean(PROP_USE_CON_LOCK, value); + Boolean b = parseBoolean(PROP_USE_LOCK, value); if (b != null) { poolProperties.setUseLock(b.booleanValue()); } } value = properties.getProperty(PROP_DATASOURCE); - if (value != null) { + if (value != null && !value.trim().isEmpty()) { // Only genuinely string-valued dataSource attributes reach this - // point: getObjectInstance hands an actual javax.sql.DataSource - // RefAddr content straight to the pool instead of stringifying it. - // A string cannot be turned into a data source here. + // point: getObjectInstance hands a live data source object + // carried under the attribute (DataSource, XADataSource or + // ConnectionPoolDataSource) straight to the pool instead of + // stringifying it. A string cannot be turned into a data source + // here. throw new IllegalArgumentException(MessageFactory.get("dataSourceFactory.cannotSetDataSourceString", value)); } value = properties.getProperty(PROP_DATASOURCE_JNDI); if (value != null) { - poolProperties.setDataSourceJNDI(value); + String trimmed = value.trim(); + if (!trimmed.isEmpty()) { + // A stored blank name would be one no context can resolve, + // failing the whole resource on the lookup. + poolProperties.setDataSourceJNDI(trimmed); + } } value = properties.getProperty(PROP_SUSPECT_TIMEOUT); if (value != null) { - poolProperties.setSuspectTimeout(Integer.parseInt(value)); + Integer parsed = parseInteger(PROP_SUSPECT_TIMEOUT, value); + if (parsed != null) { + poolProperties.setSuspectTimeout(parsed.intValue()); + } } value = properties.getProperty(PROP_ALTERNATE_USERNAME_ALLOWED); @@ -870,6 +1024,62 @@ public class DataSourceFactory implements ObjectFactory { throw new IllegalArgumentException(MessageFactory.get("dataSourceFactory.invalidBoolean", value, name)); } + /** + * Parses the value configured for a numeric attribute. Like + * {@link #parseBoolean(String, String)} the value tolerates surrounding + * whitespace, an empty value means "leave the property unset" rather than + * failing the parse, and a value that cannot be parsed fails the + * configuration naming the property and quoting the value instead of + * surfacing as a bare {@link NumberFormatException} about an anonymous + * input string. + * + * @param name the property name, for the error message + * @param value the configured value, never {@code null} + * + * @return the parsed value, or {@code null} when the value is empty and + * the property is therefore to be left unset + * + * @throws IllegalArgumentException if the value is not a whole number + */ + private static Long parseNumber(String name, String value) { + String trimmed = value.trim(); + if (trimmed.isEmpty()) { + return null; + } + try { + return Long.valueOf(trimmed); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + MessageFactory.get("dataSourceFactory.invalidNumber", trimmed, name), e); + } + } + + /** + * Parses the value configured for a numeric attribute whose setter takes + * an {@code int}. + * + * @param name the property name, for the error message + * @param value the configured value, never {@code null} + * + * @return the parsed value, or {@code null} when the value is empty and + * the property is therefore to be left unset + * + * @throws IllegalArgumentException if the value is not a whole number, or + * is outside the range an {@code int} + * can hold + */ + private static Integer parseInteger(String name, String value) { + Long parsed = parseNumber(name, value); + if (parsed == null) { + return null; + } + if (parsed.longValue() < Integer.MIN_VALUE || parsed.longValue() > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + MessageFactory.get("dataSourceFactory.numberOutOfRange", value, name)); + } + return Integer.valueOf(parsed.intValue()); + } + /** * Creates and configures a {@link DataSource} instance based on the given properties. * @@ -904,20 +1114,22 @@ public class DataSourceFactory implements ObjectFactory { * <p> * When {@code dataSourceObject} is not {@code null}, it is set on the pool configuration before the pool is * created, and the {@code dataSourceJNDI} lookup is skipped: the pool is built around the object that was handed - * to the factory directly. + * to the factory directly. The object is one of the data source types the pool's connect path accepts + * ({@code javax.sql.DataSource}, {@code javax.sql.XADataSource} or + * {@code javax.sql.ConnectionPoolDataSource}), not necessarily a {@code javax.sql.DataSource}. * </p> * * @param properties The datasource configuration properties * @param context The JNDI context * @param XA {@code true} to create an XA data source - * @param dataSourceObject A live {@link DataSource} the pool should wrap, or {@code null} + * @param dataSourceObject A live data source object the pool should wrap, or {@code null} * * @return The configured datasource * * @throws Exception if an error occurs creating the data source */ protected DataSource createDataSource(Properties properties, Context context, boolean XA, - DataSource dataSourceObject) throws Exception { + Object dataSourceObject) throws Exception { PoolConfiguration poolProperties = parsePoolProperties(properties); if (dataSourceObject != null) { poolProperties.setDataSource(dataSourceObject); @@ -927,9 +1139,7 @@ public class DataSourceFactory implements ObjectFactory { } org.apache.tomcat.jdbc.pool.DataSource dataSource = XA ? new XADataSource(poolProperties) : new org.apache.tomcat.jdbc.pool.DataSource(poolProperties); - // Initialise the pool itself dataSource.createPool(); - // Return the configured DataSource instance return dataSource; } @@ -939,8 +1149,9 @@ public class DataSourceFactory implements ObjectFactory { * @param context The JNDI context * @param poolProperties The pool configuration * - * @throws NamingException if the name configured through the dataSourceJNDI property cannot be resolved. The name - * was configured specifically to supply the connections for the pool, so a pool without + * @throws NamingException if the name configured through the dataSourceJNDI property cannot be resolved, or + * resolves to an object the pool cannot create connections from. The name was + * configured specifically to supply the connections for the pool, so a pool without * that data source is not a usable fallback and the failure is reported instead of * being deferred to the first connection request. */ @@ -950,13 +1161,15 @@ public class DataSourceFactory implements ObjectFactory { if (context != null) { jndiDS = context.lookup(poolProperties.getDataSourceJNDI()); } else { - log.warn(MessageFactory.get("dataSourceFactory.jndiContextNull")); + // Not a fault: a bare Reference lookup outside a container + // arrives with no local context, and the InitialContext + // fallback below is written for exactly that. + log.debug(MessageFactory.get("dataSourceFactory.jndiContextNull")); } } catch (NamingException e) { if (log.isDebugEnabled()) { - log.debug( - "The name \"" + poolProperties.getDataSourceJNDI() + "\" cannot be found in the local context.", - e); + log.debug(MessageFactory.get("dataSourceFactory.jndiLocalNameNotFound", + poolProperties.getDataSourceJNDI()), e); } } NamingException initialContextFailure = null; @@ -965,12 +1178,37 @@ public class DataSourceFactory implements ObjectFactory { context = new InitialContext(); jndiDS = context.lookup(poolProperties.getDataSourceJNDI()); } catch (NamingException e) { + // Not warned here: this failure always ends in the thrown + // jndiLookupFailed below, which names the same name and + // carries this exception as its cause, and the caller logs + // that - warning as well only made the one failed resource + // read as two. initialContextFailure = e; - log.warn(MessageFactory.get("dataSourceFactory.jndiNameNotFound", - poolProperties.getDataSourceJNDI()), e); } } if (jndiDS != null) { + if (jndiDS instanceof DataSourceProxy) { + // setDataSource's own rejection of layered pools, reported as + // this method's contract promises and with the context the + // rejection alone lacks: the attribute and the name that + // resolved to the other pool. + throw new NamingException(MessageFactory.get( + "dataSourceFactory.jndiLayeredPool", + poolProperties.getDataSourceJNDI(), + jndiDS.getClass().getName())); + } + if (!isSupportedDataSourceObject(jndiDS)) { + // The same requirement the object-valued dataSource attribute + // enforces: the name was configured to supply the pool's + // connections, and a binding the connect path cannot use must + // fail here naming the attribute and the name, rather than + // surfacing at connect time as a generic error about an + // unknown class. + throw new NamingException(MessageFactory.get( + "dataSourceFactory.jndiNotDataSource", + poolProperties.getDataSourceJNDI(), + jndiDS.getClass().getName())); + } poolProperties.setDataSource(jndiDS); return; } @@ -986,6 +1224,14 @@ public class DataSourceFactory implements ObjectFactory { /** * Parse properties from the string. Format of the string must be [propertyName=property;]*. * + * <p> + * Kept as a protected extension point for subclasses that parse property + * strings of their own; this factory no longer calls it, as its + * {@code connectionProperties} attribute goes through the canonical + * {@link PoolProperties#setConnectionProperties(String)}, which parses with the + * same delegate used here. + * </p> + * * @param propText The properties string * * @return the properties diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java index c1ef63bc69..5340b6ce1c 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java @@ -20,9 +20,11 @@ import java.io.IOException; import java.io.Serializable; import java.io.StringReader; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -454,18 +456,38 @@ public class PoolProperties implements PoolConfiguration, Cloneable, Serializabl interceptors = new InterceptorDefinition[0]; } else { String[] interceptorValues = interceptorList.split(";"); - InterceptorDefinition[] definitions = new InterceptorDefinition[interceptorValues.length + 1]; + List<InterceptorDefinition> definitions = + new ArrayList<>(interceptorValues.length + 1); // Always add the trap interceptor to the mix - definitions[0] = new InterceptorDefinition(TrapException.class); + definitions.add(new InterceptorDefinition(TrapException.class)); for (int i = 0; i < interceptorValues.length; i++) { int propIndex = interceptorValues[i].indexOf('('); int endIndex = interceptorValues[i].indexOf(')'); + String name; + String propsAsString = null; if (propIndex < 0 || endIndex < 0 || endIndex <= propIndex) { - definitions[i + 1] = new InterceptorDefinition(interceptorValues[i].trim()); + name = interceptorValues[i].trim(); } else { - String name = interceptorValues[i].substring(0, propIndex).trim(); - definitions[i + 1] = new InterceptorDefinition(name); - String propsAsString = interceptorValues[i].substring(propIndex + 1, endIndex); + name = interceptorValues[i].substring(0, propIndex).trim(); + propsAsString = interceptorValues[i].substring(propIndex + 1, endIndex); + } + if (name.isEmpty()) { + // The name is empty when a leading or doubled + // separator leaves an empty segment, or when a + // segment opens straight on "(": there is no + // interceptor to load, and a definition without a + // class name fails the whole pool start later, on + // the generated "...pool.interceptor." class, with a + // message naming nothing. split() already drops a + // redundant trailing separator and the property parse + // below tolerates a redundant separator inside the + // parentheses: skipping the empty name makes a + // redundant separator cosmetic throughout the syntax + // rather than fatal wherever split() kept it. + continue; + } + InterceptorDefinition definition = new InterceptorDefinition(name); + if (propsAsString != null) { String[] props = propsAsString.split(","); for (int j = 0; j < props.length; j++) { String prop = props[j]; @@ -484,11 +506,12 @@ public class PoolProperties implements PoolConfiguration, Cloneable, Serializabl } String propName = prop.substring(0, pidx).trim(); String propValue = prop.substring(pidx + 1).trim(); - definitions[i + 1].addProperty(new InterceptorProperty(propName, propValue)); + definition.addProperty(new InterceptorProperty(propName, propValue)); } } + definitions.add(definition); } - interceptors = definitions; + interceptors = definitions.toArray(new InterceptorDefinition[0]); } } return interceptors; @@ -509,7 +532,10 @@ public class PoolProperties implements PoolConfiguration, Cloneable, Serializabl * properties to the driver instead of leaving them merged in. Entries the * string never contributed, such as the {@code user} and {@code password} * injected by {@link DataSourceFactory#parsePoolProperties(Properties)} or - * mirrored in by {@link DataSourceProxy}, are kept. A configuration whose + * mirrored in by {@link DataSourceProxy}, are kept, and while a + * {@code username}/{@code password} field is set its entry holds the + * field's value even when this string names that key, the rule the + * credential setters establish. A configuration whose * database properties are {@code null} only stores the raw string, as there * is no property collection to parse into. The remove and re-add run inside * this one synchronized method; an unsynchronised reader of the database @@ -544,6 +570,19 @@ public class PoolProperties implements PoolConfiguration, Cloneable, Serializabl dbProps.setProperty(key, parsed.getProperty(key)); keys.add(key); } + // The credential setters' rule outlives the string: while a credential + // field is set, its dbProperties entry holds the field's value, even + // when this string named the key itself - DataSourceFactory injects the + // configured username/password over the string's pairs, so a + // reconfigure that drops the keys from the string must not strip + // credentials the configuration still carries. With the field unset the + // entry is the string's own again, exactly as after setPassword(null). + if (username != null) { + dbProps.setProperty(PoolUtilities.PROP_USER, username); + } + if (password != null) { + dbProps.setProperty(PoolUtilities.PROP_PASSWORD, password); + } connectionPropertyKeys = keys; } diff --git a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties index 8e937701fc..591d47308b 100644 --- a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties +++ b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/util/LocalStrings.properties @@ -86,14 +86,21 @@ dataSource.unableToCreateObjectName=Unable to create object name for JDBC pool. dataSource.unableToRegisterMBean=Unable to register JDBC pool MBean. dataSource.unableToUnregisterMBean=Unable to unregister JDBC pool MBean [{0}]. The registration handle was already given up, so the close or callback that asked for this release continues. -dataSourceFactory.cannotSetDataSourceString=Can''t set dataSource property as a string "{0}". Either carry the javax.sql.DataSource object itself under the dataSource attribute of the Reference, or use the dataSourceJNDI attribute to name a data source bound in JNDI. +dataSourceFactory.cannotSetDataSourceLayered=Can''t set dataSource property to another connection pool ({0}): a pool cannot wrap another pool. Carry the JDBC driver''s data source object under the dataSource attribute, or use the dataSourceJNDI attribute to name one bound in JNDI. +dataSourceFactory.cannotSetDataSourceString=Can''t set dataSource property as a string "{0}". Either carry the data source object itself under the dataSource attribute of the Reference - a javax.sql.DataSource, javax.sql.XADataSource or javax.sql.ConnectionPoolDataSource - or use the dataSourceJNDI attribute to name a data source bound in JNDI. dataSourceFactory.invalidBoolean=Invalid boolean value ''{0}'' for property {1} dataSourceFactory.invalidClassName={0} is not a valid class name/type for this JNDI factory. +dataSourceFactory.invalidNumber=Invalid numeric value ''{0}'' for property {1}: expected a whole number dataSourceFactory.invalidProperty={0} is not a valid property, it will be ignored. dataSourceFactory.invalidSetting={0} is not a valid setting, it will have no effect. +dataSourceFactory.invalidTransactionIsolation=Invalid value ''{0}'' for property {1}: expected NONE, READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE or the numeric transaction isolation level of java.sql.Connection dataSourceFactory.jndiContextNull=dataSourceJNDI property is configured, but local JNDI context is null. +dataSourceFactory.jndiLayeredPool=The name [{0}] resolved through the ''dataSourceJNDI'' attribute to another connection pool ({1}): a pool cannot wrap another pool. Point the name at a data source provided by the JDBC driver. +dataSourceFactory.jndiLocalNameNotFound=The name "{0}" cannot be found in the local context. dataSourceFactory.jndiLookupFailed=Unable to create the connection pool: the name [{0}] configured through the ''dataSourceJNDI'' attribute was not found in the local JNDI context nor in the InitialContext. The pool is built around the data source that name refers to; check that the name is correct and that the data source is bound and visible to the application before the pool is created. -dataSourceFactory.jndiNameNotFound=The name "{0}" cannot be found in the InitialContext. +dataSourceFactory.jndiNotDataSource=The name [{0}] resolved through the ''dataSourceJNDI'' attribute to an object of class {1}, which the pool cannot create connections from. The name must be bound to a javax.sql.DataSource, a javax.sql.XADataSource or a javax.sql.ConnectionPoolDataSource. +dataSourceFactory.nullContentProperty=The {0} attribute of the JNDI reference carries no content; the property is left unset. +dataSourceFactory.numberOutOfRange=Invalid numeric value ''{0}'' for property {1}: the value is outside the range the property accepts dataSourceProxy.alternateUsernameIgnored=alternateUsernameAllowed is set to false so the provided user name [{0}] and associated password have been ignored dataSourceProxy.connectionNotXA=Connection from pool does not implement javax.sql.XAConnection diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryJndiLookup.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryJndiLookup.java index 8a2698ff8e..1e00065b55 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryJndiLookup.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryJndiLookup.java @@ -177,6 +177,103 @@ class TestDataSourceFactoryJndiLookup extends JdbcPoolBaseTest { "the pool must establish connections through the looked up data source"); } + @Test + void dataSourceJNDIBoundToSomethingElseFailsTheFactoryNamingTheClass() { + // The sibling dataSource attribute validates its object against the + // types the connect path understands; the lookup route must hold the + // bound object to the same requirement. Pre-fix any binding reached + // the pool and only surfaced at connect time as a generic error about + // an unknown class, if the initial size deferred the first connect at + // all. + Reference ref = backingDataSourceReference("0"); + Context context = new ContextStub() { + @Override + public Object lookup(String name) { + return MISSING_JNDI_NAME.equals(name) ? "not-a-data-source" : null; + } + + @Override + public Object lookup(Name name) { + return lookup(name.toString()); + } + }; + + NamingException e = Assertions.assertThrows(NamingException.class, + () -> new DataSourceFactory().getObjectInstance(ref, + new CompositeName("jdbc/TestDS"), context, null), + "a name bound to a class the pool cannot connect from must fail the factory call"); + Assertions.assertTrue(e.getMessage().contains(MISSING_JNDI_NAME), + "the failure must name the misbound name, was: " + e.getMessage()); + Assertions.assertTrue(e.getMessage().contains("dataSourceJNDI"), + "the failure must name the misconfigured attribute, was: " + + e.getMessage()); + Assertions.assertTrue(e.getMessage().contains(String.class.getName()), + "the failure must name the class the name resolved to, was: " + + e.getMessage()); + } + + @Test + void dataSourceJNDIBoundToAnotherPoolFailsWithANamingException() { + // A binding of another tomcat-jdbc pool is rejected by setDataSource + // as a layered pool - but performJNDILookup promises NamingException, + // and the bare IllegalArgumentException carried neither the attribute + // nor the name that resolved to the other pool. + Reference ref = backingDataSourceReference("0"); + org.apache.tomcat.jdbc.pool.DataSource otherPool = + new org.apache.tomcat.jdbc.pool.DataSource(); + Context context = new ContextStub() { + @Override + public Object lookup(String name) { + return MISSING_JNDI_NAME.equals(name) ? otherPool : null; + } + + @Override + public Object lookup(Name name) { + return lookup(name.toString()); + } + }; + + NamingException e = Assertions.assertThrows(NamingException.class, + () -> new DataSourceFactory().getObjectInstance(ref, + new CompositeName("jdbc/TestDS"), context, null), + "a layered-pool binding must fail with the method's declared" + + " exception type, not an unchecked rejection"); + Assertions.assertTrue(e.getMessage().contains(MISSING_JNDI_NAME), + "the failure must name the misbound name, was: " + e.getMessage()); + Assertions.assertTrue(e.getMessage().contains("dataSourceJNDI"), + "the failure must name the misconfigured attribute, was: " + + e.getMessage()); + } + + @Test + void blankDataSourceJNDIAttributeLeavesThePropertyUnset() throws Exception { + // A blank value means "not configured", as it does for the boolean, + // numeric and isolation attributes of the same factory. Pre-fix the + // blank was stored and looked up as a name, and the resource died on + // a NamingException about the name [] even though the pool had a + // perfectly usable driver/URL configuration. + Reference ref = new Reference("javax.sql.DataSource", null, null); + ref.add(new StringRefAddr("jmxEnabled", "false")); + ref.add(new StringRefAddr("initialSize", "1")); + ref.add(new StringRefAddr("minIdle", "0")); + ref.add(new StringRefAddr("driverClassName", "org.h2.Driver")); + ref.add(new StringRefAddr("url", getUrl())); + ref.add(new StringRefAddr("username", "sa")); + ref.add(new StringRefAddr("password", "")); + ref.add(new StringRefAddr("dataSourceJNDI", " ")); + + DataSource ds = manage((DataSource) new DataSourceFactory() + .getObjectInstance(ref, new CompositeName("jdbc/TestDS"), null, null)); + + Assertions.assertNull(ds.getPoolProperties().getDataSourceJNDI(), + "a blank dataSourceJNDI must leave the property unset"); + try (java.sql.Connection c = ds.getConnection(); + java.sql.ResultSet rs = c.createStatement().executeQuery("SELECT 1")) { + Assertions.assertTrue(rs.next(), + "the pool must still work from its driver/URL configuration"); + } + } + /** * A JNDI context in which nothing is bound: every operation except * {@code lookup} is unsupported, and no {@code lookup} can succeed. diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryNoOpPropertyWarnings.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryNoOpPropertyWarnings.java index c7a027baec..e9800dfb37 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryNoOpPropertyWarnings.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryNoOpPropertyWarnings.java @@ -131,17 +131,16 @@ class TestDataSourceFactoryNoOpPropertyWarnings { /** * A factory that stops where the real one parses the properties, so the * full JNDI route can be exercised without creating a pool behind it. The - * {@code dataSourceObject} parameter type is deliberately the fully - * qualified {@code javax.sql.DataSource} of the overridden signature: an - * override that narrows it to the pool's own class would be an overload - * the factory never calls. + * {@code dataSourceObject} parameter type is deliberately the widened + * {@code Object} of the overridden signature: an override that narrows it + * would be an overload the factory never calls. */ private static final class ParsingFactory extends DataSourceFactory { private PoolConfiguration parsed; @Override protected javax.sql.DataSource createDataSource(Properties properties, Context context, boolean xa, - javax.sql.DataSource dataSourceObject) { + Object dataSourceObject) { // exactly what the real implementation does first parsed = DataSourceFactory.parsePoolProperties(properties); return null; @@ -208,12 +207,18 @@ class TestDataSourceFactoryNoOpPropertyWarnings { properties.setProperty("numTestsPerEvictionRun", "not-a-number"); try (FactoryLogCapture logs = new FactoryLogCapture()) { - Assertions.assertThrows(NumberFormatException.class, + IllegalArgumentException e = Assertions.assertThrows( + IllegalArgumentException.class, () -> DataSourceFactory.parsePoolProperties(properties), "a value that cannot be parsed must still fail the parse, as it did before"); // the warning reports the property being configured, not the // success of parsing it, so it must precede the parse failure logs.assertWarnedNoEffect("numTestsPerEvictionRun"); + Assertions.assertInstanceOf(NumberFormatException.class, e.getCause(), + "the parse failure must be kept as the cause"); + Assertions.assertTrue(e.getMessage().contains("numTestsPerEvictionRun"), + "the failure must name the misconfigured attribute, was: " + + e.getMessage()); } } diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryRefAddrContent.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryRefAddrContent.java index c02eac4890..bef1e185da 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryRefAddrContent.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryRefAddrContent.java @@ -42,8 +42,9 @@ import org.apache.tomcat.jdbc.pool.PoolProperties; * contract makes {@code getContent()} nullable and {@code Object}- * valued, so the factory loop must survive a null content instead of * stringifying blindly, and a {@code dataSource} attribute carrying an - * actual {@code javax.sql.DataSource} object must reach the pool as - * that object rather than as its {@code toString()}. + * actual backing object must reach the pool as that object rather than + * as its {@code toString()} - for every backing type the pool's own + * connect path accepts, not only {@code javax.sql.DataSource}. */ class TestDataSourceFactoryRefAddrContent extends JdbcPoolBaseTest { @@ -119,6 +120,65 @@ class TestDataSourceFactoryRefAddrContent extends JdbcPoolBaseTest { "the pool must establish connections through that data source"); } + @Test + void objectValuedXaOnlyDataSourceRefAddrBacksThePool() throws Exception { + // A driver's XA data source, like Oracle's, implements + // javax.sql.XADataSource without being a javax.sql.DataSource. The + // pool's connect path supports exactly that backing, but the + // instanceof test in the factory loop recognised only + // javax.sql.DataSource: the live object was stringified and the + // resource then died on the resulting string with a message telling + // the operator to do what the Reference already did. + Reference ref = new Reference("javax.sql.XADataSource", null, null); + ref.add(new StringRefAddr("jmxEnabled", "false")); + ref.add(new StringRefAddr("initialSize", "1")); + ref.add(new StringRefAddr("minIdle", "0")); + org.h2.jdbcx.JdbcDataSource delegate = new org.h2.jdbcx.JdbcDataSource(); + delegate.setUrl(getUrl()); + delegate.setUser("sa"); + delegate.setPassword(""); + Object xaOnly = singleInterfaceDataSource(delegate, javax.sql.XADataSource.class); + ref.add(new ObjectContentRefAddr("dataSource", xaOnly)); + + Object result = new DataSourceFactory().getObjectInstance(ref, + new CompositeName("jdbc/TestDS"), null, null); + Assertions.assertInstanceOf(org.apache.tomcat.jdbc.pool.XADataSource.class, result, + "a javax.sql.XADataSource reference must yield the pool's XA data source"); + org.apache.tomcat.jdbc.pool.XADataSource ds = + manage((org.apache.tomcat.jdbc.pool.XADataSource) result); + + Assertions.assertSame(xaOnly, ds.getPoolProperties().getDataSource(), + "the object carried by the dataSource RefAddr must back the pool"); + assertPoolConnects(ds); + Assertions.assertEquals(1, poolSessions(), + "the pool must establish connections through that XA data source"); + } + + @Test + void objectValuedConnectionPoolDataSourceRefAddrBacksThePool() throws Exception { + // Same as above for the pooled-connections backing: a driver + // data source that is a javax.sql.ConnectionPoolDataSource but not + // a javax.sql.DataSource (PostgreSQL's CPDS sources, for instance) + // must reach the pool as an object, not as its toString(). + Reference ref = poolReference(); + org.h2.jdbcx.JdbcDataSource delegate = new org.h2.jdbcx.JdbcDataSource(); + delegate.setUrl(getUrl()); + delegate.setUser("sa"); + delegate.setPassword(""); + Object cpdsOnly = singleInterfaceDataSource(delegate, + javax.sql.ConnectionPoolDataSource.class); + ref.add(new ObjectContentRefAddr("dataSource", cpdsOnly)); + + DataSource ds = managedPool(new DataSourceFactory().getObjectInstance( + ref, new CompositeName("jdbc/TestDS"), null, null)); + + Assertions.assertSame(cpdsOnly, ds.getPoolProperties().getDataSource(), + "the object carried by the dataSource RefAddr must back the pool"); + assertPoolConnects(ds); + Assertions.assertEquals(1, poolSessions(), + "the pool must establish connections through that pooled-connections data source"); + } + @Test void objectValuedDataSourceRefAddrTakesPrecedenceOverDataSourceJNDI() throws Exception { @@ -147,6 +207,32 @@ class TestDataSourceFactoryRefAddrContent extends JdbcPoolBaseTest { assertPoolConnects(ds); } + @Test + void objectValuedDataSourceRefAddrCarryingAnotherPoolIsRejectedWithTheAttributeContext() { + // A pool cannot wrap another pool. Pre-fix the object passed the + // factory's own acceptance check and was rejected deeper down by + // PoolProperties with a bare "Layered pools are not allowed." - + // neither the attribute at fault nor the object's class was named, + // unlike every other failure this factory reports (and unlike the + // dataSourceJNDI route's diagnosis of the same binding). + Reference ref = poolReference(); + org.apache.tomcat.jdbc.pool.DataSource otherPool = + new org.apache.tomcat.jdbc.pool.DataSource(); + ref.add(new ObjectContentRefAddr("dataSource", otherPool)); + + IllegalArgumentException e = Assertions.assertThrows( + IllegalArgumentException.class, + () -> new DataSourceFactory().getObjectInstance(ref, + new CompositeName("jdbc/TestDS"), null, null)); + Assertions.assertTrue(e.getMessage().contains("dataSource"), + "the rejection must name the misconfigured attribute, was: " + + e.getMessage()); + Assertions.assertTrue( + e.getMessage().contains(otherPool.getClass().getName()), + "the rejection must name the class the attribute carried, was: " + + e.getMessage()); + } + @Test void stringValuedDataSourceRefAddrIsRejectedWithAnActionableMessage() { // A genuinely string-valued dataSource attribute still cannot become @@ -271,6 +357,35 @@ class TestDataSourceFactoryRefAddrContent extends JdbcPoolBaseTest { } } + /* + * A JDBC data source exposed through a single interface, as the drivers + * that give their XA or pooled data source only that one type do. Every + * interface call is delegated to the given real data source, which must + * implement that interface. + */ + private static Object singleInterfaceDataSource(final Object delegate, + Class<?> exposedInterface) { + return Proxy.newProxyInstance( + TestDataSourceFactoryRefAddrContent.class.getClassLoader(), + new Class<?>[] { exposedInterface }, + (proxy, method, args) -> { + switch (method.getName()) { + case "toString": + return "SingleInterfaceDataSource"; + case "hashCode": + return System.identityHashCode(proxy); + case "equals": + return proxy == args[0]; + default: + try { + return method.invoke(delegate, args); + } catch (java.lang.reflect.InvocationTargetException e) { + throw e.getCause(); + } + } + }); + } + /* * A JNDI context whose lookup returns the given object for any name, * standing in for a bound data source. diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryUnknownPropertyWarnings.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryUnknownPropertyWarnings.java index 273aedb0ad..7435203793 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryUnknownPropertyWarnings.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestDataSourceFactoryUnknownPropertyWarnings.java @@ -165,7 +165,7 @@ class TestDataSourceFactoryUnknownPropertyWarnings extends JdbcPoolBaseTest { @Override protected javax.sql.DataSource createDataSource(Properties properties, Context context, boolean xa, - javax.sql.DataSource dataSourceObject) { + Object dataSourceObject) { // exactly what the real implementation does first parsed = DataSourceFactory.parsePoolProperties(properties); return null; diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestNamingFactory.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestNamingFactory.java index 57abf46aee..47ee390bdb 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestNamingFactory.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/naming/TestNamingFactory.java @@ -36,6 +36,7 @@ import org.apache.tomcat.jdbc.JdbcPoolBaseTest; import org.apache.tomcat.jdbc.pool.DataSource; import org.apache.tomcat.jdbc.pool.DataSourceFactory; import org.apache.tomcat.jdbc.pool.PoolConfiguration; +import org.apache.tomcat.jdbc.pool.PoolProperties; import org.apache.tomcat.jdbc.pool.XADataSource; /** @@ -107,6 +108,23 @@ public class TestNamingFactory extends JdbcPoolBaseTest { } } + @Test + void concreteXaPoolClassNameReferenceProducesXaDataSourceType() throws Exception { + // A resource whose type names the factory's own XA class - the class + // the factory news for every XA reference - must build that XA pool, + // where previously only the sibling non-XA concrete class was + // accepted and this one came back as an unresolvable resource. + Name name = new CompositeName("jdbc/ConcreteXaDS"); + Object result = new DataSourceFactory().getObjectInstance( + dataSourceReference("org.apache.tomcat.jdbc.pool.XADataSource"), + name, null, null); + Assertions.assertInstanceOf(XADataSource.class, result, + "the concrete XA class name must yield the pool's XA type"); + if (result instanceof DataSource) { + manage((DataSource) result); + } + } + /* * Boolean attributes are applied strictly: only "true" and "false", * ignoring case and surrounding whitespace, configure the property; @@ -228,6 +246,299 @@ public class TestNamingFactory extends JdbcPoolBaseTest { "the exact lowercase spelling must keep working"); } + /* + * The defaultTransactionIsolation attribute follows the same strict + * policy: names and the numeric java.sql.Connection constants are + * accepted ignoring surrounding whitespace, an empty attribute means + * "unset" - the "leave the driver default alone" state - and anything + * else fails the parse. Pre-fix an unusable value was silently applied + * as that unset sentinel, so connections ran at the driver's isolation + * level while the only trace was a System.err line claiming the + * attribute had not been set at all. + */ + + @Test + void isolationLevelNamesApplyIgnoringCaseAndSurroundingWhitespace() { + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("defaultTransactionIsolation", " read_committed "); + Assertions.assertEquals(java.sql.Connection.TRANSACTION_READ_COMMITTED, + DataSourceFactory.parsePoolProperties(attrs) + .getDefaultTransactionIsolation(), + "\" read_committed \" must be accepted, whitespace included"); + + attrs.setProperty("defaultTransactionIsolation", "NONE"); + Assertions.assertEquals(java.sql.Connection.TRANSACTION_NONE, + DataSourceFactory.parsePoolProperties(attrs) + .getDefaultTransactionIsolation(), + "the documented name must still apply"); + } + + @Test + void numericIsolationLevelAppliesIgnoringSurroundingWhitespace() { + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("defaultTransactionIsolation", " 2 "); + Assertions.assertEquals(2, + DataSourceFactory.parsePoolProperties(attrs) + .getDefaultTransactionIsolation(), + "the numeric form must be accepted, whitespace included"); + } + + @Test + void unusableIsolationLevelFailsTheParseNamingTheProperty() { + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("defaultTransactionIsolation", "READ_COMMIT"); + + IllegalArgumentException e = Assertions.assertThrows( + IllegalArgumentException.class, + () -> DataSourceFactory.parsePoolProperties(attrs), + "an unusable isolation level must fail the parse instead of " + + "silently leaving the level at the driver default"); + Assertions.assertTrue(e.getMessage().contains("defaultTransactionIsolation"), + "the failure must name the misconfigured attribute, was: " + + e.getMessage()); + Assertions.assertTrue(e.getMessage().contains("READ_COMMIT"), + "the failure must show the offending value, was: " + + e.getMessage()); + Assertions.assertNotNull(e.getCause(), + "the parse failure must be kept as the cause"); + } + + @Test + void emptyIsolationLevelAttributeLeavesThePropertyUnset() { + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("defaultTransactionIsolation", ""); + + PoolConfiguration configured = + DataSourceFactory.parsePoolProperties(attrs); + + Assertions.assertEquals(DataSourceFactory.UNKNOWN_TRANSACTIONISOLATION, + configured.getDefaultTransactionIsolation(), + "an empty attribute must keep the \"leave the driver default " + + "alone\" state"); + } + + /* + * The numeric attributes follow the same strict policy as the booleans: + * surrounding whitespace is ignored, an empty attribute means "unset" + * (the PoolProperties default) rather than a NumberFormatException for + * the empty string, and a value that cannot be parsed - or does not fit + * the property - fails the configuration naming the property, instead of + * aborting the whole JNDI resource with an exception that only quotes + * the offending text. + */ + + @Test + void numericAttributesApplyIgnoringSurroundingWhitespace() { + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("maxActive", " 12 "); + attrs.setProperty("maxAge", " 86400000000 "); + + PoolConfiguration configured = + DataSourceFactory.parsePoolProperties(attrs); + + Assertions.assertEquals(12, configured.getMaxActive(), + "\" 12 \" must be accepted, whitespace included"); + Assertions.assertEquals(86400000000L, configured.getMaxAge(), + "a long attribute must be accepted, whitespace included"); + } + + @Test + void emptyNumericAttributesLeaveThePropertiesUnset() { + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("maxActive", ""); + attrs.setProperty("maxWait", " "); + + PoolConfiguration configured = + DataSourceFactory.parsePoolProperties(attrs); + + Assertions.assertEquals(PoolProperties.DEFAULT_MAX_ACTIVE, + configured.getMaxActive(), + "an empty numeric attribute must leave the default in place"); + Assertions.assertEquals(new PoolProperties().getMaxWait(), + configured.getMaxWait(), + "an all-whitespace numeric attribute must leave the default in place"); + } + + @Test + void unparseableNumericValueFailsTheParseNamingTheProperty() { + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("maxWait", "12 seconds"); + + IllegalArgumentException e = Assertions.assertThrows( + IllegalArgumentException.class, + () -> DataSourceFactory.parsePoolProperties(attrs)); + Assertions.assertTrue(e.getMessage().contains("maxWait"), + "the failure must name the misconfigured attribute, was: " + + e.getMessage()); + Assertions.assertTrue(e.getMessage().contains("12 seconds"), + "the failure must show the offending value, was: " + + e.getMessage()); + Assertions.assertInstanceOf(NumberFormatException.class, e.getCause(), + "the parse failure must be kept as the cause"); + } + + @Test + void numericValueBeyondThePropertyRangeFailsTheParseNamingTheProperty() { + java.util.Properties attrs = new java.util.Properties(); + // a millisecond value no int setter can hold (~2.7 years) + attrs.setProperty("minEvictableIdleTimeMillis", "86400000000"); + + IllegalArgumentException e = Assertions.assertThrows( + IllegalArgumentException.class, + () -> DataSourceFactory.parsePoolProperties(attrs)); + Assertions.assertTrue(e.getMessage().contains("minEvictableIdleTimeMillis"), + "the failure must name the misconfigured attribute, was: " + + e.getMessage()); + Assertions.assertTrue(e.getMessage().contains("86400000000"), + "the failure must show the offending value, was: " + + e.getMessage()); + } + + @Test + void blankStringAttributesLeaveThePropertiesUnset() { + // The same cleared-template policy the booleans, numerics, the + // isolation level and dataSourceJNDI follow. Pre-fix a stored empty + // defaultCatalog reached connection.setCatalog("") on every physical + // connection - driver-dependent from inert to a per-connect failure - + // and a blank dataSource aborted the resource with guidance about + // carrying the object, irrelevant to a value cleared to mean "off". + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("defaultCatalog", ""); + attrs.setProperty("dataSource", " "); + attrs.setProperty("driverClassName", ""); + attrs.setProperty("jdbcInterceptors", " "); + attrs.setProperty("validationQuery", ""); + + PoolConfiguration configured = + DataSourceFactory.parsePoolProperties(attrs); + + Assertions.assertNull(configured.getDefaultCatalog(), + "an empty defaultCatalog must leave the property unset"); + Assertions.assertNull(configured.getDataSource(), + "a blank dataSource must leave the property unset"); + Assertions.assertNull(configured.getDriverClassName(), + "a blank driverClassName must leave the property unset"); + Assertions.assertNull(configured.getJdbcInterceptors(), + "a blank jdbcInterceptors must leave the property unset"); + Assertions.assertNull(configured.getValidationQuery(), + "a blank validationQuery must leave the property unset," + + " routing validation to isValid()"); + } + + @Test + void blankDriverClassNameStillBuildsTheDriverManagerFallbackPool() + throws Exception { + // Pre-fix, the empty attribute defeated the connect path's documented + // DriverManager fallback (keyed on the property being null) and + // aborted the whole resource on loading the empty class. + Reference ref = new Reference("javax.sql.DataSource", null, null); + ref.add(new StringRefAddr("jmxEnabled", "false")); + ref.add(new StringRefAddr("initialSize", "1")); + ref.add(new StringRefAddr("minIdle", "0")); + ref.add(new StringRefAddr("url", getUrl())); + ref.add(new StringRefAddr("username", "sa")); + ref.add(new StringRefAddr("password", "")); + ref.add(new StringRefAddr("driverClassName", "")); + ref.add(new StringRefAddr("jdbcInterceptors", " ")); + + DataSource ds = manage((DataSource) new DataSourceFactory() + .getObjectInstance(ref, new CompositeName("jdbc/BlankDS"), + null, null)); + + try (java.sql.Connection c = ds.getConnection(); + java.sql.ResultSet rs = c.createStatement().executeQuery("SELECT 1")) { + Assertions.assertTrue(rs.next(), + "the pool must build and connect through the fallback"); + } + } + + @Test + void driverClassNameAppliesIgnoringSurroundingWhitespace() { + // A Java class name carries no meaningful surrounding whitespace, so + // the padded shape a line-wrapped XML attribute yields must load the + // class it names, like the padded numeric and boolean spellings + // already do. Pre-fix only the blank test trimmed and the padded + // value went to the class loader verbatim. + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("driverClassName", " org.h2.Driver "); + + Assertions.assertEquals("org.h2.Driver", + DataSourceFactory.parsePoolProperties(attrs).getDriverClassName(), + "a padded class name must be applied trimmed"); + } + + @Test + void interceptorListWithRedundantSeparatorsStillBuildsThePool() + throws Exception { + // Pre-fix, one stray doubled separator - or one whitespace-only + // segment - made createPool() load the generated class "... + // pool.interceptor." and abort the whole resource with a + // SQLException whose message was null; the identical separator + // dropped at the end of the list was tolerated. + Reference ref = new Reference("javax.sql.DataSource", null, null); + ref.add(new StringRefAddr("jmxEnabled", "false")); + ref.add(new StringRefAddr("initialSize", "1")); + ref.add(new StringRefAddr("minIdle", "0")); + ref.add(new StringRefAddr("url", getUrl())); + ref.add(new StringRefAddr("username", "sa")); + ref.add(new StringRefAddr("password", "")); + ref.add(new StringRefAddr("jdbcInterceptors", + " ; StatementFinalizer;;SlowQueryReport ; ")); + + DataSource ds = manage((DataSource) new DataSourceFactory() + .getObjectInstance(ref, new CompositeName("jdbc/SemiDS"), + null, null)); + + try (java.sql.Connection c = ds.getConnection(); + java.sql.ResultSet rs = c.createStatement().executeQuery("SELECT 1")) { + Assertions.assertTrue(rs.next(), + "the pool must build and borrow with its named chain"); + } + } + + @Test + void aConfiguredCatalogIsStoredUntrimmed() { + // The counterpart: a catalog name is a database identifier, so only + // the blank test trims - a configured name keeps its exact spelling. + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("defaultCatalog", " My Catalog "); + + Assertions.assertEquals(" My Catalog ", + DataSourceFactory.parsePoolProperties(attrs).getDefaultCatalog(), + "a configured catalog name must be stored verbatim"); + } + + @Test + void blankObjectNameLeavesThePoolAtItsGeneratedName() { + // A cleared object_name attribute must not reach the pool as an + // empty quoted name - the JMX object name would carry an empty name + // component where the property being unset lets the pool name + // itself. + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("object_name", " "); + + String name = DataSourceFactory.parsePoolProperties(attrs).getName(); + + Assertions.assertNotEquals("\"\"", name, + "a blank object_name must not become an empty quoted name"); + Assertions.assertTrue(name.startsWith("Tomcat Connection Pool["), + "an unset object_name must leave the generated pool name," + + " was: " + name); + } + + @Test + void aConfiguredObjectNameIsQuotedButStoredVerbatim() { + // The counterpart, like the catalog: the name is quoted precisely so + // that "My Pool" survives the JMX object name, and only the blank + // test trims - a configured name keeps its exact spelling. + java.util.Properties attrs = new java.util.Properties(); + attrs.setProperty("object_name", " My Pool "); + + Assertions.assertEquals("\" My Pool \"", + DataSourceFactory.parsePoolProperties(attrs).getName(), + "a configured pool name must be quoted but stored verbatim"); + } + /* A factory-built pool, H2-backed, with one extra attribute. */ private DataSource factoryPoolWithAutoCommit(String value) throws Exception { diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestJdbcInterceptorConfigParsing.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestJdbcInterceptorConfigParsing.java index 27de71ba15..99548b1808 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestJdbcInterceptorConfigParsing.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestJdbcInterceptorConfigParsing.java @@ -172,26 +172,46 @@ class TestJdbcInterceptorConfigParsing { } @Test - void extraSemicolonsProduceAnExtraDefinitionOnlyInSomePositions() { - // This one DOES get an extra/empty definition + void redundantSemicolonsAreCosmeticInEveryPosition() { + // A redundant separator must mean the same thing wherever it + // appears: split() always dropped the trailing one, the property + // parse always tolerated one inside the parentheses, and an empty + // leading or doubled one used to survive as a definition without a + // class name - which the pool could only try to load as + // "...pool.interceptor.", failing the whole start with a message + // that named nothing. All positions now yield the same chain. + String[][] spellings = { + { ";LeadingSemicolonInterceptor", "LeadingSemicolonInterceptor" }, + { "Doubled;;SemicolonInterceptor", "Doubled,SemicolonInterceptor" }, + { "TrailingSemicolonInterceptor;", "TrailingSemicolonInterceptor" }, + { "TrailingWhitespaceInterceptor; ", "TrailingWhitespaceInterceptor" }, + }; + for (String[] spelling : spellings) { + PoolProperties props = new PoolProperties(); + props.setJdbcInterceptors(spelling[0]); + InterceptorDefinition[] jiDefs = props.getJdbcInterceptorsAsArray(); + Assertions.assertNotNull(jiDefs); + String[] names = spelling[1].split(","); + Assertions.assertEquals(names.length + 1, jiDefs.length, + spelling[0] + " must parse as trap plus the named interceptors"); + Assertions.assertEquals(TrapException.class.getName(), jiDefs[0].getClassName()); + for (int i = 0; i < names.length; i++) { + Assertions.assertEquals(names[i], jiDefs[i + 1].getClassName(), + spelling[0] + " must keep its real interceptors, in order"); + } + } + } + + @Test + void aSegmentWithoutANameContributesNoDefinition() { + // "(", " ( " and a segment that opens straight on the property list + // name no interceptor at all; keeping such a definition only moved + // the failure from the parser to the pool start PoolProperties props = new PoolProperties(); - props.setJdbcInterceptors(";EmptyLeadingSemiInterceptor"); + props.setJdbcInterceptors("(parm=value);NamedInterceptor"); InterceptorDefinition[] jiDefs = props.getJdbcInterceptorsAsArray(); - Assertions.assertNotNull(jiDefs); - Assertions.assertEquals(3, jiDefs.length); - - // This one does NOT get an extra/empty definition (no trailing whitespace) - props = new PoolProperties(); - props.setJdbcInterceptors("EmptyTrailingSemiInterceptor;"); - jiDefs = props.getJdbcInterceptorsAsArray(); - Assertions.assertNotNull(jiDefs); Assertions.assertEquals(2, jiDefs.length); - - // This one DOES get an extra/empty definition (with trailing whitespace) - props = new PoolProperties(); - props.setJdbcInterceptors("EmptyTrailingSemiInterceptor; "); - jiDefs = props.getJdbcInterceptorsAsArray(); - Assertions.assertNotNull(jiDefs); - Assertions.assertEquals(3, jiDefs.length); + Assertions.assertEquals(TrapException.class.getName(), jiDefs[0].getClassName()); + Assertions.assertEquals("NamedInterceptor", jiDefs[1].getClassName()); } } diff --git a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java index 689a8a73ed..57ee3de17a 100644 --- a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java +++ b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java @@ -213,6 +213,65 @@ class TestPoolProperties { "a credential injected outside the string must survive reconfiguration"); } + @Test + void credentialsOverridingTheStringSurviveAReconfigureThatDropsThem() { + // The factory's own shape: the string names user/password, and the + // username/password attributes are injected over the string's pairs. + // Pre-fix the injection overwrote the values but the string stayed the + // owner of the keys, so a JMX reconfigure whose string no longer named + // the credentials removed them from the driver properties while the + // credential fields still held them. + Properties attrs = new Properties(); + attrs.setProperty("connectionProperties", + "keep=yes;user=fromstring;password=fromstring"); + attrs.setProperty("username", DEFAULT_USER); + attrs.setProperty("password", DEFAULT_PASSWD); + + PoolProperties configured = + (PoolProperties) DataSourceFactory.parsePoolProperties(attrs); + + Assertions.assertEquals(DEFAULT_USER, + configured.getDbProperties().getProperty("user"), + "the configured username must override the string's pair"); + Assertions.assertEquals(DEFAULT_PASSWD, + configured.getDbProperties().getProperty("password"), + "the configured password must override the string's pair"); + + // the JMX reconfigure path + configured.setConnectionProperties("keep=again"); + + Assertions.assertEquals("again", + configured.getDbProperties().getProperty("keep"), + "a pair the new string names must carry the new value"); + Assertions.assertEquals(DEFAULT_USER, + configured.getDbProperties().getProperty("user"), + "a credential the fields still carry must not be stripped by a" + + " reconfigure that dropped the key from the string"); + Assertions.assertEquals(DEFAULT_PASSWD, + configured.getDbProperties().getProperty("password"), + "a credential the fields still carry must not be stripped by a" + + " reconfigure that dropped the key from the string"); + } + + @Test + void aCredentialEntryOnlyTheStringCarriesStaysTheStringsOwn() { + // Counterpart pin: with the field unset the entry is the string's, so + // the reconfigure really does drop it - the re-assert must not turn + // setConnectionProperties into a credential keeper that outlives both + // the field and the string. + PoolProperties properties = new PoolProperties(); + properties.setConnectionProperties("user=fromstring"); + + Assertions.assertEquals("fromstring", + properties.getDbProperties().getProperty("user"), + "the string's pair must be applied while no username is set"); + + properties.setConnectionProperties("keep=yes"); + + Assertions.assertNull(properties.getDbProperties().getProperty("user"), + "the string must keep owning the entry while the field is unset"); + } + @Test void clearedConnectionPropertiesRemoveEveryParsedPair() { PoolProperties properties = new PoolProperties(); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
