/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2012-2016 ForgeRock AS. */ package org.opends.guitools.controlpanel.browser; import static org.forgerock.opendj.ldap.LdapException.*; import static org.forgerock.opendj.ldap.ResultCode.*; import static org.forgerock.opendj.ldap.SearchScope.*; import static org.forgerock.opendj.ldap.requests.Requests.*; import static org.opends.admin.ads.util.ConnectionUtils.*; import static org.opends.messages.AdminToolMessages.*; import static org.opends.server.schema.SchemaConstants.*; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.naming.InterruptedNamingException; import javax.naming.NameNotFoundException; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.SizeLimitExceededException; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.LdapName; import javax.swing.SwingUtilities; import javax.swing.tree.TreeNode; import org.forgerock.i18n.LocalizedIllegalArgumentException; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.LdapException; import org.forgerock.opendj.ldap.RDN; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.responses.SearchResultEntry; import org.forgerock.opendj.ldif.ConnectionEntryReader; import org.opends.admin.ads.util.ConnectionWrapper; import org.opends.guitools.controlpanel.ui.nodes.BasicNode; import org.opends.messages.AdminToolMessages; import org.opends.server.types.DirectoryException; import org.opends.server.types.HostPort; import org.opends.server.types.LDAPURL; import org.opends.server.types.OpenDsException; /** * The class that is in charge of doing the LDAP searches required to update a * node: search the local entry, detect if it has children, retrieve the * attributes required to render the node, etc. */ public class NodeRefresher extends AbstractNodeTask { /** The enumeration containing all the states the refresher can have. */ public enum State { /** The refresher is queued, but not started. */ QUEUED, /** The refresher is reading the local entry. */ READING_LOCAL_ENTRY, /** The refresher is solving a referral. */ SOLVING_REFERRAL, /** The refresher is detecting whether the entry has children or not. */ DETECTING_CHILDREN, /** The refresher is searching for the children of the entry. */ SEARCHING_CHILDREN, /** The refresher is finished. */ FINISHED, /** The refresher is cancelled. */ CANCELLED, /** The refresher has been interrupted. */ INTERRUPTED, /** The refresher has failed. */ FAILED } private final BrowserController controller; private State state; private final boolean recursive; private SearchResultEntry localEntry; private SearchResultEntry remoteEntry; private LDAPURL remoteUrl; private boolean isLeafNode; private final List childEntries = new ArrayList<>(); private final boolean differential; private Exception exception; private Object exceptionArg; /** * The constructor of the refresher object. * @param node the node on the tree to be updated. * @param ctlr the BrowserController. * @param localEntry the local entry corresponding to the node. * @param recursive whether this task is recursive or not (children must be searched). */ NodeRefresher(BasicNode node, BrowserController ctlr, SearchResultEntry localEntry, boolean recursive) { super(node); controller = ctlr; state = State.QUEUED; this.recursive = recursive; this.localEntry = localEntry; differential = false; } /** * Returns the local entry the refresher is handling. * @return the local entry the refresher is handling. */ public SearchResultEntry getLocalEntry() { return localEntry; } /** * Returns the remote entry for the node. It will be {@code null} if * the entry is not a referral. * @return the remote entry for the node. */ public SearchResultEntry getRemoteEntry() { return remoteEntry; } /** * Returns the URL of the remote entry. It will be {@code null} if * the entry is not a referral. * @return the URL of the remote entry. */ public LDAPURL getRemoteUrl() { return remoteUrl; } /** * Tells whether the node is a leaf or not. * @return {@code true} if the node is a leaf and {@code false} otherwise. */ public boolean isLeafNode() { return isLeafNode; } /** * Returns the child entries of the node. * @return the child entries of the node. */ public List getChildEntries() { return childEntries; } /** * Returns whether this refresher object is working on differential mode or not. * @return {@code true} if the refresher is working on differential * mode and {@code false} otherwise. */ public boolean isDifferential() { return differential; } /** * Returns the exception that occurred during the processing. It returns * {@code null} if no exception occurred. * @return the exception that occurred during the processing. */ public Exception getException() { return exception; } /** * Returns the argument of the exception that occurred during the processing. * It returns {@code null} if no exception occurred or if the exception * has no arguments. * @return the argument exception that occurred during the processing. */ public Object getExceptionArg() { return exceptionArg; } /** * Returns the displayed entry in the browser. This depends on the * visualization options in the BrowserController. * @return the remote entry if the entry is a referral and the * BrowserController is following referrals and the local entry otherwise. */ public SearchResultEntry getDisplayedEntry() { if (controller.getFollowReferrals() && remoteEntry != null) { return remoteEntry; } else { return localEntry; } } /** * Returns the LDAP URL of the displayed entry in the browser. This depends * on the visualization options in the BrowserController. * @return the remote entry LDAP URL if the entry is a referral and the * BrowserController is following referrals and the local entry LDAP URL * otherwise. */ public LDAPURL getDisplayedUrl() { if (controller.getFollowReferrals() && remoteUrl != null) { return remoteUrl; } else { return controller.findUrlForLocalEntry(getNode()); } } /** * Returns whether the refresh is over or not. * * @return {@code true} if the refresh is over and {@code false} otherwise. */ public boolean isInFinalState() { return state == State.FINISHED || state == State.CANCELLED || state == State.FAILED || state == State.INTERRUPTED; } /** The method that actually does the refresh. */ @Override public void run() { final BasicNode node = getNode(); try { boolean checkExpand = false; if (localEntry == null) { changeStateTo(State.READING_LOCAL_ENTRY); runReadLocalEntry(); } if (!isInFinalState()) { if (controller.getFollowReferrals() && isReferralEntry(localEntry)) { changeStateTo(State.SOLVING_REFERRAL); runSolveReferral(); } if (node.isLeaf()) { changeStateTo(State.DETECTING_CHILDREN); runDetectChildren(); } if (controller.nodeIsExpanded(node) && recursive) { changeStateTo(State.SEARCHING_CHILDREN); runSearchChildren(); /* If the node is not expanded, we have to refresh its children when we expand it */ } else if (recursive && (!node.isLeaf() || !isLeafNode)) { node.setRefreshNeededOnExpansion(true); checkExpand = true; } changeStateTo(State.FINISHED); if (checkExpand && mustAutomaticallyExpand(node)) { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { controller.expandNode(node); } }); } } } catch (NamingException ne) { exception = ne; exceptionArg = null; } catch(SearchAbandonException x) { exception = x.getException(); exceptionArg = x.getArg(); try { changeStateTo(x.getState()); } catch(SearchAbandonException xx) { // We've done all what we can... } } } /** * Tells whether a custom filter is being used (specified by the user in the * browser dialog) or not. * @return {@code true} if a custom filter is being used and {@code false} otherwise. */ private boolean useCustomFilter() { return controller.getFilter() != null && !BrowserController.ALL_OBJECTS_FILTER.equals(controller.getFilter()); } /** * Performs the search in the case the user specified a custom filter. * * @param node * the parent node we perform the search from. * @param conn * the connection to be used. * @throws IOException * if a problem occurred. */ private void searchForCustomFilter(BasicNode node, ConnectionWrapper conn) throws IOException { SearchRequest request = newSearchRequest(node.getDN(), WHOLE_SUBTREE, controller.getFilter(), NO_ATTRIBUTES) .setSizeLimit(1); try (ConnectionEntryReader s = conn.getConnection().search(request)) { if (!s.hasNext()) { throw newLdapException(NO_SUCH_OBJECT, "Entry " + node.getDN() + " does not verify filter "+controller.getFilter()); } while (s.hasNext()) { s.readEntry(); } } catch (LdapException e) { if (e.getResult().getResultCode() == ResultCode.SIZE_LIMIT_EXCEEDED) { // We are just searching for an entry, but if there is more than one // this exception will be thrown. We call sr.hasMore after the // first entry has been retrieved to avoid sending a systematic // abandon when closing the s NamingEnumeration. // See CR 6976906. } else { throw e; } } } /** * Performs the search in the case the user specified a custom filter. * @param dn the parent DN we perform the search from. * @param conn the connection to be used. * @throws NamingException if a problem occurred. */ private void searchForCustomFilter(String dn, ConnectionWrapper conn) throws NamingException { SearchControls ctls = controller.getBasicSearchControls(); ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); ctls.setReturningAttributes(new String[]{}); ctls.setCountLimit(1); NamingEnumeration s = conn.getLdapContext().search(new LdapName(dn), controller.getFilter(), ctls); try { if (!s.hasMore()) { throw new NameNotFoundException("Entry "+dn+ " does not verify filter "+controller.getFilter()); } while (s.hasMore()) { s.next(); } } catch (SizeLimitExceededException slme) { // We are just searching for an entry, but if there is more than one // this exception will be thrown. We call sr.hasMore after the // first entry has been retrieved to avoid sending a systematic // abandon when closing the s NamingEnumeration. // See CR 6976906. } finally { s.close(); } } /** Read the local entry associated to the current node. */ private void runReadLocalEntry() throws SearchAbandonException { BasicNode node = getNode(); ConnectionWrapper conn = null; try { conn = controller.findConnectionForLocalEntry(node); if (conn != null) { if (useCustomFilter()) { // Check that the entry verifies the filter searchForCustomFilter(node, conn); } String filter = controller.getObjectSearchFilter(); SearchRequest request = newSearchRequest(node.getDN(), BASE_OBJECT, filter, controller.getAttrsForRedSearch()) .setSizeLimit(controller.getMaxChildren()); localEntry = conn.getConnection().searchSingleEntry(request); localEntry.setName(node.getDN()); if (localEntry == null) { /* Not enough rights to read the entry or the entry simply does not exist */ throw newLdapException(ResultCode.NO_SUCH_OBJECT, "Can't find entry: " + node.getDN()); } throwAbandonIfNeeded(null); } else { changeStateTo(State.FINISHED); } } catch(IOException | NamingException x) { throwAbandonIfNeeded(x); } finally { if (conn != null) { controller.releaseLDAPConnection(conn); } } } /** * Solve the referral associated to the current node. * This routine assumes that node.getReferral() is non null * and that BrowserController.getFollowReferrals() == true. * It also protect the browser against looping referrals by * limiting the number of hops. * @throws SearchAbandonException if the hop count limit for referrals has * been exceeded. * @throws NamingException if an error occurred searching the entry. */ private void runSolveReferral() throws SearchAbandonException, NamingException { int hopCount = 0; String[] referral = getNode().getReferral(); while (referral != null && hopCount < 10) { readRemoteEntry(referral); referral = BrowserController.getReferral(remoteEntry); hopCount++; } if (referral != null) { throwAbandonIfNeeded(new ReferralLimitExceededException( AdminToolMessages.ERR_REFERRAL_LIMIT_EXCEEDED.get(hopCount))); } } /** * Searches for the remote entry. * @param referral the referral list to be used to search the remote entry. * @throws SearchAbandonException if an error occurs. */ private void readRemoteEntry(String[] referral) throws SearchAbandonException { LDAPConnectionPool connectionPool = controller.getConnectionPool(); LDAPURL url = null; SearchResultEntry entry = null; String remoteDn = null; Exception lastException = null; Object lastExceptionArg = null; int i = 0; while (i < referral.length && entry == null) { ConnectionWrapper conn = null; try { url = LDAPURL.decode(referral[i], false); if (url.getHost() == null) { // Use the local server connection. ConnectionWrapper userConn = controller.getUserDataConnection(); HostPort hostPort = userConn.getHostPort(); url.setHost(hostPort.getHost()); url.setPort(hostPort.getPort()); url.setScheme(userConn.isLdaps() ? "ldaps" : "ldap"); } conn = connectionPool.getConnection(url); remoteDn = url.getRawBaseDN(); if (remoteDn == null || "".equals(remoteDn)) { /* The referral has not a target DN specified: we have to use the DN of the entry that contains the referral... */ if (remoteEntry != null) { remoteDn = remoteEntry.getName().toString(); } else { remoteDn = localEntry.getName().toString(); } /* We have to recreate the url including the target DN we are using */ url = new LDAPURL(url.getScheme(), url.getHost(), url.getPort(), remoteDn, url.getAttributes(), url.getScope(), url.getRawFilter(), url.getExtensions()); } if (useCustomFilter() && url.getScope() == SearchScope.BASE_OBJECT) { // Check that the entry verifies the filter searchForCustomFilter(remoteDn, conn); } String filter = getJNDIFilter(url); SearchRequest request = newSearchRequest(remoteDn, url.getScope(), filter, controller.getAttrsForBlackSearch()) .setSizeLimit(controller.getMaxChildren()); try (ConnectionEntryReader sr = conn.getConnection().search(request)) { boolean found = false; while (sr.hasNext()) { entry = sr.readEntry(); if (entry.getName().isRootDN()) { entry.setName(remoteDn); } found = true; } if (!found) { throw new NameNotFoundException(); } } catch (LdapException e) { if (e.getResult().getResultCode() == ResultCode.SIZE_LIMIT_EXCEEDED) { // We are just searching for an entry, but if there is more than one // this exception will be thrown. We call sr.hasMore after the // first entry has been retrieved to avoid sending a systematic // abandon when closing the sr NamingEnumeration. // See CR 6976906. } else { throw e; } } throwAbandonIfNeeded(null); } catch (InterruptedNamingException x) { throwAbandonIfNeeded(x); } catch (NamingException | IOException | LocalizedIllegalArgumentException | DirectoryException x) { lastException = x; lastExceptionArg = referral[i]; } finally { if (conn != null) { connectionPool.releaseConnection(conn); } } i = i + 1; } if (entry == null) { throw new SearchAbandonException(State.FAILED, lastException, lastExceptionArg); } if (url.getScope() != SearchScope.BASE_OBJECT) { // The URL is to be transformed: the code assumes that the URL points // to the remote entry. url = new LDAPURL(url.getScheme(), url.getHost(), url.getPort(), entry.getName(), url.getAttributes(), SearchScope.BASE_OBJECT, null, url.getExtensions()); } checkLoopInReferral(url, referral[i-1]); remoteUrl = url; remoteEntry = entry; } /** * Tells whether the provided node must be automatically expanded or not. * This is used when the user provides a custom filter, in this case we * expand automatically the tree. * @param node the node to analyze. * @return {@code true} if the node must be expanded and {@code false} otherwise. */ private boolean mustAutomaticallyExpand(BasicNode node) { boolean mustAutomaticallyExpand = false; if (controller.isAutomaticExpand()) { // Limit the number of expansion levels to 3 int nLevels = 0; TreeNode parent = node; while (parent != null) { nLevels ++; parent = parent.getParent(); } mustAutomaticallyExpand = nLevels <= 4; } return mustAutomaticallyExpand; } /** * Detects whether the entries has children or not. * @throws SearchAbandonException if the search was abandoned. * @throws NamingException if an error during the search occurred. */ private void runDetectChildren() throws SearchAbandonException, NamingException { if (controller.isShowContainerOnly() || !isNumSubOrdinatesUsable()) { runDetectChildrenManually(); } else { SearchResultEntry entry = getDisplayedEntry(); isLeafNode = !BrowserController.getHasSubOrdinates(entry); } } /** * Detects whether the entry has children by performing a search using the * entry as base DN. * @throws SearchAbandonException if there is an error. */ private void runDetectChildrenManually() throws SearchAbandonException { BasicNode parentNode = getNode(); ConnectionWrapper conn = null; try { // We set the search constraints so that only one entry is returned. // It's enough to know if the entry has children or not. // Send an LDAP search conn = controller.findConnectionForDisplayedEntry(parentNode); SearchRequest request = newSearchRequest( controller.findBaseDNForChildEntries(parentNode), useCustomFilter() ? WHOLE_SUBTREE : BASE_OBJECT, controller.getChildSearchFilter(), NO_ATTRIBUTES) .setSizeLimit(1); try (ConnectionEntryReader searchResults = conn.getConnection().search(request)) { throwAbandonIfNeeded(null); // Check if parentNode has children isLeafNode = !searchResults.hasNext(); } } catch (LdapException e) { if (e.getResult().getResultCode().equals(ResultCode.SIZE_LIMIT_EXCEEDED)) { // We are just searching for an entry, but if there is more than one // this exception will be thrown. We call sr.hasMore after the // first entry has been retrieved to avoid sending a systematic // abandon when closing the searchResults NamingEnumeration. // See CR 6976906. } else { throwAbandonIfNeeded(e); } } catch (NamingException x) { throwAbandonIfNeeded(x); } finally { if (conn != null) { controller.releaseLDAPConnection(conn); } } } /** * NUMSUBORDINATE HACK * numsubordinates is not usable if the displayed entry * is listed in in the hacker. * Note: *usable* means *usable for detecting children presence*. */ private boolean isNumSubOrdinatesUsable() throws NamingException { SearchResultEntry entry = getDisplayedEntry(); boolean hasSubOrdinates = BrowserController.getHasSubOrdinates(entry); if (!hasSubOrdinates) { LDAPURL url = getDisplayedUrl(); return !controller.getNumSubordinateHacker().contains(url); } // Other values are usable return true; } /** * Searches for the children. * @throws SearchAbandonException if an error occurs. */ private void runSearchChildren() throws SearchAbandonException { ConnectionWrapper conn = null; BasicNode parentNode = getNode(); parentNode.setSizeLimitReached(false); try { // Send an LDAP search conn = controller.findConnectionForDisplayedEntry(parentNode); String parentDn = controller.findBaseDNForChildEntries(parentNode); int parentComponents; try { DN dn = DN.valueOf(parentDn); parentComponents = dn.size(); } catch (Throwable t) { throw new RuntimeException("Error decoding dn: "+parentDn+" . "+t, t); } SearchScope scope = useCustomFilter() ? WHOLE_SUBTREE : SINGLE_LEVEL; SearchRequest request = newSearchRequest(parentDn, scope, controller.getChildSearchFilter(), controller.getAttrsForRedSearch()) .setSizeLimit(controller.getMaxChildren()); try (ConnectionEntryReader entries = conn.getConnection().search(request)) { while (entries.hasNext()) { SearchResultEntry r = entries.readEntry(); if (r.getName().isRootDN()) { continue; } boolean add = false; if (useCustomFilter()) { // Check that is an immediate child: use a faster method by just // comparing the number of components. final DN dn = r.getName(); add = dn.size() == parentComponents + 1; if (!add) { // Is not a direct child. Check if the parent has been added, // if it is the case, do not add the parent. If is not the case, // search for the parent and add it. RDN[] rdns = new RDN[parentComponents + 1]; final DN parentToAddDN = dn.parent(dn.size() - rdns.length); boolean mustAddParent = mustAddParent(parentToAddDN) && mustAddParent2(parentToAddDN); if (mustAddParent) { SearchResultEntry parentResult = searchManuallyEntry(conn, parentToAddDN.toString()); childEntries.add(parentResult); } } } else { add = true; } if (add) { childEntries.add(r); // Time to time we update the display if (childEntries.size() >= 20) { changeStateTo(State.SEARCHING_CHILDREN); childEntries.clear(); } } throwAbandonIfNeeded(null); } } } catch (LdapException e) { if (e.getResult().getResultCode() == ResultCode.SIZE_LIMIT_EXCEEDED) { parentNode.setSizeLimitReached(true); } else { throwAbandonIfNeeded(e); } } catch (NamingException | IOException e) { throwAbandonIfNeeded(e); } finally { if (conn != null) { controller.releaseLDAPConnection(conn); } } } private boolean mustAddParent2(final DN parentToAddDN) { final boolean resultValue[] = {true}; // Check the children added to the tree try { SwingUtilities.invokeAndWait(new Runnable() { @Override public void run() { for (int i=0; i