docs: public ResourceFactory createResourceFactory(Map<String, Object> props) throws Exception {
          final boolean trace = TraceComponent.isAnyTracingEnabled();
          if (trace && tc.isEntryEnabled())
              Tr.entry(tc, "createResourceFactory", PropertyService.hidePasswords(props));
  
          Hashtable<String, Object> cmSvcProps = new Hashtable<String, Object>();
          Hashtable<String, Object> dsSvcProps = new Hashtable<String, Object>();
          Hashtable<String, Object> driverProps = new Hashtable<String, Object>();
          Map<String, Object> vendorProps = new HashMap<String, Object>();
  
          // Initially copy everything into vendor properties and expand any variables
          VariableRegistry variableRegistry = connectorSvc.getVariableRegistry();
          for (Map.Entry<String, Object> prop : props.entrySet()) {
              Object value = prop.getValue();
              if (value instanceof String)
                  value = variableRegistry.resolveRawString((String) value);
              vendorProps.put(prop.getKey(), value);
          }
  
          String application = (String) vendorProps.remove(AppDefinedResource.APPLICATION);
          String declaringApplication = (String) vendorProps.remove(DataSourceService.DECLARING_APPLICATION);
          String module = (String) vendorProps.remove(AppDefinedResource.MODULE);
          String component = (String) vendorProps.remove(AppDefinedResource.COMPONENT);
          String jndiName = (String) vendorProps.remove(ResourceFactory.JNDI_NAME);
  
          String dataSourceID = getDataSourceID(application, module, component, jndiName);
          String conManagerID = dataSourceID + '/' + ConnectionManagerService.CONNECTION_MANAGER;
          String jdbcDriverID = dataSourceID + '/' + JDBCDriverService.JDBC_DRIVER;
  
          String conManagerFilter = FilterUtils.createPropertyFilter(ID, conManagerID);
          String jdbcDriverFilter = FilterUtils.createPropertyFilter(ID, jdbcDriverID);
  
          StringBuilder filter = new StringBuilder(FilterUtils.createPropertyFilter(ID, dataSourceID));
          filter.insert(filter.length() - 1, '*');
          // Fail if server.xml is already using the id
          if (!removeExistingConfigurations(filter.toString()))
              throw new IllegalArgumentException(dataSourceID); // internal error, shouldn't ever have been permitted in server.xml
  
          cmSvcProps.put(ID, conManagerID);
          cmSvcProps.put(CONFIG_DISPLAY_ID, conManagerID);
          driverProps.put(ID, jdbcDriverID);
          driverProps.put(CONFIG_DISPLAY_ID, jdbcDriverID);
          dsSvcProps.put(ID, dataSourceID);
          dsSvcProps.put(CONFIG_DISPLAY_ID, dataSourceID);
          dsSvcProps.put(ResourceFactory.JNDI_NAME, jndiName);
          // Use the unique identifier because jndiName is not always unique for app-defined data sources
          dsSvcProps.put(DataSourceService.UNIQUE_JNDI_NAME, dataSourceID);
          dsSvcProps.put(DataSourceService.TARGET_CONNECTION_MANAGER, conManagerFilter);
          dsSvcProps.put(DataSourceService.TARGET_JDBC_DRIVER, jdbcDriverFilter);
          dsSvcProps.put("connectionManager.cardinality.minimum", 1); // require exactly 1 connection manager (the one specified by the target)
  
          String containerAuthDataRef = (String) vendorProps.remove(DSConfig.CONTAINER_AUTH_DATA_REF);
          if (containerAuthDataRef != null) {
              String authDataFilter = FilterUtils.createPropertyFilter(ID, containerAuthDataRef);
              dsSvcProps.put(DataSourceService.TARGET_CONTAINER_AUTH_DATA, authDataFilter);
              dsSvcProps.put("containerAuthData.cardinality.minimum", 1);
          }
  
          String recoveryAuthDataRef = (String) vendorProps.remove(DSConfig.RECOVERY_AUTH_DATA_REF);
          if (recoveryAuthDataRef != null) {
              String authDataFilter = FilterUtils.createPropertyFilter(ID, recoveryAuthDataRef);
              dsSvcProps.put(DataSourceService.TARGET_RECOVERY_AUTH_DATA, authDataFilter);
              dsSvcProps.put("recoveryAuthData.cardinality.minimum", 1);
          }
  
          if (application != null) {
              dsSvcProps.put(AppDefinedResource.APPLICATION, application);
              if (module != null) {
                  dsSvcProps.put(AppDefinedResource.MODULE, module);
                  if (component != null)
                      dsSvcProps.put(AppDefinedResource.COMPONENT, component);
              }
          }
  
          Object value;
  
          for (String name : DSConfig.DATA_SOURCE_PROPS)
              if ((value = vendorProps.remove(name)) != null)
                  dsSvcProps.put(name, value);
  
          BundleContext bundleContext = DataSourceService.priv.getBundleContext(FrameworkUtil.getBundle(DataSourceResourceFactoryBuilder.class));
  
          // className
          String className = (String) vendorProps.remove(DataSourceDef.className.name());
  
          // url should be used if properties like serverName/portNumber/databaseName are unspecified
          String url = (String) vendorProps.remove(DataSourceDef.url.name());
          if (vendorProps.containsKey(DataSourceDef.databaseName.name()) || vendorProps.containsKey(DataSourceDef.portNumber.name()))
              url = null;
  
          // libraryRef - scan shared libraries from the application
          className = updateWithLibraries(bundleContext, application, declaringApplication, className, url, driverProps, dsSvcProps);
  
          boolean isUCP = className.startsWith("oracle.ucp");
          
          for (String name : ConnectionManagerService.CONNECTION_MANAGER_PROPS) {
              //keep the datasource definition conn manager props in vender props when using UCP so they are passed to UCP
              if(!isUCP || !ConnectionManagerService.CONNECTION_MANAGER_DSD_PROPS.contains(name))
                  if ((value = vendorProps.remove(name)) != null) {
                      cmSvcProps.put(name, value);
                  }
          }
          
          // initialPoolSize > 0 not supported unless using UCP
          if(!isUCP) {
              value = vendorProps.remove(DataSourceDef.initialPoolSize.name());
              if (value != null && ((Integer) value) > 0)
                  ConnectorService.logMessage(Level.INFO, "IGNORE_FEATURE_J2CA0240", DataSourceDef.initialPoolSize.name(), jndiName);
          }
          
          // maxStatements per datasource --> statementCacheSize per connection or for UCP maxStatements per connection
          value = vendorProps.remove(DataSourceDef.maxStatements.name());
          if (value != null) {
              Integer maxPoolSize = (Integer) (isUCP ? vendorProps.get(DataSourceDef.maxPoolSize.name()) : cmSvcProps.get(DataSourceDef.maxPoolSize.name()));
              int stmtCacheSize = maxPoolSize == null || maxPoolSize <= 0 ? 0 : ((Integer) value / maxPoolSize);
              if(isUCP) {
                  //when using UCP put maxStatements back to vendor props after dividing so it's sent to UCP
                  vendorProps.put(DataSourceDef.maxStatements.name(), stmtCacheSize);
              } else {
                  dsSvcProps.put(DSConfig.STATEMENT_CACHE_SIZE, stmtCacheSize);
              }
          }
  
          // serverName defaults to "localhost" (unless it's Derby Embedded, which doesn't have serverName)
          if (!vendorProps.containsKey(DataSourceDef.serverName.name()))
              if (url != null)
                  vendorProps.put("URL", url);
              else if (!className.startsWith("org.apache.derby.jdbc.Embedded"))
                  vendorProps.put(DataSourceDef.serverName.name(), "localhost");
  
          PropertyService.parseDurationProperties(vendorProps, className, connectorSvc);
          PropertyService.parsePasswordProperties(vendorProps);
  
          // Add vendor properties to the data source properties as flattened config
          for (Map.Entry<String, Object> entry : vendorProps.entrySet())
              dsSvcProps.put(BASE_PROPERTIES_KEY + entry.getKey(), entry.getValue());
  
          StringBuilder dsFilter = new StringBuilder(200);
          dsFilter.append("(&").append(FilterUtils.createPropertyFilter(ID, dataSourceID));
          dsFilter.append(FilterUtils.createPropertyFilter(Constants.OBJECTCLASS, DataSourceService.class.getName())).append(")");
  
          ResourceFactory factory = new AppDefinedResourceFactory(this, bundleContext, dataSourceID, dsFilter.toString(), declaringApplication);
          ConfigurationAdmin configAdmin = null;
          try {
              String bundleLocation = bundleContext.getBundle().getLocation();
              String jcaBundleLocation = DataSourceService.priv.getBundleContext(FrameworkUtil.getBundle(ConnectorService.class)).getBundle().getLocation();
  
              configAdmin = configAdminRef.getService();
  
              Configuration conMgrConfig = configAdmin.createFactoryConfiguration(ConnectionManagerService.FACTORY_PID, jcaBundleLocation);
              conMgrConfig.update(cmSvcProps);
              dsSvcProps.put(DSConfig.CONNECTION_MANAGER_REF, new String[] { conMgrConfig.getPid() });
  
              Configuration driverConfig = configAdmin.createFactoryConfiguration(JDBCDriverService.FACTORY_PID, bundleLocation);
              driverConfig.update(driverProps);
              dsSvcProps.put(DSConfig.JDBC_DRIVER_REF, new String[] { driverConfig.getPid() });
  
              Configuration dsSvcConfig = configAdmin.createFactoryConfiguration(DataSourceService.FACTORY_PID, bundleLocation);
              dsSvcConfig.update(dsSvcProps);
          } catch (Exception x) {
              factory.destroy();
              throw x;
          } catch (Error x) {
              factory.destroy();
              throw x;
          } finally {
              if(configAdmin != null)
                  bundleContext.ungetService(configAdminRef.getReference());
          }
  
          if (trace && tc.isEntryEnabled())
              Tr.exit(tc, "createResourceFactory", factory);
          return factory;
      }