class JDBCAdminProvider implements AdminProvider {

    private static final Logger Log = LoggerFactory.getLogger(JDBCAdminProvider.class);

    private final String getAdminsSQL;
    private final String insertAdminsSQL;
    private final String deleteAdminsSQL;
    private final String xmppDomain;
    private final boolean useConnectionProvider;

    private String connectionString;

    /**
     * Constructs a new JDBC admin provider.
     */
    public JDBCAdminProvider() {
        // Convert XML based provider setup to Database based
        JiveGlobals.migrateProperty("jdbcProvider.driver");
        JiveGlobals.migrateProperty("jdbcProvider.connectionString");
        JiveGlobals.migrateProperty("jdbcAdminProvider.getAdminsSQL");

        xmppDomain = XMPPServerInfo.XMPP_DOMAIN.getValue();
        useConnectionProvider = JiveGlobals.getBooleanProperty("jdbcAdminProvider.useConnectionProvider");

        // Load database statement for reading admin list
        getAdminsSQL = JiveGlobals.getProperty("jdbcAdminProvider.getAdminsSQL");
        insertAdminsSQL = JiveGlobals.getProperty("jdbcAdminProvider.insertAdminsSQL", "");
        deleteAdminsSQL = JiveGlobals.getProperty("jdbcAdminProvider.deleteAdminsSQL", "");

        // Load the JDBC driver and connection string
        if (!useConnectionProvider) {
            String jdbcDriver = JiveGlobals.getProperty("jdbcProvider.driver");
            try {
                Class.forName(jdbcDriver).getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                Log.error("Unable to load JDBC driver: " + jdbcDriver, e);
                return;
            }
            connectionString = JiveGlobals.getProperty("jdbcProvider.connectionString");
        }
    }

    /**
     * XMPP disallows some characters in identifiers, requiring them to be escaped.
     *
     * This implementation assumes that the database returns properly escaped identifiers,
     * but can apply escaping by setting the value of the 'jdbcAdminProvider.isEscaped'
     * property to 'false'.
     *
     * @return 'false' if this implementation needs to escape database content before processing.
     */
    protected boolean assumePersistedDataIsEscaped()
    {
        return JiveGlobals.getBooleanProperty( "jdbcAdminProvider.isEscaped", true );
    }

    @Override
    public synchronized List<JID> getAdmins() {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;

        List<JID> jids = new ArrayList<>();
        try {
            con = getConnection();
            pstmt = con.prepareStatement(getAdminsSQL);
            rs = pstmt.executeQuery();
            while (rs.next()) {
                // OF-1837: When the database does not hold escaped data, escape values before processing them further.
                final String username;
                if (assumePersistedDataIsEscaped()) {
                    username = rs.getString(1);
                } else {
                    username = JID.escapeNode( rs.getString(1) );
                }
                jids.add(new JID(username + "@" + xmppDomain));
            }
            return jids;
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } finally {
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }

    }

    private void changeAdmins(final Connection con, final String sql, final List<JID> admins) throws SQLException {<FILL_FUNCTION_BODY>}

    @Override
    public synchronized void setAdmins(List<JID> newAdmins) {
        if (isReadOnly()) {
            // Reject the operation since the provider is read-only
            throw new UnsupportedOperationException();
        }

        final List<JID> currentAdmins = getAdmins();
        // Get a list of everyone in the new list not in the current list
        final List<JID> adminsToAdd = new ArrayList<>(newAdmins);
        adminsToAdd.removeAll(currentAdmins);
        // Get a list of everyone in the current list not in the new list
        currentAdmins.removeAll(newAdmins);
        try (final Connection con = getConnection()) {
            changeAdmins(con, insertAdminsSQL, adminsToAdd);
            changeAdmins(con, deleteAdminsSQL, currentAdmins);
        } catch (SQLException e) {
            throw new RuntimeException(e);

        }
    }

    @Override
    public boolean isReadOnly() {
        return insertAdminsSQL.isEmpty() || deleteAdminsSQL.isEmpty();
    }

    private Connection getConnection() throws SQLException {
        if (useConnectionProvider) {
            return DbConnectionManager.getConnection();
        }
        return DriverManager.getConnection(connectionString);
    }
}

        if (!admins.isEmpty()) {
            try (final PreparedStatement pstmt = con.prepareStatement(sql)) {
                for (final JID jid : admins) {
                    // OF-1837: When the database does not hold escaped data, our query should use unescaped values in the 'where' clause.
                    final String queryValue = assumePersistedDataIsEscaped() ? jid.getNode() : JID.unescapeNode( jid.getNode() );
                    pstmt.setString(1, queryValue);
                    pstmt.execute();
                }
            }
        }
    
<no_super_class>