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}