001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2013-2016 ForgeRock AS.
016 */
017
018package org.opends.guitools.controlpanel.task;
019
020import static org.opends.messages.AdminToolMessages.*;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Set;
027import java.util.SortedSet;
028import java.util.TreeSet;
029
030import javax.naming.NameNotFoundException;
031import javax.naming.NamingEnumeration;
032import javax.naming.NamingException;
033import javax.naming.directory.SearchControls;
034import javax.naming.directory.SearchResult;
035import javax.naming.ldap.BasicControl;
036import javax.naming.ldap.Control;
037import javax.naming.ldap.InitialLdapContext;
038import javax.swing.SwingUtilities;
039import javax.swing.tree.TreePath;
040
041import org.opends.admin.ads.util.ConnectionUtils;
042import org.opends.guitools.controlpanel.browser.BrowserController;
043import org.opends.guitools.controlpanel.datamodel.BackendDescriptor;
044import org.opends.guitools.controlpanel.datamodel.BaseDNDescriptor;
045import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
046import org.opends.guitools.controlpanel.datamodel.CustomSearchResult;
047import org.opends.guitools.controlpanel.ui.ColorAndFontConstants;
048import org.opends.guitools.controlpanel.ui.ProgressDialog;
049import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
050import org.opends.guitools.controlpanel.ui.nodes.BrowserNodeInfo;
051import org.opends.guitools.controlpanel.util.Utilities;
052import org.forgerock.i18n.LocalizableMessage;
053import org.opends.server.schema.SchemaConstants;
054import org.forgerock.opendj.ldap.DN;
055import org.opends.server.types.DirectoryException;
056import org.opends.server.util.ServerConstants;
057
058/** The task that is launched when an entry must be deleted. */
059public class DeleteEntryTask extends Task
060{
061  private Set<String> backendSet;
062  private DN lastDn;
063  private int nDeleted;
064  private int nToDelete = -1;
065  private BrowserController controller;
066  private TreePath[] paths;
067  private long lastProgressTime;
068  private boolean equivalentCommandWithControlPrinted;
069  private boolean equivalentCommandWithoutControlPrinted;
070  private boolean useAdminCtx;
071
072  /**
073   * Constructor of the task.
074   * @param info the control panel information.
075   * @param dlg the progress dialog where the task progress will be displayed.
076   * @param paths the tree paths of the entries that must be deleted.
077   * @param controller the Browser Controller.
078   */
079  public DeleteEntryTask(ControlPanelInfo info, ProgressDialog dlg,
080      TreePath[] paths, BrowserController controller)
081  {
082    super(info, dlg);
083    backendSet = new HashSet<>();
084    this.controller = controller;
085    this.paths = paths;
086    SortedSet<DN> entries = new TreeSet<>();
087    boolean canPrecalculateNumberOfEntries = true;
088    nToDelete = paths.length;
089    for (TreePath path : paths)
090    {
091      BasicNode node = (BasicNode)path.getLastPathComponent();
092      entries.add(DN.valueOf(node.getDN()));
093    }
094    for (BackendDescriptor backend : info.getServerDescriptor().getBackends())
095    {
096      for (BaseDNDescriptor baseDN : backend.getBaseDns())
097      {
098        for (DN dn : entries)
099        {
100          if (dn.isSubordinateOrEqualTo(baseDN.getDn()))
101          {
102            backendSet.add(backend.getBackendID());
103            break;
104          }
105        }
106      }
107    }
108    if (!canPrecalculateNumberOfEntries)
109    {
110      nToDelete = -1;
111    }
112  }
113
114  @Override
115  public Type getType()
116  {
117    return Type.DELETE_ENTRY;
118  }
119
120  @Override
121  public Set<String> getBackends()
122  {
123    return backendSet;
124  }
125
126  @Override
127  public LocalizableMessage getTaskDescription()
128  {
129    return INFO_CTRL_PANEL_DELETE_ENTRY_TASK_DESCRIPTION.get();
130  }
131
132  @Override
133  protected String getCommandLinePath()
134  {
135    return null;
136  }
137
138  @Override
139  protected ArrayList<String> getCommandLineArguments()
140  {
141    return new ArrayList<>();
142  }
143
144  @Override
145  public boolean canLaunch(Task taskToBeLaunched,
146      Collection<LocalizableMessage> incompatibilityReasons)
147  {
148    if (!isServerRunning()
149        && state == State.RUNNING
150        && runningOnSameServer(taskToBeLaunched))
151    {
152      // All the operations are incompatible if they apply to this
153      // backend for safety.
154      Set<String> backends = new TreeSet<>(taskToBeLaunched.getBackends());
155      backends.retainAll(getBackends());
156      if (!backends.isEmpty())
157      {
158        incompatibilityReasons.add(getIncompatibilityMessage(this, taskToBeLaunched));
159        return false;
160      }
161    }
162    return true;
163  }
164
165  @Override
166  public boolean regenerateDescriptor()
167  {
168    return false;
169  }
170
171  @Override
172  public void runTask()
173  {
174    state = State.RUNNING;
175    lastException = null;
176
177    ArrayList<DN> alreadyDeleted = new ArrayList<>();
178    ArrayList<BrowserNodeInfo> toNotify = new ArrayList<>();
179    try
180    {
181      for (TreePath path : paths)
182      {
183        BasicNode node = (BasicNode)path.getLastPathComponent();
184        try
185        {
186          DN dn = DN.valueOf(node.getDN());
187          boolean isDnDeleted = false;
188          for (DN deletedDn : alreadyDeleted)
189          {
190            if (dn.isSubordinateOrEqualTo(deletedDn))
191            {
192              isDnDeleted = true;
193              break;
194            }
195          }
196          if (!isDnDeleted)
197          {
198            InitialLdapContext ctx =
199              controller.findConnectionForDisplayedEntry(node);
200            useAdminCtx = controller.isConfigurationNode(node);
201            if (node.hasSubOrdinates())
202            {
203              deleteSubtreeWithControl(ctx, dn, path, toNotify);
204            }
205            else
206            {
207              deleteSubtreeRecursively(ctx, dn, path, toNotify);
208            }
209            alreadyDeleted.add(dn);
210          }
211        }
212        catch (DirectoryException de)
213        {
214          throw new RuntimeException("Unexpected error parsing dn: "+
215              node.getDN(), de);
216        }
217      }
218      if (!toNotify.isEmpty())
219      {
220        final List<BrowserNodeInfo> fToNotify = new ArrayList<>(toNotify);
221        toNotify.clear();
222        SwingUtilities.invokeLater(new Runnable()
223        {
224          @Override
225          public void run()
226          {
227            notifyEntriesDeleted(fToNotify);
228          }
229        });
230      }
231      state = State.FINISHED_SUCCESSFULLY;
232    }
233    catch (Throwable t)
234    {
235      lastException = t;
236      state = State.FINISHED_WITH_ERROR;
237    }
238    if (nDeleted > 1)
239    {
240      getProgressDialog().appendProgressHtml(Utilities.applyFont(
241          "<br>"+INFO_CTRL_PANEL_ENTRIES_DELETED.get(nDeleted),
242          ColorAndFontConstants.progressFont));
243    }
244  }
245
246  /**
247   * Notifies that some entries have been deleted.  This will basically update
248   * the browser controller so that the tree reflects the changes that have
249   * been made.
250   * @param deletedNodes the nodes that have been deleted.
251   */
252  private void notifyEntriesDeleted(Collection<BrowserNodeInfo> deletedNodes)
253  {
254    TreePath pathToSelect = null;
255    for (BrowserNodeInfo nodeInfo : deletedNodes)
256    {
257      TreePath parentPath = controller.notifyEntryDeleted(nodeInfo);
258      if (pathToSelect != null)
259      {
260        if (parentPath.getPathCount() < pathToSelect.getPathCount())
261        {
262          pathToSelect = parentPath;
263        }
264      }
265      else
266      {
267        pathToSelect = parentPath;
268      }
269    }
270    if (pathToSelect != null)
271    {
272      TreePath selectedPath = controller.getTree().getSelectionPath();
273      if (selectedPath == null)
274      {
275        controller.getTree().setSelectionPath(pathToSelect);
276      }
277      else if (!selectedPath.equals(pathToSelect) &&
278          pathToSelect.getPathCount() < selectedPath.getPathCount())
279      {
280        controller.getTree().setSelectionPath(pathToSelect);
281      }
282    }
283  }
284
285  private void deleteSubtreeRecursively(InitialLdapContext ctx, DN dnToRemove,
286      TreePath path, ArrayList<BrowserNodeInfo> toNotify)
287  throws NamingException, DirectoryException
288  {
289    lastDn = dnToRemove;
290
291    long t = System.currentTimeMillis();
292    boolean canDelete = nToDelete > 0 && nToDelete > nDeleted;
293    boolean displayProgress =
294      canDelete && ((nDeleted % 20) == 0 || t - lastProgressTime > 5000);
295
296    if (displayProgress)
297    {
298      // Only display the first entry equivalent command-line.
299      SwingUtilities.invokeLater(new Runnable()
300      {
301        @Override
302        public void run()
303        {
304          if (!equivalentCommandWithoutControlPrinted)
305          {
306            printEquivalentCommandToDelete(lastDn, false);
307            equivalentCommandWithoutControlPrinted = true;
308          }
309          getProgressDialog().setSummary(
310              LocalizableMessage.raw(
311                  Utilities.applyFont(
312                      INFO_CTRL_PANEL_DELETING_ENTRY_SUMMARY.get(lastDn),
313                      ColorAndFontConstants.defaultFont)));
314        }
315      });
316    }
317
318    try
319    {
320      SearchControls ctls = new SearchControls();
321      ctls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
322      String filter =
323        "(|(objectClass=*)(objectclass=ldapsubentry))";
324      ctls.setReturningAttributes(
325          new String[] { SchemaConstants.NO_ATTRIBUTES });
326      NamingEnumeration<SearchResult> entryDNs =
327        ctx.search(Utilities.getJNDIName(dnToRemove.toString()), filter, ctls);
328
329      DN entryDNFound = dnToRemove;
330      try
331      {
332        while (entryDNs.hasMore())
333        {
334          SearchResult sr = entryDNs.next();
335          if (!sr.getName().equals(""))
336          {
337            CustomSearchResult res =
338              new CustomSearchResult(sr, dnToRemove.toString());
339            entryDNFound = DN.valueOf(res.getDN());
340            deleteSubtreeRecursively(ctx, entryDNFound, null, toNotify);
341          }
342        }
343      }
344      finally
345      {
346        entryDNs.close();
347      }
348
349    } catch (NameNotFoundException nnfe) {
350      // The entry is not there: it has been removed
351    }
352
353    try
354    {
355      ctx.destroySubcontext(Utilities.getJNDIName(dnToRemove.toString()));
356      if (path != null)
357      {
358        toNotify.add(controller.getNodeInfoFromPath(path));
359      }
360      nDeleted ++;
361      if (displayProgress)
362      {
363        lastProgressTime = t;
364        final Collection<BrowserNodeInfo> fToNotify;
365        if (!toNotify.isEmpty())
366        {
367          fToNotify = new ArrayList<>(toNotify);
368          toNotify.clear();
369        }
370        else
371        {
372          fToNotify = null;
373        }
374        SwingUtilities.invokeLater(new Runnable()
375        {
376          @Override
377          public void run()
378          {
379            getProgressDialog().getProgressBar().setIndeterminate(false);
380            getProgressDialog().getProgressBar().setValue(
381                (100 * nDeleted) / nToDelete);
382            if (fToNotify != null)
383            {
384              notifyEntriesDeleted(fToNotify);
385            }
386          }
387        });
388      }
389    } catch (NameNotFoundException nnfe)
390    {
391      // The entry is not there: it has been removed
392    }
393  }
394
395  private void deleteSubtreeWithControl(InitialLdapContext ctx, DN dn,
396      TreePath path, ArrayList<BrowserNodeInfo> toNotify)
397  throws NamingException
398  {
399    lastDn = dn;
400    long t = System.currentTimeMillis();
401    //  Only display the first entry equivalent command-line.
402    SwingUtilities.invokeLater(new Runnable()
403    {
404      @Override
405      public void run()
406      {
407        if (!equivalentCommandWithControlPrinted)
408        {
409          printEquivalentCommandToDelete(lastDn, true);
410          equivalentCommandWithControlPrinted = true;
411        }
412        getProgressDialog().setSummary(
413            LocalizableMessage.raw(
414                Utilities.applyFont(
415                    INFO_CTRL_PANEL_DELETING_ENTRY_SUMMARY.get(lastDn),
416                    ColorAndFontConstants.defaultFont)));
417      }
418    });
419    //  Use a copy of the dir context since we are using an specific
420    // control to delete the subtree and this can cause
421    // synchronization problems when the tree is refreshed.
422    InitialLdapContext ctx1 = null;
423    try
424    {
425      ctx1 = ConnectionUtils.cloneInitialLdapContext(ctx,
426          getInfo().getConnectTimeout(),
427          getInfo().getTrustManager(), null);
428      Control[] ctls = {
429          new BasicControl(ServerConstants.OID_SUBTREE_DELETE_CONTROL)};
430      ctx1.setRequestControls(ctls);
431      ctx1.destroySubcontext(Utilities.getJNDIName(dn.toString()));
432    }
433    finally
434    {
435      try
436      {
437        ctx1.close();
438      }
439      catch (Throwable th)
440      {
441      }
442    }
443    nDeleted ++;
444    lastProgressTime = t;
445    if (path != null)
446    {
447      toNotify.add(controller.getNodeInfoFromPath(path));
448    }
449    final Collection<BrowserNodeInfo> fToNotify;
450    if (!toNotify.isEmpty())
451    {
452      fToNotify = new ArrayList<>(toNotify);
453      toNotify.clear();
454    }
455    else
456    {
457      fToNotify = null;
458    }
459    SwingUtilities.invokeLater(new Runnable()
460    {
461      @Override
462      public void run()
463      {
464        getProgressDialog().getProgressBar().setIndeterminate(false);
465        getProgressDialog().getProgressBar().setValue(
466            (100 * nDeleted) / nToDelete);
467        if (fToNotify != null)
468        {
469          notifyEntriesDeleted(fToNotify);
470        }
471      }
472    });
473  }
474
475  /**
476   * Prints in the progress dialog the equivalent command-line to delete a
477   * subtree.
478   * @param dn the DN of the subtree to be deleted.
479   * @param usingControl whether we must include the control or not.
480   */
481  private void printEquivalentCommandToDelete(DN dn, boolean usingControl)
482  {
483    ArrayList<String> args = new ArrayList<>(getObfuscatedCommandLineArguments(
484        getConnectionCommandLineArguments(useAdminCtx, true)));
485    args.add(getNoPropertiesFileArgument());
486    if (usingControl)
487    {
488      args.add("-J");
489      args.add(ServerConstants.OID_SUBTREE_DELETE_CONTROL);
490    }
491    args.add(dn.toString());
492    printEquivalentCommandLine(getCommandLinePath("ldapdelete"),
493        args,
494        INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_DELETE_ENTRY.get(dn));
495  }
496}