From 6a83dd0ee44b49a253f72e6c69af0c9da4f7225c Mon Sep 17 00:00:00 2001 From: Patrick Reinhart Date: Sun, 9 Jun 2024 21:34:47 +0200 Subject: [PATCH] major: reworked DatabaseManager logic - Method DatabaseManager::getCollection changed signature to take URI and connection properties only. - DatabaseManager::registerDatabase and DatabaseManager::deregisterDatabase check for existing registered Database using the equals method, - No longer strip `xmldb:` prefix when accessing the Database::acceptsURI and Database::getCollection. Signed-off-by: Patrick Reinhart --- settings.gradle | 2 +- .../java/org/xmldb/api/DatabaseManager.java | 313 ++++++++++-------- .../java/org/xmldb/api/base/Database.java | 90 +++-- .../org/xmldb/api/base/DatabaseAction.java | 63 ++++ .../org/xmldb/api/DatabaseManagerTest.java | 169 +++++----- .../java/org/xmldb/api/TestCollection.java | 48 ++- .../org/xmldb/api/TestCollectionData.java | 54 +++ .../java/org/xmldb/api/TestDatabase.java | 10 +- 8 files changed, 457 insertions(+), 292 deletions(-) create mode 100644 src/main/java/org/xmldb/api/base/DatabaseAction.java create mode 100644 src/testFixtures/java/org/xmldb/api/TestCollectionData.java diff --git a/settings.gradle b/settings.gradle index ba42f54..175140d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -44,7 +44,7 @@ plugins { reckon { defaultInferredScope = 'minor' snapshots() - scopeCalc = calcScopeFromProp() + scopeCalc = calcScopeFromProp().or(calcScopeFromCommitMessages()) stageCalc = calcStageFromProp() // enable parse of old `xmldb-api-xxx' tags tagParser = tagName -> java.util.Optional.of(tagName) diff --git a/src/main/java/org/xmldb/api/DatabaseManager.java b/src/main/java/org/xmldb/api/DatabaseManager.java index b149f16..6e4667e 100644 --- a/src/main/java/org/xmldb/api/DatabaseManager.java +++ b/src/main/java/org/xmldb/api/DatabaseManager.java @@ -40,162 +40,179 @@ package org.xmldb.api; import static org.xmldb.api.base.ErrorCodes.INSTANCE_NAME_ALREADY_REGISTERED; -import static org.xmldb.api.base.ErrorCodes.INVALID_DATABASE; -import static org.xmldb.api.base.ErrorCodes.INVALID_URI; import static org.xmldb.api.base.ErrorCodes.NO_SUCH_DATABASE; -import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; -import java.util.concurrent.locks.StampedLock; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; import org.xmldb.api.base.Collection; import org.xmldb.api.base.Database; +import org.xmldb.api.base.DatabaseAction; import org.xmldb.api.base.XMLDBException; /** - * {@code DatabaseManager} is the entry point for the API and enables you to get the initial - * {@code Collection} references necessary to do anything useful with the API. - * {@code DatabaseManager} is intended to be provided as a concrete implementation in a particular + * {@link DatabaseManager} is the entry point for the API and enables you to get the initial + * {@link Collection} references necessary to do anything useful with the API. + * {@link DatabaseManager} is intended to be provided as a concrete implementation in a particular * programming language. Individual language mappings should define the exact syntax and semantics * of its use. */ public final class DatabaseManager { - static final String URI_PREFIX = "xmldb:"; - - static final Properties properties = new Properties(); - static final StampedLock dbLock = new StampedLock(); - static final Map databases = new HashMap<>(5); + /** + * Defines the URI prefix declaring a XML database + */ + public static final String URI_PREFIX = "xmldb:"; - static boolean strictRegistrationBehavior = - Boolean.getBoolean("org.xmldb.api.strictRegistrationBehavior"); + private static final Map properties = new ConcurrentHashMap<>(); + private static final CopyOnWriteArrayList registeredDatabases = + new CopyOnWriteArrayList<>(); private DatabaseManager() {} /** - * Returns a set of all available {@code Database} implementations that have been registered with - * this {@code DatabaseManager}. + * Returns a set of all available {@link Database} implementations that have been registered with + * this {@link DatabaseManager}. * - * @return An array of {@code Database} instances. One for each {@code Database} registered with - * the {@code DatabaseManager}. If no {@code Database} instances exist then an empty set + * @return An array of {@link Database} instances. One for each {@link Database} registered with + * the {@link DatabaseManager}. If no {@link Database} instances exist then an empty set * is returned. * @since 2.0 */ public static Set getDatabases() { - // try optimistic read first - long stamp = dbLock.tryOptimisticRead(); - if (stamp > 0) { - final Set result = new HashSet<>(databases.values()); - if (dbLock.validate(stamp)) { - return result; - } - } - - // fallback to locking read - stamp = dbLock.readLock(); - try { - return new HashSet<>(databases.values()); - } finally { - dbLock.unlockRead(stamp); - } + return registeredDatabases.stream().map(DatabaseInfo::database).collect(Collectors.toSet()); } /** - * Registers a new {@code Database} implementation with the {@code DatabaseManager}. + * Registers a new {@link Database} implementation with the {@link DatabaseManager}. * * @param database The database instance to register. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any - * vendor specific errors that occur. {@code ErrorCodes.INVALID_DATABASE} if the provided - * {@code Database} instance is invalid. + * @throws XMLDBException with expected error codes. + * {@link org.xmldb.api.base.ErrorCodes#VENDOR_ERROR} for any vendor specific errors that + * occur. {@link org.xmldb.api.base.ErrorCodes#INVALID_DATABASE} if the provided + * {@link Database} instance is invalid. */ public static void registerDatabase(final Database database) throws XMLDBException { - final String name = database.getName(); - if (name == null || name.isEmpty()) { - throw new XMLDBException(INVALID_DATABASE); - } - final long stamp = dbLock.writeLock(); - try { - updateDatabases(name, database); - } finally { - dbLock.unlockWrite(stamp); - } - + registerDatabase(database, null); } - private static void updateDatabases(final String databaseName, final Database database) + /** + * Registers a new {@link Database} implementation with the {@link DatabaseManager}. + * + * @param database The database instance to register. + * @throws XMLDBException with expected error codes. + * {@link org.xmldb.api.base.ErrorCodes#VENDOR_ERROR} for any vendor specific errors that + * occur. {@link org.xmldb.api.base.ErrorCodes#INVALID_DATABASE} if the provided + * {@link Database} instance is invalid. + */ + public static void registerDatabase(final Database database, final DatabaseAction action) throws XMLDBException { - final Database existing = databases.putIfAbsent(databaseName, database); - if (existing != null && existing != database && strictRegistrationBehavior) { + if (!registeredDatabases.addIfAbsent(new DatabaseInfo(database, action))) { throw new XMLDBException(INSTANCE_NAME_ALREADY_REGISTERED); } } /** - * Deregisters a {@code Database} implementation from the {@code DatabaseManager}. Once a - * {@code Database} has been deregistered it can no longer be used to handle requests. + * Deregisters a {@link Database} implementation from the {@link DatabaseManager}. Once a + * {@link Database} has been deregistered it can no longer be used to handle requests. * - * @param database The {@code Database} instance to deregister. + * @param database The {@link Database} instance to deregister. */ public static void deregisterDatabase(final Database database) { - final long stamp = dbLock.writeLock(); - try { - databases.values().removeIf(database::equals); - } finally { - dbLock.unlockWrite(stamp); - } + registeredDatabases.removeIf(info -> { + if (info.database.equals(database)) { + info.deregister(); + return true; + } + return false; + }); } /** - * Retrieves a {@code Collection} instance from the database for the given URI. The format of the + * Retrieves a {@link Collection} instance from the database for the given URI. The format of the * majority of the URI is database implementation specific however the uri must begin with * characters xmldb: and be followed by the name of the database instance as returned by - * {@code Database.getName()} and a colon character. An example would be for the database named + * {@link Database#getName()} and a colon character. An example would be for the database named * "vendordb" the URI handed to getCollection would look something like the following. - * {@code xmldb:vendordb://host:port/path/to/collection}. The xmldb: prefix will be removed from - * the URI prior to handing the URI to the {@code Database} instance for handling. - *

+ * {@code xmldb:vendordb://host:port/path/to/collection}. + *

* This method is called when no authentication is necessary for the database. * * @param uri The database specific URI to use to locate the collection. - * @return A {@code Collection} instance for the requested collection or null if the collection - * could not be found. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any - * vendor specific errors that occur. {@code ErrroCodes.INVALID_URI} If the URI is not in - * a valid format. {@code ErrroCodes.NO_SUCH_DATABASE} If a {@code Database} instance - * could not be found to handle the provided URI. + * @return A {@link Collection} instance for the requested collection or {@code null} if the + * collection could not be found. + * @throws XMLDBException with expected error codes. + * {@link org.xmldb.api.base.ErrorCodes#VENDOR_ERROR} for any vendor specific errors that + * occur. {@link org.xmldb.api.base.ErrorCodes#INVALID_URI} If the URI is not in a valid + * format. {@link org.xmldb.api.base.ErrorCodes#NO_SUCH_DATABASE} If a {@link Database} + * instance could not be found to handle the provided URI. */ public static Collection getCollection(final String uri) throws XMLDBException { - return getCollection(uri, null, null); + return getCollection(uri, new Properties()); } /** - * Retrieves a {@code Collection} instance from the database for the given URI. The format of the + * Retrieves a {@link Collection} instance from the database for the given URI. The format of the * majority of the URI is database implementation specific however the uri must begin with * characters xmldb: and be followed by the name of the database instance as returned by - * {@code Database.getName()} and a colon character. An example would be for the database named + * {@link Database#getName()} and a colon character. An example would be for the database named * "vendordb" the URI handed to getCollection would look something like the following. - * {@code xmldb:vendordb://host:port/path/to/collection}. The xmldb: prefix will be removed from - * the URI prior to handing the URI to the {@code Database} instance for handling. + * {@code xmldb:vendordb://host:port/path/to/collection}. * * @param uri The database specific URI to use to locate the collection. - * @param username The username to use for authentication to the database or null if the database - * does not support authentication. - * @param password The password to use for authentication to the database or null if the database - * does not support authentication. - * @return A {@code Collection} instance for the requested collection or null if the collection - * could not be found. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any - * vendor specific errors that occur. {@code ErrroCodes.INVALID_URI} If the URI is not in - * a valid format. {@code ErrroCodes.NO_SUCH_DATABASE} If a {@code Database} instance - * could not be found to handle the provided URI. {@code ErrroCodes.PERMISSION_DENIED} If - * the {@code username} and {@code password} were not accepted by the database. + * @param user The username to use for authentication to the database or {@code null} if the + * database does not support authentication. + * @param password The password to use for authentication to the database or {@code null} if the + * database does not support authentication. + * @return A {@code Collection} instance for the requested collection or {@code null} if the + * collection could not be found. + * @throws XMLDBException with expected error codes. + * {@link org.xmldb.api.base.ErrorCodes#VENDOR_ERROR} for any vendor specific errors that + * occur. {@link org.xmldb.api.base.ErrorCodes#INVALID_URI} If the URI is not in a valid + * format. {@link org.xmldb.api.base.ErrorCodes#NO_SUCH_DATABASE} If a {@link Database} + * instance could not be found to handle the provided URI. + * {@link org.xmldb.api.base.ErrorCodes#PERMISSION_DENIED} If the {@code username} and + * {@code password} were not accepted by the database. */ - public static Collection getCollection(final String uri, final String username, - final String password) throws XMLDBException { - final Database db = getDatabase(uri); - return db.getCollection(stripURIPrefix(uri), username, password); + public static Collection getCollection(final String uri, final String user, final String password) + throws XMLDBException { + Properties info = new Properties(); + if (user != null) { + info.put("user", user); + } + if (password != null) { + info.put("password", password); + } + return getCollection(uri, info); + } + + /** + * Retrieves a {@link Collection} instance from the database for the given URI. The format of the + * majority of the URI is database implementation specific however the uri must begin with + * characters xmldb: and be followed by the name of the database instance as returned by + * {@link Database#getName()} and a colon character. An example would be for the database named + * "vendordb" the URI handed to getCollection would look something like the following. + * {@code xmldb:vendordb://host:port/path/to/collection}. + * + * @param uri The database specific URI to use to locate the collection. + * @param info The database specific connection options + * @return A {@code Collection} instance for the requested collection or {@code null} if the + * collection could not be found. + * @throws XMLDBException with expected error codes. + * {@link org.xmldb.api.base.ErrorCodes#VENDOR_ERROR} for any vendor specific errors that + * occur. {@link org.xmldb.api.base.ErrorCodes#INVALID_URI} If the URI is not in a valid + * format. {@link org.xmldb.api.base.ErrorCodes#NO_SUCH_DATABASE} If a {@link Database} + * instance could not be found to handle the provided URI. + * {@link org.xmldb.api.base.ErrorCodes#PERMISSION_DENIED} If the {@code username} and + * {@code password} were not accepted by the database. + * @since 3.0 + */ + public static Collection getCollection(final String uri, final Properties info) + throws XMLDBException { + return withDatabase(uri, database -> database.getCollection(uri, info)); } /** @@ -204,94 +221,96 @@ public static Collection getCollection(final String uri, final String username, * * @param uri The database specific URI to use to locate the collection. * @return The XML:DB Core Level conformance for the uri. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any - * vendor specific errors that occur. {@code ErrroCodes.INVALID_URI} If the URI is not in - * a valid format. {@code ErrroCodes.NO_SUCH_DATABASE} If a {@code Database} instance - * could not be found to handle the provided URI. + * @throws XMLDBException with expected error codes. + * {@link org.xmldb.api.base.ErrorCodes#VENDOR_ERROR} for any vendor specific errors that + * occur. {@link org.xmldb.api.base.ErrorCodes#INVALID_URI} If the URI is not in a valid + * format. {@link org.xmldb.api.base.ErrorCodes#NO_SUCH_DATABASE} If a {@link Database} + * instance could not be found to handle the provided URI. */ public static String getConformanceLevel(final String uri) throws XMLDBException { - final Database database = getDatabase(uri); - return database.getConformanceLevel(); + return withDatabase(uri, Database::getConformanceLevel); } /** - * Retrieves a property that has been set for the {@code DatabaseManager}. + * Retrieves a property that has been set for the {@link DatabaseManager}. * * @param name The property name * @return The property value */ public static String getProperty(final String name) { - return properties.getProperty(name); + return properties.get(name); } /** - * Sets a property for the {@code DatabaseManager}. + * Sets a property for the {@link DatabaseManager}. * * @param name The property name * @param value The value to set. */ public static void setProperty(final String name, final String value) { - properties.put(name, value); + if (value == null) { + properties.remove(name); + } else { + properties.put(name, value); + } } /** - * Retrieves the registered {@code Database} instance associated with the provided URI. + * Retrieves the registered {@link Database} instance associated with the provided URI. * * @param uri The uri containing the database reference. - * @return the requested {@code Database} instance. + * @return the requested {@link Database} instance. * @throws XMLDBException if an error occurs whilst getting the database */ - static Database getDatabase(final String uri) throws XMLDBException { - final String databaseAndCollection = stripURIPrefix(uri); - - final int end = databaseAndCollection.indexOf(":"); - if (end == -1) { - throw new XMLDBException(INVALID_URI); - } - - final String databaseName = databaseAndCollection.substring(0, end); - - // try optimistic read first - long stamp = dbLock.tryOptimisticRead(); - if (stamp > 0) { - final Database db = databases.get(databaseName); - if (dbLock.validate(stamp)) { - if (db == null) { - throw new XMLDBException(NO_SUCH_DATABASE); + static T withDatabase(final String uri, final DatabaseFunction function) + throws XMLDBException { + // Walk through the loaded registeredDrivers attempting to make a connection. + // Remember the first exception that gets raised, so we can re-throw it. + XMLDBException reason = null; + for (DatabaseInfo info : registeredDatabases) { + if (info.acceptsURI(uri)) { + try { + return function.apply(info.database); + } catch (XMLDBException ex) { + if (reason == null) { + reason = ex; + } } - return db; } } - - // fallback to locking read - final Database db; - stamp = dbLock.readLock(); - try { - db = databases.get(databaseName); - } finally { - dbLock.unlockRead(stamp); + if (reason != null) { + throw reason; } + throw new XMLDBException(NO_SUCH_DATABASE, "No matching database found for: " + uri); + } + + @FunctionalInterface + interface DatabaseFunction { + T apply(Database database) throws XMLDBException; + } - if (db == null) { - throw new XMLDBException(NO_SUCH_DATABASE, "No matching database found for: " + uri); + record DatabaseInfo(Database database, DatabaseAction action) { + boolean acceptsURI(String uri) { + return database.acceptsURI(uri); } - return db; - } + void deregister() { + if (action != null) { + action.deregister(); + } + } - /** - * Removes the URI_PREFIX from the front of the URI. This is so the database can focus on handling - * its own URIs. - * - * @param uri The full URI to strip. - * @return The database specific portion of the URI. - * @throws XMLDBException if an error occurs whilst stripping the URI prefix - */ - static String stripURIPrefix(final String uri) throws XMLDBException { - if (!uri.startsWith(URI_PREFIX)) { - throw new XMLDBException(INVALID_URI); + @Override + public int hashCode() { + return database.hashCode(); } - return uri.substring(URI_PREFIX.length()); + @Override + public boolean equals(Object obj) { + if (obj instanceof DatabaseInfo other) { + return database.equals(other.database); + } + return false; + } } } diff --git a/src/main/java/org/xmldb/api/base/Database.java b/src/main/java/org/xmldb/api/base/Database.java index c6cce28..442512b 100644 --- a/src/main/java/org/xmldb/api/base/Database.java +++ b/src/main/java/org/xmldb/api/base/Database.java @@ -39,65 +39,91 @@ */ package org.xmldb.api.base; +import java.util.Properties; + /** - * {@code Database} is an encapsulation of the database driver functionality that is necessary to - * access an XML database. Each vendor must provide their own implmentation of the {@code Database} - * interface. The implementation is registered with the {@code DatabaseManager} to provide access to - * the resources of the XML database. + * The interface that every XMLDB base class must implement. + *

+ * The {@code DatabaseManager} allows for multiple databases. + * + *

+ * Each XMLDB should supply a class that implements the {@linkplain Database} interface. + * + *

+ * The {@code DatabaseManager} will try to load as many drivers as it can find and then for any + * given {@code Database} request, it will ask each {@code Database} in turn to try to connect to + * the target URI. * - * In general usage client applications should only access {@code Database} implementations directly - * during initialization. + *

+ * It is strongly recommended that each {@code Database} class should be small and standalone so + * that the {@code Database} class can be loaded and queried without bringing in vast quantities of + * supporting code. + * + *

+ * When a {@code Database} class is loaded, it should create an instance of itself and register it + * with the {@code DatabaseManager}. This means that a user can load and register a driver by + * calling: + *

+ * {@code Class.forName("foo.bah.Database")} + *

+ * A {@code Database} may create a {@linkplain DatabaseAction} implementation in order to receive + * notifications when {@linkplain org.xmldb.api.DatabaseManager#deregisterDatabase} has been called. */ public interface Database extends Configurable { /** * Returns the name associated with the Database instance. * * @return the name of the object. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any + * @throws XMLDBException with expected error codes. {@link ErrorCodes#VENDOR_ERROR} for any * vendor specific errors that occur. */ String getName() throws XMLDBException; /** - * Retrieves a {@code Collection} instance based on the URI provided in the {@code uri} parameter. - * The format of the URI is defined in the documentation for DatabaseManager.getCollection(). - * - * Authentication is handled via username and password however it is not required that the - * database support authentication. Databases that do not support authentication MUST ignore the - * {@code username} and {@code password} if those provided are not null. + * Attempts to make a connection to the given database URI and return its root {@code Collection}. + * The {@code Database} should return "null" if it realizes it is the wrong kind of driver to + * connect to the given URI. This will be common, as when the {@code DatabaseManager} is asked to + * connect to a given URI it passes the URI to each loaded database in turn. + *

+ * The driver should throw an {@code XMLDBException} if it is the right database to connect for + * the given URI but has trouble connecting to the database. + *

+ * The {@code Properties} argument can be used to pass arbitrary string tag/value pairs as + * connection arguments. Normally at least "user" and "password" properties should be included in + * the {@code Properties} object. + *

+ * Note: If a property is specified as part of the {@code uri} and is also specified in the + * {@code Properties} object, it is implementation-defined as to which value will take precedence. + * For maximum portability, an application should only specify a property once. * - * @param uri the URI to use to locate the collection. - * @param username The username to use for authentication to the database or null if the database - * does not support authentication. - * @param password The password to use for authentication to the database or null if the database - * does not support authentication. - * @return A {@code Collection} instance for the requested collection or null if the collection - * could not be found. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any - * vendor specific errors that occur. {@code ErrroCodes.INVALID_URI} If the URI is not in - * a valid format. {@code ErrroCodes.PERMISSION_DENIED} If the {@code username} and + * @param uri the URI of the database to which to connect and return the root collection + * @param info a list of arbitrary string tag/value pairs as connection arguments. Normally at + * least a "user" and "password" property should be included. + * @return a {@code Collection} object that represents a connection to the URI + * @throws XMLDBException with expected error codes. {@link ErrorCodes#VENDOR_ERROR} for any + * vendor specific errors that occur. {@link ErrorCodes#INVALID_URI} If the URI is not in + * a valid format. {@link ErrorCodes#PERMISSION_DENIED} If the {@code username} and * {@code password} were not accepted by the database. + * @since 3.0 */ - Collection getCollection(String uri, String username, String password) throws XMLDBException; + Collection getCollection(String uri, Properties info) throws XMLDBException; /** - * acceptsURI determines whether this {@code Database} implementation can handle the URI. It - * should return true if the Database instance knows how to handle the URI and false otherwise. + * acceptsURI determines whether this {@link Database} implementation can handle the URI. It + * should return {@code true} if the Database instance knows how to handle the URI and + * {@code false} otherwise. * * @param uri the URI to check for. - * @return true if the URI can be handled, false otherwise. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any - * vendor specific errors that occur. {@code ErrroCodes.INVALID_URI} If the URI is not in - * a valid format. + * @return {@code true} if the URI can be handled, {@code false} otherwise. */ - boolean acceptsURI(String uri) throws XMLDBException; + boolean acceptsURI(String uri); /** * Returns the XML:DB API Conformance level for the implementation. This can be used by client * programs to determine what functionality is available to them. * * @return the XML:DB API conformance level for this implementation. - * @throws XMLDBException with expected error codes. {@code ErrorCodes.VENDOR_ERROR} for any + * @throws XMLDBException with expected error codes. {@link ErrorCodes#VENDOR_ERROR} for any * vendor specific errors that occur. */ String getConformanceLevel() throws XMLDBException; diff --git a/src/main/java/org/xmldb/api/base/DatabaseAction.java b/src/main/java/org/xmldb/api/base/DatabaseAction.java new file mode 100644 index 0000000..5dd6567 --- /dev/null +++ b/src/main/java/org/xmldb/api/base/DatabaseAction.java @@ -0,0 +1,63 @@ +/* + * The XML:DB Initiative Software License, Version 1.0 + * + * Copyright (c) 2000-2024 The XML:DB Initiative. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The end-user documentation included with the redistribution, if any, must include the + * following acknowledgment: "This product includes software developed by the XML:DB Initiative + * (http://www.xmldb.org/)." Alternately, this acknowledgment may appear in the software itself, if + * and wherever such third-party acknowledgments normally appear. + * + * 4. The name "XML:DB Initiative" must not be used to endorse or promote products derived from this + * software without prior written permission. For written permission, please contact info@xmldb.org. + * + * 5. Products derived from this software may not be called "XML:DB", nor may "XML:DB" appear in + * their name, without prior written permission of the XML:DB Initiative. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ================================================================================================= + * This software consists of voluntary contributions made by many individuals on behalf of the + * XML:DB Initiative. For more information on the XML:DB Initiative, please see + * + */ +package org.xmldb.api.base; + +import org.xmldb.api.DatabaseManager; + +public interface DatabaseAction { + /** + * Method called by {@linkplain DatabaseManager#deregisterDatabase(Database) } to notify the + * Database that it was de-registered. + *

+ * The {@code deregister} method is intended only to be used by database and not by applications. + * Databases are recommended to not implement {@code DatabaseAction} in a public class. If there + * are active connections to the database at the time that the {@code deregister} method is + * called, it is implementation specific as to whether the connections are closed or allowed to + * continue. Once this method is called, it is implementation specific as to whether the database + * may limit the ability to open collections of a database, invoke other {@code Database} methods + * or throw a {@code XMLDBException}. Consult your database's documentation for additional + * information on its behavior. + * + * @see DatabaseManager#registerDatabase(Database, DatabaseAction) + * @see DatabaseManager#deregisterDatabase(Database) + * @since 3 + */ + void deregister(); +} diff --git a/src/test/java/org/xmldb/api/DatabaseManagerTest.java b/src/test/java/org/xmldb/api/DatabaseManagerTest.java index 3e5e557..b19af5d 100644 --- a/src/test/java/org/xmldb/api/DatabaseManagerTest.java +++ b/src/test/java/org/xmldb/api/DatabaseManagerTest.java @@ -41,24 +41,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static org.xmldb.api.base.ErrorCodes.INSTANCE_NAME_ALREADY_REGISTERED; -import static org.xmldb.api.base.ErrorCodes.INVALID_DATABASE; -import static org.xmldb.api.base.ErrorCodes.INVALID_URI; import static org.xmldb.api.base.ErrorCodes.NO_SUCH_DATABASE; -import java.util.AbstractMap.SimpleEntry; -import java.util.Map.Entry; +import java.util.Properties; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.xmldb.api.base.Collection; import org.xmldb.api.base.Database; +import org.xmldb.api.base.DatabaseAction; import org.xmldb.api.base.XMLDBException; @ExtendWith(MockitoExtension.class) @@ -67,111 +65,102 @@ class DatabaseManagerTest { Database dbOne; @Mock Database dbTwo; - - @BeforeEach - void setUp() { - DatabaseManager.strictRegistrationBehavior = false; - } + @Mock + DatabaseAction dbAction; + @Mock + Collection collection; @AfterEach - void tearDown() throws Exception { - DatabaseManager.databases.clear(); - DatabaseManager.properties.clear(); + void tearDown() { + DatabaseManager.getDatabases().forEach(DatabaseManager::deregisterDatabase); + DatabaseManager.setProperty("key", null); + verifyNoMoreInteractions(dbOne, dbTwo, dbAction, collection); } @Test - void testGetDatabases() { + void testGetDatabases() throws XMLDBException { assertThat(DatabaseManager.getDatabases()).isEmpty(); - DatabaseManager.databases.put("1", dbOne); + DatabaseManager.registerDatabase(dbOne); assertThat(DatabaseManager.getDatabases()).containsExactly(dbOne); - DatabaseManager.databases.put("2", dbTwo); + DatabaseManager.registerDatabase(dbTwo); assertThat(DatabaseManager.getDatabases()).containsExactlyInAnyOrder(dbOne, dbTwo); } - @Test - void testRegisterDatabase_no_or_empty_name() throws XMLDBException { - when(dbOne.getName()).thenReturn(null); - - assertThatExceptionOfType(XMLDBException.class) - .isThrownBy(() -> DatabaseManager.registerDatabase(dbOne)).satisfies(e -> { - assertThat(e.errorCode).isEqualTo(INVALID_DATABASE); - assertThat(e.vendorErrorCode).isZero(); - }); - - assertThat(DatabaseManager.databases).isEmpty(); - - when(dbOne.getName()).thenReturn(""); - - assertThatExceptionOfType(XMLDBException.class) - .isThrownBy(() -> DatabaseManager.registerDatabase(dbOne)).satisfies(e -> { - assertThat(e.errorCode).isEqualTo(INVALID_DATABASE); - assertThat(e.vendorErrorCode).isZero(); - }); - - assertThat(DatabaseManager.databases).isEmpty(); - } - @Test void testRegisterDatabase() throws XMLDBException { - when(dbOne.getName()).thenReturn("databaseNameOne"); - DatabaseManager.registerDatabase(dbOne); - assertThat(DatabaseManager.databases.entrySet()) - .containsExactly(entry("databaseNameOne", dbOne)); - - when(dbTwo.getName()).thenReturn("databaseNameTwo"); + assertThat(DatabaseManager.getDatabases()).containsExactly(dbOne); DatabaseManager.registerDatabase(dbTwo); - assertThat(DatabaseManager.databases.entrySet()).containsExactlyInAnyOrder( - entry("databaseNameOne", dbOne), entry("databaseNameTwo", dbTwo)); + assertThat(DatabaseManager.getDatabases()).containsExactlyInAnyOrder(dbOne, dbTwo); } @Test void testDeregisterDatabase() throws XMLDBException { - DatabaseManager.databases.put("one", dbOne); - DatabaseManager.databases.put("databaseNameOne", dbOne); - DatabaseManager.databases.put("databaseAliasNameOne", dbOne); + DatabaseManager.registerDatabase(dbOne); + DatabaseManager.registerDatabase(dbTwo, dbAction); DatabaseManager.deregisterDatabase(dbOne); + DatabaseManager.deregisterDatabase(dbTwo); - assertThat(DatabaseManager.databases).isEmpty(); + assertThat(DatabaseManager.getDatabases()).isEmpty(); + verify(dbAction).deregister(); } @Test - void testGetCollectionString() throws XMLDBException { - DatabaseManager.databases.put("dbName", dbOne); - Collection collection = mock(Collection.class); + void testGetCollection() throws XMLDBException { + DatabaseManager.registerDatabase(dbOne); + Properties info = new Properties(); - when(dbOne.getCollection("dbName:collection", null, null)).thenReturn(collection); + when(dbOne.acceptsURI("xmldb:dbName:collection")).thenReturn(true); + when(dbOne.getCollection("xmldb:dbName:collection", info)).thenReturn(collection); assertThat(DatabaseManager.getCollection("xmldb:dbName:collection")).isEqualTo(collection); } @Test - void testGetCollectionStringStringString() throws XMLDBException { - DatabaseManager.databases.put("dbName", dbOne); - Collection collection = mock(Collection.class); + void testGetCollectionUserPassword() throws XMLDBException { + DatabaseManager.registerDatabase(dbOne); + Properties info = new Properties(); + info.setProperty("user", "username1"); + info.setProperty("password", "password1"); + + when(dbOne.acceptsURI("xmldb:dbName:collection")).thenReturn(true); + when(dbOne.getCollection("xmldb:dbName:collection", info)).thenReturn(collection); + + assertThat(DatabaseManager.getCollection("xmldb:dbName:collection", "username1", "password1")) + .isEqualTo(collection); + } + + @Test + void testGetCollectionConnectInfo() throws XMLDBException { + DatabaseManager.registerDatabase(dbOne); + Properties info = new Properties(); + info.setProperty("user", "username2"); + info.setProperty("password", "password2"); - when(dbOne.getCollection("dbName:collection", "username", "password")).thenReturn(collection); + when(dbOne.acceptsURI("xmldb:dbName:collection")).thenReturn(true); + when(dbOne.getCollection("xmldb:dbName:collection", info)).thenReturn(collection); - assertThat(DatabaseManager.getCollection("xmldb:dbName:collection", "username", "password")) + assertThat(DatabaseManager.getCollection("xmldb:dbName:collection", info)) .isEqualTo(collection); } @Test void testGetConformanceLevel() throws XMLDBException { - DatabaseManager.databases.put("dbName", dbOne); + DatabaseManager.registerDatabase(dbOne); + when(dbOne.acceptsURI("xmldb:dbName:collection")).thenReturn(true); when(dbOne.getConformanceLevel()).thenReturn("1"); - assertThat(DatabaseManager.getConformanceLevel("xmldb:dbName::collection")).isEqualTo("1"); + assertThat(DatabaseManager.getConformanceLevel("xmldb:dbName:collection")).isEqualTo("1"); } @Test void testGetProperty() { - DatabaseManager.properties.setProperty("key", "value"); + DatabaseManager.setProperty("key", "value"); assertThat(DatabaseManager.getProperty("key")).isEqualTo("value"); assertThat(DatabaseManager.getProperty("keyTwo")).isNull(); @@ -181,52 +170,54 @@ void testGetProperty() { void testSetProperty() { DatabaseManager.setProperty("key", "value"); - assertThat(DatabaseManager.properties.getProperty("key")).isEqualTo("value"); + assertThat(DatabaseManager.getProperty("key")).isEqualTo("value"); } @Test - void testRegisterDatabase_using_strict_check() throws XMLDBException { - DatabaseManager.strictRegistrationBehavior = true; - - when(dbOne.getName()).thenReturn("databaseNameOne"); - when(dbTwo.getName()).thenReturn("databaseNameOne"); - + void testRegisterDatabase_alreadyRegistered() throws XMLDBException { DatabaseManager.registerDatabase(dbOne); assertThatExceptionOfType(XMLDBException.class) - .isThrownBy(() -> DatabaseManager.registerDatabase(dbTwo)).satisfies(e -> { + .isThrownBy(() -> DatabaseManager.registerDatabase(dbOne)).satisfies(e -> { assertThat(e.errorCode).isEqualTo(INSTANCE_NAME_ALREADY_REGISTERED); assertThat(e.vendorErrorCode).isZero(); }); } @Test - void testStripURIPrefix() { + void testWithDatabaseConnectionError() throws XMLDBException { + Properties info = new Properties(); + XMLDBException error = new XMLDBException(); + DatabaseManager.registerDatabase(dbOne); + + when(dbOne.acceptsURI("xmldb:somedb:collection")).thenReturn(true); + when(dbOne.getCollection("xmldb:somedb:collection", info)).thenThrow(error); + assertThatExceptionOfType(XMLDBException.class) - .isThrownBy(() -> DatabaseManager.stripURIPrefix("unkown-prefix")).satisfies(e -> { - assertThat(e.errorCode).isEqualTo(INVALID_URI); - assertThat(e.vendorErrorCode).isZero(); - }); + .isThrownBy(() -> DatabaseManager.withDatabase("xmldb:somedb:collection", + db -> db.getCollection("xmldb:somedb:collection", info))) + .isEqualTo(error); } @Test - void testGetDatabaseWrongUri() { - assertThatExceptionOfType(XMLDBException.class) - .isThrownBy(() -> DatabaseManager.getDatabase("xmldb:somedb")).satisfies(e -> { - assertThat(e.errorCode).isEqualTo(INVALID_URI); - assertThat(e.vendorErrorCode).isZero(); - }); + void testWithDatabaseNull() throws XMLDBException { + Properties info = new Properties(); + DatabaseManager.registerDatabase(dbOne); + DatabaseManager.registerDatabase(dbTwo); + + when(dbOne.acceptsURI("xmldb:somedb:collection")).thenReturn(true); + when(dbOne.getCollection("xmldb:somedb:collection", info)).thenReturn(null); + + assertThat((Collection) DatabaseManager.withDatabase("xmldb:somedb:collection", + db -> db.getCollection("xmldb:somedb:collection", info))).isNull(); } @Test - void testGetDatabaseUnkown() { + void testWithDatabaseUnknown() { assertThatExceptionOfType(XMLDBException.class) - .isThrownBy(() -> DatabaseManager.getDatabase("xmldb:somedb:collection")).satisfies(e -> { + .isThrownBy(() -> DatabaseManager.withDatabase("xmldb:somedb:collection", db -> "check")) + .satisfies(e -> { assertThat(e.errorCode).isEqualTo(NO_SUCH_DATABASE); assertThat(e.vendorErrorCode).isZero(); }); } - - private static Entry entry(String key, Database value) { - return new SimpleEntry<>(key, value); - } } diff --git a/src/testFixtures/java/org/xmldb/api/TestCollection.java b/src/testFixtures/java/org/xmldb/api/TestCollection.java index 855e2b6..e320d0c 100644 --- a/src/testFixtures/java/org/xmldb/api/TestCollection.java +++ b/src/testFixtures/java/org/xmldb/api/TestCollection.java @@ -45,6 +45,8 @@ import java.time.Instant; import java.util.List; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.xmldb.api.base.Collection; import org.xmldb.api.base.Resource; @@ -52,25 +54,29 @@ import org.xmldb.api.base.XMLDBException; public class TestCollection extends ConfigurableImpl implements Collection { - private final String name; - private final Collection parent; - private final Instant creation; + private final TestCollectionData data; + private final ConcurrentMap childCollections; private boolean closed; + private Collection parent; - public TestCollection(String name) { - this(name, null); + public TestCollection(final TestCollectionData data) { + this(data, null); } - public TestCollection(String name, Collection parent) { - this.name = name; + public TestCollection(final TestCollectionData data, final Collection parent) { + this.data = data; this.parent = parent; - creation = Instant.now(); + childCollections = new ConcurrentHashMap<>(); + } + + public static TestCollection create(String name) { + return new TestCollection(new TestCollectionData(name)); } @Override public final String getName() throws XMLDBException { - return name; + return data.name(); } @Override @@ -88,24 +94,25 @@ public S getService(Class serviceType) throws XMLDBExcept throw new XMLDBException(NOT_IMPLEMENTED); } - @Override - public Collection getParentCollection() throws XMLDBException { - return parent; - } - @Override public int getChildCollectionCount() throws XMLDBException { - return 0; + return childCollections.size(); } @Override public List listChildCollections() throws XMLDBException { - return emptyList(); + return childCollections.keySet().stream().toList(); } @Override public Collection getChildCollection(String collectionName) throws XMLDBException { - return null; + return new TestCollection( + childCollections.computeIfAbsent(collectionName, TestCollectionData::new), this); + } + + @Override + public Collection getParentCollection() throws XMLDBException { + return parent; } @Override @@ -155,6 +162,11 @@ public void close() throws XMLDBException { @Override public Instant getCreationTime() throws XMLDBException { - return creation; + return data.creation(); + } + + @Override + public String toString() { + return "/%s".formatted(data.name()); } } diff --git a/src/testFixtures/java/org/xmldb/api/TestCollectionData.java b/src/testFixtures/java/org/xmldb/api/TestCollectionData.java new file mode 100644 index 0000000..9f57772 --- /dev/null +++ b/src/testFixtures/java/org/xmldb/api/TestCollectionData.java @@ -0,0 +1,54 @@ +/* + * The XML:DB Initiative Software License, Version 1.0 + * + * Copyright (c) 2000-2024 The XML:DB Initiative. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are permitted + * provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of conditions + * and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list of + * conditions and the following disclaimer in the documentation and/or other materials provided with + * the distribution. + * + * 3. The end-user documentation included with the redistribution, if any, must include the + * following acknowledgment: "This product includes software developed by the XML:DB Initiative + * (http://www.xmldb.org/)." Alternately, this acknowledgment may appear in the software itself, if + * and wherever such third-party acknowledgments normally appear. + * + * 4. The name "XML:DB Initiative" must not be used to endorse or promote products derived from this + * software without prior written permission. For written permission, please contact info@xmldb.org. + * + * 5. Products derived from this software may not be called "XML:DB", nor may "XML:DB" appear in + * their name, without prior written permission of the XML:DB Initiative. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ================================================================================================= + * This software consists of voluntary contributions made by many individuals on behalf of the + * XML:DB Initiative. For more information on the XML:DB Initiative, please see + * + */ +package org.xmldb.api; + +import java.time.Instant; +import java.util.Objects; + +public record TestCollectionData(String name, Instant creation) { + public TestCollectionData(String name) { + this(name, Instant.now()); + } + + public TestCollectionData { + Objects.requireNonNull(name); + Objects.requireNonNull(creation); + } +} diff --git a/src/testFixtures/java/org/xmldb/api/TestDatabase.java b/src/testFixtures/java/org/xmldb/api/TestDatabase.java index f199836..3704376 100644 --- a/src/testFixtures/java/org/xmldb/api/TestDatabase.java +++ b/src/testFixtures/java/org/xmldb/api/TestDatabase.java @@ -41,6 +41,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.Properties; import org.xmldb.api.base.Collection; import org.xmldb.api.base.Database; @@ -71,18 +72,17 @@ public final String getName() throws XMLDBException { } public TestCollection addCollection(String collectionName) { - return collections.computeIfAbsent(collectionName, TestCollection::new); + return collections.computeIfAbsent(collectionName, TestCollection::create); } @Override - public Collection getCollection(String uri, String username, String password) - throws XMLDBException { + public Collection getCollection(String uri, Properties info) throws XMLDBException { return collections.get(uri); } @Override - public boolean acceptsURI(String uri) throws XMLDBException { - return false; + public boolean acceptsURI(String uri) { + return uri.startsWith(DatabaseManager.URI_PREFIX + "test"); } @Override