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}