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