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}