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/**
059 * The task that is launched when an entry must be deleted.
060 */
061public class DeleteEntryTask extends Task
062{
063  private Set<String> backendSet;
064  private DN lastDn;
065  private int nDeleted;
066  private int nToDelete = -1;
067  private BrowserController controller;
068  private TreePath[] paths;
069  private long lastProgressTime;
070  private boolean equivalentCommandWithControlPrinted;
071  private boolean equivalentCommandWithoutControlPrinted;
072  private boolean useAdminCtx;
073
074  /**
075   * Constructor of the task.
076   * @param info the control panel information.
077   * @param dlg the progress dialog where the task progress will be displayed.
078   * @param paths the tree paths of the entries that must be deleted.
079   * @param controller the Browser Controller.
080   */
081  public DeleteEntryTask(ControlPanelInfo info, ProgressDialog dlg,
082      TreePath[] paths, BrowserController controller)
083  {
084    super(info, dlg);
085    backendSet = new HashSet<>();
086    this.controller = controller;
087    this.paths = paths;
088    SortedSet<DN> entries = new TreeSet<>();
089    boolean canPrecalculateNumberOfEntries = true;
090    nToDelete = paths.length;
091    for (TreePath path : paths)
092    {
093      BasicNode node = (BasicNode)path.getLastPathComponent();
094      entries.add(DN.valueOf(node.getDN()));
095    }
096    for (BackendDescriptor backend : info.getServerDescriptor().getBackends())
097    {
098      for (BaseDNDescriptor baseDN : backend.getBaseDns())
099      {
100        for (DN dn : entries)
101        {
102          if (dn.isSubordinateOrEqualTo(baseDN.getDn()))
103          {
104            backendSet.add(backend.getBackendID());
105            break;
106          }
107        }
108      }
109    }
110    if (!canPrecalculateNumberOfEntries)
111    {
112      nToDelete = -1;
113    }
114  }
115
116  /** {@inheritDoc} */
117  public Type getType()
118  {
119    return Type.DELETE_ENTRY;
120  }
121
122  /** {@inheritDoc} */
123  public Set<String> getBackends()
124  {
125    return backendSet;
126  }
127
128  /** {@inheritDoc} */
129  public LocalizableMessage getTaskDescription()
130  {
131    return INFO_CTRL_PANEL_DELETE_ENTRY_TASK_DESCRIPTION.get();
132  }
133
134  /** {@inheritDoc} */
135  protected String getCommandLinePath()
136  {
137    return null;
138  }
139
140  /** {@inheritDoc} */
141  protected ArrayList<String> getCommandLineArguments()
142  {
143    return new ArrayList<>();
144  }
145
146  /** {@inheritDoc} */
147  public boolean canLaunch(Task taskToBeLaunched,
148      Collection<LocalizableMessage> incompatibilityReasons)
149  {
150    if (!isServerRunning()
151        && state == State.RUNNING
152        && runningOnSameServer(taskToBeLaunched))
153    {
154      // All the operations are incompatible if they apply to this
155      // backend for safety.
156      Set<String> backends = new TreeSet<>(taskToBeLaunched.getBackends());
157      backends.retainAll(getBackends());
158      if (!backends.isEmpty())
159      {
160        incompatibilityReasons.add(getIncompatibilityMessage(this, taskToBeLaunched));
161        return false;
162      }
163    }
164    return true;
165  }
166
167  /** {@inheritDoc} */
168  public boolean regenerateDescriptor()
169  {
170    return false;
171  }
172
173  /** {@inheritDoc} */
174  public void runTask()
175  {
176    state = State.RUNNING;
177    lastException = null;
178
179    ArrayList<DN> alreadyDeleted = new ArrayList<>();
180    ArrayList<BrowserNodeInfo> toNotify = new ArrayList<>();
181    try
182    {
183      for (TreePath path : paths)
184      {
185        BasicNode node = (BasicNode)path.getLastPathComponent();
186        try
187        {
188          DN dn = DN.valueOf(node.getDN());
189          boolean isDnDeleted = false;
190          for (DN deletedDn : alreadyDeleted)
191          {
192            if (dn.isSubordinateOrEqualTo(deletedDn))
193            {
194              isDnDeleted = true;
195              break;
196            }
197          }
198          if (!isDnDeleted)
199          {
200            InitialLdapContext ctx =
201              controller.findConnectionForDisplayedEntry(node);
202            useAdminCtx = controller.isConfigurationNode(node);
203            if (node.hasSubOrdinates())
204            {
205              deleteSubtreeWithControl(ctx, dn, path, toNotify);
206            }
207            else
208            {
209              deleteSubtreeRecursively(ctx, dn, path, toNotify);
210            }
211            alreadyDeleted.add(dn);
212          }
213        }
214        catch (DirectoryException de)
215        {
216          throw new RuntimeException("Unexpected error parsing dn: "+
217              node.getDN(), de);
218        }
219      }
220      if (!toNotify.isEmpty())
221      {
222        final List<BrowserNodeInfo> fToNotify = new ArrayList<>(toNotify);
223        toNotify.clear();
224        SwingUtilities.invokeLater(new Runnable()
225        {
226          public void run()
227          {
228            notifyEntriesDeleted(fToNotify);
229          }
230        });
231      }
232      state = State.FINISHED_SUCCESSFULLY;
233    }
234    catch (Throwable t)
235    {
236      lastException = t;
237      state = State.FINISHED_WITH_ERROR;
238    }
239    if (nDeleted > 1)
240    {
241      getProgressDialog().appendProgressHtml(Utilities.applyFont(
242          "<br>"+INFO_CTRL_PANEL_ENTRIES_DELETED.get(nDeleted),
243          ColorAndFontConstants.progressFont));
244    }
245  }
246
247  /**
248   * Notifies that some entries have been deleted.  This will basically update
249   * the browser controller so that the tree reflects the changes that have
250   * been made.
251   * @param deletedNodes the nodes that have been deleted.
252   */
253  private void notifyEntriesDeleted(Collection<BrowserNodeInfo> deletedNodes)
254  {
255    TreePath pathToSelect = null;
256    for (BrowserNodeInfo nodeInfo : deletedNodes)
257    {
258      TreePath parentPath = controller.notifyEntryDeleted(nodeInfo);
259      if (pathToSelect != null)
260      {
261        if (parentPath.getPathCount() < pathToSelect.getPathCount())
262        {
263          pathToSelect = parentPath;
264        }
265      }
266      else
267      {
268        pathToSelect = parentPath;
269      }
270    }
271    if (pathToSelect != null)
272    {
273      TreePath selectedPath = controller.getTree().getSelectionPath();
274      if (selectedPath == null)
275      {
276        controller.getTree().setSelectionPath(pathToSelect);
277      }
278      else if (!selectedPath.equals(pathToSelect) &&
279          pathToSelect.getPathCount() < selectedPath.getPathCount())
280      {
281        controller.getTree().setSelectionPath(pathToSelect);
282      }
283    }
284  }
285
286  private void deleteSubtreeRecursively(InitialLdapContext ctx, DN dnToRemove,
287      TreePath path, ArrayList<BrowserNodeInfo> toNotify)
288  throws NamingException, DirectoryException
289  {
290    lastDn = dnToRemove;
291
292    long t = System.currentTimeMillis();
293    boolean canDelete = nToDelete > 0 && nToDelete > nDeleted;
294    boolean displayProgress =
295      canDelete && ((nDeleted % 20) == 0 || t - lastProgressTime > 5000);
296
297    if (displayProgress)
298    {
299      // Only display the first entry equivalent command-line.
300      SwingUtilities.invokeLater(new Runnable()
301      {
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          public void run()
377          {
378            getProgressDialog().getProgressBar().setIndeterminate(false);
379            getProgressDialog().getProgressBar().setValue(
380                (100 * nDeleted) / nToDelete);
381            if (fToNotify != null)
382            {
383              notifyEntriesDeleted(fToNotify);
384            }
385          }
386        });
387      }
388    } catch (NameNotFoundException nnfe)
389    {
390      // The entry is not there: it has been removed
391    }
392  }
393
394  private void deleteSubtreeWithControl(InitialLdapContext ctx, DN dn,
395      TreePath path, ArrayList<BrowserNodeInfo> toNotify)
396  throws NamingException
397  {
398    lastDn = dn;
399    long t = System.currentTimeMillis();
400    //  Only display the first entry equivalent command-line.
401    SwingUtilities.invokeLater(new Runnable()
402    {
403      public void run()
404      {
405        if (!equivalentCommandWithControlPrinted)
406        {
407          printEquivalentCommandToDelete(lastDn, true);
408          equivalentCommandWithControlPrinted = true;
409        }
410        getProgressDialog().setSummary(
411            LocalizableMessage.raw(
412                Utilities.applyFont(
413                    INFO_CTRL_PANEL_DELETING_ENTRY_SUMMARY.get(lastDn),
414                    ColorAndFontConstants.defaultFont)));
415      }
416    });
417    //  Use a copy of the dir context since we are using an specific
418    // control to delete the subtree and this can cause
419    // synchronization problems when the tree is refreshed.
420    InitialLdapContext ctx1 = null;
421    try
422    {
423      ctx1 = ConnectionUtils.cloneInitialLdapContext(ctx,
424          getInfo().getConnectTimeout(),
425          getInfo().getTrustManager(), null);
426      Control[] ctls = {
427          new BasicControl(ServerConstants.OID_SUBTREE_DELETE_CONTROL)};
428      ctx1.setRequestControls(ctls);
429      ctx1.destroySubcontext(Utilities.getJNDIName(dn.toString()));
430    }
431    finally
432    {
433      try
434      {
435        ctx1.close();
436      }
437      catch (Throwable th)
438      {
439      }
440    }
441    nDeleted ++;
442    lastProgressTime = t;
443    if (path != null)
444    {
445      toNotify.add(controller.getNodeInfoFromPath(path));
446    }
447    final Collection<BrowserNodeInfo> fToNotify;
448    if (!toNotify.isEmpty())
449    {
450      fToNotify = new ArrayList<>(toNotify);
451      toNotify.clear();
452    }
453    else
454    {
455      fToNotify = null;
456    }
457    SwingUtilities.invokeLater(new Runnable()
458    {
459      public void run()
460      {
461        getProgressDialog().getProgressBar().setIndeterminate(false);
462        getProgressDialog().getProgressBar().setValue(
463            (100 * nDeleted) / nToDelete);
464        if (fToNotify != null)
465        {
466          notifyEntriesDeleted(fToNotify);
467        }
468      }
469    });
470  }
471
472  /**
473   * Prints in the progress dialog the equivalent command-line to delete a
474   * subtree.
475   * @param dn the DN of the subtree to be deleted.
476   * @param usingControl whether we must include the control or not.
477   */
478  private void printEquivalentCommandToDelete(DN dn, boolean usingControl)
479  {
480    ArrayList<String> args = new ArrayList<>(getObfuscatedCommandLineArguments(
481        getConnectionCommandLineArguments(useAdminCtx, true)));
482    args.add(getNoPropertiesFileArgument());
483    if (usingControl)
484    {
485      args.add("-J");
486      args.add(ServerConstants.OID_SUBTREE_DELETE_CONTROL);
487    }
488    args.add(dn.toString());
489    printEquivalentCommandLine(getCommandLinePath("ldapdelete"),
490        args,
491        INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_DELETE_ENTRY.get(dn));
492  }
493}