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 2014-2015 ForgeRock AS 026 */ 027package org.opends.guitools.controlpanel.task; 028 029import static org.opends.messages.AdminToolMessages.*; 030import static org.opends.server.config.ConfigConstants.*; 031 032import java.util.ArrayList; 033import java.util.Collection; 034import java.util.HashSet; 035import java.util.Iterator; 036import java.util.List; 037import java.util.Set; 038import java.util.TreeSet; 039 040import javax.naming.NamingException; 041import javax.naming.directory.Attribute; 042import javax.naming.directory.BasicAttribute; 043import javax.naming.directory.DirContext; 044import javax.naming.directory.ModificationItem; 045import javax.naming.ldap.InitialLdapContext; 046import javax.swing.SwingUtilities; 047import javax.swing.tree.TreePath; 048 049import org.forgerock.i18n.LocalizableMessage; 050import org.forgerock.opendj.ldap.ByteString; 051import org.opends.guitools.controlpanel.browser.BrowserController; 052import org.opends.guitools.controlpanel.datamodel.BackendDescriptor; 053import org.opends.guitools.controlpanel.datamodel.BaseDNDescriptor; 054import org.opends.guitools.controlpanel.datamodel.CannotRenameException; 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.StatusGenericPanel; 060import org.opends.guitools.controlpanel.ui.ViewEntryPanel; 061import org.opends.guitools.controlpanel.ui.nodes.BasicNode; 062import org.opends.guitools.controlpanel.util.Utilities; 063import org.opends.messages.AdminToolMessages; 064import org.opends.server.core.DirectoryServer; 065import org.opends.server.types.*; 066 067/** The task that is called when we must modify an entry. */ 068public class ModifyEntryTask extends Task 069{ 070 private Set<String> backendSet; 071 private boolean mustRename; 072 private boolean hasModifications; 073 private CustomSearchResult oldEntry; 074 private DN oldDn; 075 private ArrayList<ModificationItem> modifications; 076 private ModificationItem passwordModification; 077 private Entry newEntry; 078 private BrowserController controller; 079 private TreePath treePath; 080 private boolean useAdminCtx; 081 082 /** 083 * Constructor of the task. 084 * @param info the control panel information. 085 * @param dlg the progress dialog where the task progress will be displayed. 086 * @param newEntry the entry containing the new values. 087 * @param oldEntry the old entry as we retrieved using JNDI. 088 * @param controller the BrowserController. 089 * @param path the TreePath corresponding to the node in the tree that we 090 * want to modify. 091 */ 092 public ModifyEntryTask(ControlPanelInfo info, ProgressDialog dlg, 093 Entry newEntry, CustomSearchResult oldEntry, 094 BrowserController controller, TreePath path) 095 { 096 super(info, dlg); 097 backendSet = new HashSet<>(); 098 this.oldEntry = oldEntry; 099 this.newEntry = newEntry; 100 this.controller = controller; 101 this.treePath = path; 102 DN newDn = newEntry.getName(); 103 try 104 { 105 oldDn = DN.valueOf(oldEntry.getDN()); 106 for (BackendDescriptor backend : info.getServerDescriptor().getBackends()) 107 { 108 for (BaseDNDescriptor baseDN : backend.getBaseDns()) 109 { 110 if (newDn.isDescendantOf(baseDN.getDn()) || 111 oldDn.isDescendantOf(baseDN.getDn())) 112 { 113 backendSet.add(backend.getBackendID()); 114 } 115 } 116 } 117 mustRename = !newDn.equals(oldDn); 118 } 119 catch (OpenDsException ode) 120 { 121 throw new RuntimeException("Could not parse DN: " + oldEntry.getDN(), ode); 122 } 123 modifications = getModifications(newEntry, oldEntry, getInfo()); 124 // Find password modifications 125 for (ModificationItem mod : modifications) 126 { 127 if (mod.getAttribute().getID().equalsIgnoreCase("userPassword")) 128 { 129 passwordModification = mod; 130 break; 131 } 132 } 133 if (passwordModification != null) 134 { 135 modifications.remove(passwordModification); 136 } 137 hasModifications = !modifications.isEmpty() 138 || !oldDn.equals(newEntry.getName()) 139 || passwordModification != null; 140 } 141 142 /** 143 * Tells whether there actually modifications on the entry. 144 * @return <CODE>true</CODE> if there are modifications and <CODE>false</CODE> 145 * otherwise. 146 */ 147 public boolean hasModifications() 148 { 149 return hasModifications; 150 } 151 152 /** {@inheritDoc} */ 153 public Type getType() 154 { 155 return Type.MODIFY_ENTRY; 156 } 157 158 /** {@inheritDoc} */ 159 public Set<String> getBackends() 160 { 161 return backendSet; 162 } 163 164 /** {@inheritDoc} */ 165 public LocalizableMessage getTaskDescription() 166 { 167 return INFO_CTRL_PANEL_MODIFY_ENTRY_TASK_DESCRIPTION.get(oldEntry.getDN()); 168 } 169 170 /** {@inheritDoc} */ 171 protected String getCommandLinePath() 172 { 173 return null; 174 } 175 176 /** {@inheritDoc} */ 177 protected ArrayList<String> getCommandLineArguments() 178 { 179 return new ArrayList<>(); 180 } 181 182 /** {@inheritDoc} */ 183 public boolean canLaunch(Task taskToBeLaunched, 184 Collection<LocalizableMessage> incompatibilityReasons) 185 { 186 if (!isServerRunning() 187 && state == State.RUNNING 188 && runningOnSameServer(taskToBeLaunched)) 189 { 190 // All the operations are incompatible if they apply to this 191 // backend for safety. This is a short operation so the limitation 192 // has not a lot of impact. 193 Set<String> backends = new TreeSet<>(taskToBeLaunched.getBackends()); 194 backends.retainAll(getBackends()); 195 if (!backends.isEmpty()) 196 { 197 incompatibilityReasons.add(getIncompatibilityMessage(this, taskToBeLaunched)); 198 return false; 199 } 200 } 201 return true; 202 } 203 204 /** {@inheritDoc} */ 205 public boolean regenerateDescriptor() 206 { 207 return false; 208 } 209 210 /** {@inheritDoc} */ 211 public void runTask() 212 { 213 state = State.RUNNING; 214 lastException = null; 215 216 try 217 { 218 BasicNode node = (BasicNode)treePath.getLastPathComponent(); 219 InitialLdapContext ctx = controller.findConnectionForDisplayedEntry(node); 220 useAdminCtx = controller.isConfigurationNode(node); 221 if (!mustRename) 222 { 223 if (!modifications.isEmpty()) { 224 ModificationItem[] mods = 225 new ModificationItem[modifications.size()]; 226 modifications.toArray(mods); 227 228 SwingUtilities.invokeLater(new Runnable() 229 { 230 public void run() 231 { 232 printEquivalentCommandToModify(newEntry.getName(), modifications, 233 useAdminCtx); 234 getProgressDialog().appendProgressHtml( 235 Utilities.getProgressWithPoints( 236 INFO_CTRL_PANEL_MODIFYING_ENTRY.get(oldEntry.getDN()), 237 ColorAndFontConstants.progressFont)); 238 } 239 }); 240 241 ctx.modifyAttributes(Utilities.getJNDIName(oldEntry.getDN()), mods); 242 243 SwingUtilities.invokeLater(new Runnable() 244 { 245 public void run() 246 { 247 getProgressDialog().appendProgressHtml( 248 Utilities.getProgressDone( 249 ColorAndFontConstants.progressFont)); 250 controller.notifyEntryChanged( 251 controller.getNodeInfoFromPath(treePath)); 252 controller.getTree().removeSelectionPath(treePath); 253 controller.getTree().setSelectionPath(treePath); 254 } 255 }); 256 } 257 } 258 else 259 { 260 modifyAndRename(ctx, oldDn, oldEntry, newEntry, modifications); 261 } 262 state = State.FINISHED_SUCCESSFULLY; 263 } 264 catch (Throwable t) 265 { 266 lastException = t; 267 state = State.FINISHED_WITH_ERROR; 268 } 269 } 270 271 /** {@inheritDoc} */ 272 public void postOperation() 273 { 274 if (lastException == null 275 && state == State.FINISHED_SUCCESSFULLY 276 && passwordModification != null) 277 { 278 try 279 { 280 Object o = passwordModification.getAttribute().get(); 281 String sPwd; 282 if (o instanceof byte[]) 283 { 284 try 285 { 286 sPwd = new String((byte[])o, "UTF-8"); 287 } 288 catch (Throwable t) 289 { 290 throw new RuntimeException("Unexpected error: "+t, t); 291 } 292 } 293 else 294 { 295 sPwd = String.valueOf(o); 296 } 297 ResetUserPasswordTask newTask = new ResetUserPasswordTask(getInfo(), 298 getProgressDialog(), (BasicNode)treePath.getLastPathComponent(), 299 controller, sPwd.toCharArray()); 300 if (!modifications.isEmpty() || mustRename) 301 { 302 getProgressDialog().appendProgressHtml("<br><br>"); 303 } 304 StatusGenericPanel.launchOperation(newTask, 305 INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUMMARY.get(), 306 INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_SUMMARY.get(), 307 INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_DETAILS.get(), 308 ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_SUMMARY.get(), 309 ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_DETAILS.get(), 310 null, 311 getProgressDialog(), 312 false, 313 getInfo()); 314 getProgressDialog().setVisible(true); 315 } 316 catch (NamingException ne) 317 { 318 // This should not happen 319 throw new RuntimeException("Unexpected exception: "+ne, ne); 320 } 321 } 322 } 323 324 /** 325 * Modifies and renames the entry. 326 * @param ctx the connection to the server. 327 * @param oldDN the oldDN of the entry. 328 * @param originalEntry the original entry. 329 * @param newEntry the new entry. 330 * @param originalMods the original modifications (these are required since 331 * we might want to update them). 332 * @throws CannotRenameException if we cannot perform the modification. 333 * @throws NamingException if an error performing the modification occurs. 334 */ 335 private void modifyAndRename(DirContext ctx, final DN oldDN, 336 CustomSearchResult originalEntry, final Entry newEntry, 337 final ArrayList<ModificationItem> originalMods) 338 throws CannotRenameException, NamingException 339 { 340 RDN oldRDN = oldDN.rdn(); 341 RDN newRDN = newEntry.getName().rdn(); 342 343 if (rdnTypeChanged(oldRDN, newRDN) 344 && userChangedObjectclass(originalMods) 345 /* See if the original entry contains the new naming attribute(s) if it does we will be able 346 to perform the renaming and then the modifications without problem */ 347 && !entryContainsRdnTypes(originalEntry, newRDN)) 348 { 349 throw new CannotRenameException(AdminToolMessages.ERR_CANNOT_MODIFY_OBJECTCLASS_AND_RENAME.get()); 350 } 351 352 SwingUtilities.invokeLater(new Runnable() 353 { 354 public void run() 355 { 356 printEquivalentRenameCommand(oldDN, newEntry.getName(), useAdminCtx); 357 getProgressDialog().appendProgressHtml( 358 Utilities.getProgressWithPoints( 359 INFO_CTRL_PANEL_RENAMING_ENTRY.get(oldDN, newEntry.getName()), 360 ColorAndFontConstants.progressFont)); 361 } 362 }); 363 364 ctx.rename(Utilities.getJNDIName(oldDn.toString()), 365 Utilities.getJNDIName(newEntry.getName().toString())); 366 367 final TreePath[] newPath = {null}; 368 369 SwingUtilities.invokeLater(new Runnable() 370 { 371 public void run() 372 { 373 getProgressDialog().appendProgressHtml( 374 Utilities.getProgressDone(ColorAndFontConstants.progressFont)); 375 getProgressDialog().appendProgressHtml("<br>"); 376 TreePath parentPath = controller.notifyEntryDeleted( 377 controller.getNodeInfoFromPath(treePath)); 378 newPath[0] = controller.notifyEntryAdded( 379 controller.getNodeInfoFromPath(parentPath), 380 newEntry.getName().toString()); 381 } 382 }); 383 384 385 ModificationItem[] mods = new ModificationItem[originalMods.size()]; 386 originalMods.toArray(mods); 387 if (mods.length > 0) 388 { 389 SwingUtilities.invokeLater(new Runnable() 390 { 391 public void run() 392 { 393 DN dn = newEntry.getName(); 394 printEquivalentCommandToModify(dn, originalMods, useAdminCtx); 395 getProgressDialog().appendProgressHtml( 396 Utilities.getProgressWithPoints( 397 INFO_CTRL_PANEL_MODIFYING_ENTRY.get(dn), 398 ColorAndFontConstants.progressFont)); 399 } 400 }); 401 402 ctx.modifyAttributes(Utilities.getJNDIName(newEntry.getName().toString()), mods); 403 404 SwingUtilities.invokeLater(new Runnable() 405 { 406 public void run() 407 { 408 getProgressDialog().appendProgressHtml( 409 Utilities.getProgressDone(ColorAndFontConstants.progressFont)); 410 if (newPath[0] != null) 411 { 412 controller.getTree().setSelectionPath(newPath[0]); 413 } 414 } 415 }); 416 } 417 } 418 419 private boolean rdnTypeChanged(RDN oldRDN, RDN newRDN) 420 { 421 if (newRDN.getNumValues() != oldRDN.getNumValues()) 422 { 423 return true; 424 } 425 426 for (int i = 0; i < newRDN.getNumValues(); i++) 427 { 428 if (!find(oldRDN, newRDN.getAttributeName(i))) 429 { 430 return true; 431 } 432 } 433 return false; 434 } 435 436 private boolean find(RDN rdn, String attrName) 437 { 438 for (int j = 0; j < rdn.getNumValues(); j++) 439 { 440 if (attrName.equalsIgnoreCase(rdn.getAttributeName(j))) 441 { 442 return true; 443 } 444 } 445 return false; 446 } 447 448 private boolean userChangedObjectclass(final ArrayList<ModificationItem> mods) 449 { 450 for (ModificationItem mod : mods) 451 { 452 if (ATTR_OBJECTCLASS.equalsIgnoreCase(mod.getAttribute().getID())) 453 { 454 return true; 455 } 456 } 457 return false; 458 } 459 460 private boolean entryContainsRdnTypes(CustomSearchResult entry, RDN rdn) 461 { 462 for (int i = 0; i < rdn.getNumValues(); i++) 463 { 464 List<Object> values = entry.getAttributeValues(rdn.getAttributeName(i)); 465 if (values.isEmpty()) 466 { 467 return false; 468 } 469 } 470 return true; 471 } 472 473 /** 474 * Gets the modifications to apply between two entries. 475 * @param newEntry the new entry. 476 * @param oldEntry the old entry. 477 * @param info the ControlPanelInfo, used to retrieve the schema for instance. 478 * @return the modifications to apply between two entries. 479 */ 480 public static ArrayList<ModificationItem> getModifications(Entry newEntry, 481 CustomSearchResult oldEntry, ControlPanelInfo info) { 482 ArrayList<ModificationItem> modifications = new ArrayList<>(); 483 Schema schema = info.getServerDescriptor().getSchema(); 484 485 List<org.opends.server.types.Attribute> newAttrs = newEntry.getAttributes(); 486 newAttrs.add(newEntry.getObjectClassAttribute()); 487 for (org.opends.server.types.Attribute attr : newAttrs) 488 { 489 String attrName = attr.getNameWithOptions(); 490 if (!ViewEntryPanel.isEditable(attrName, schema)) 491 { 492 continue; 493 } 494 AttributeType attrType = schema.getAttributeType(attr.getName().toLowerCase()); 495 if (attrType == null) 496 { 497 attrType = DirectoryServer.getDefaultAttributeType(attr.getName().toLowerCase()); 498 } 499 List<ByteString> newValues = new ArrayList<>(); 500 Iterator<ByteString> it = attr.iterator(); 501 while (it.hasNext()) 502 { 503 newValues.add(it.next()); 504 } 505 List<Object> oldValues = oldEntry.getAttributeValues(attrName); 506 507 boolean isAttributeInNewRdn = false; 508 ByteString rdnValue = null; 509 RDN rdn = newEntry.getName().rdn(); 510 for (int i=0; i<rdn.getNumValues() && !isAttributeInNewRdn; i++) 511 { 512 isAttributeInNewRdn = 513 rdn.getAttributeName(i).equalsIgnoreCase(attrName); 514 if (isAttributeInNewRdn) 515 { 516 rdnValue = rdn.getAttributeValue(i); 517 } 518 } 519 520 /* Check the attributes of the old DN. If we are renaming them they 521 * will be deleted. Check that they are on the new entry but not in 522 * the new RDN. If it is the case we must add them after the renaming. 523 */ 524 ByteString oldRdnValueToAdd = null; 525 /* Check the value in the RDN that will be deleted. If the value was 526 * on the previous RDN but not in the new entry it will be deleted. So 527 * we must avoid to include it as a delete modification in the 528 * modifications. 529 */ 530 ByteString oldRdnValueDeleted = null; 531 RDN oldRDN = null; 532 try 533 { 534 oldRDN = DN.valueOf(oldEntry.getDN()).rdn(); 535 } 536 catch (DirectoryException de) 537 { 538 throw new RuntimeException("Unexpected error parsing DN: "+ 539 oldEntry.getDN(), de); 540 } 541 for (int i=0; i<oldRDN.getNumValues(); i++) 542 { 543 if (oldRDN.getAttributeName(i).equalsIgnoreCase(attrName)) 544 { 545 ByteString value = oldRDN.getAttributeValue(i); 546 if (attr.contains(value)) 547 { 548 if (rdnValue == null || !rdnValue.equals(value)) 549 { 550 oldRdnValueToAdd = value; 551 } 552 } 553 else 554 { 555 oldRdnValueDeleted = value; 556 } 557 break; 558 } 559 } 560 if (oldValues == null) 561 { 562 Set<ByteString> vs = new HashSet<>(newValues); 563 if (rdnValue != null) 564 { 565 vs.remove(rdnValue); 566 } 567 if (!vs.isEmpty()) 568 { 569 modifications.add(new ModificationItem( 570 DirContext.ADD_ATTRIBUTE, 571 createAttribute(attrName, newValues))); 572 } 573 } else { 574 List<ByteString> toDelete = getValuesToDelete(oldValues, newValues); 575 if (oldRdnValueDeleted != null) 576 { 577 toDelete.remove(oldRdnValueDeleted); 578 } 579 List<ByteString> toAdd = getValuesToAdd(oldValues, newValues); 580 if (oldRdnValueToAdd != null) 581 { 582 toAdd.add(oldRdnValueToAdd); 583 } 584 if (toDelete.size() + toAdd.size() >= newValues.size() && 585 !isAttributeInNewRdn) 586 { 587 modifications.add(new ModificationItem( 588 DirContext.REPLACE_ATTRIBUTE, 589 createAttribute(attrName, newValues))); 590 } 591 else 592 { 593 if (!toDelete.isEmpty()) 594 { 595 modifications.add(new ModificationItem( 596 DirContext.REMOVE_ATTRIBUTE, 597 createAttribute(attrName, toDelete))); 598 } 599 if (!toAdd.isEmpty()) 600 { 601 List<ByteString> vs = new ArrayList<>(toAdd); 602 if (rdnValue != null) 603 { 604 vs.remove(rdnValue); 605 } 606 if (!vs.isEmpty()) 607 { 608 modifications.add(new ModificationItem( 609 DirContext.ADD_ATTRIBUTE, 610 createAttribute(attrName, vs))); 611 } 612 } 613 } 614 } 615 } 616 617 /* Check if there are attributes to delete */ 618 for (String attrName : oldEntry.getAttributeNames()) 619 { 620 if (!ViewEntryPanel.isEditable(attrName, schema)) 621 { 622 continue; 623 } 624 List<Object> oldValues = oldEntry.getAttributeValues(attrName); 625 String attrNoOptions = 626 Utilities.getAttributeNameWithoutOptions(attrName).toLowerCase(); 627 628 List<org.opends.server.types.Attribute> attrs = newEntry.getAttribute(attrNoOptions); 629 if (!find(attrs, attrName) && !oldValues.isEmpty()) 630 { 631 modifications.add(new ModificationItem( 632 DirContext.REMOVE_ATTRIBUTE, 633 new BasicAttribute(attrName))); 634 } 635 } 636 return modifications; 637 } 638 639 private static boolean find(List<org.opends.server.types.Attribute> attrs, String attrName) 640 { 641 if (attrs != null) 642 { 643 for (org.opends.server.types.Attribute attr : attrs) 644 { 645 if (attr.getNameWithOptions().equalsIgnoreCase(attrName)) 646 { 647 return true; 648 } 649 } 650 } 651 return false; 652 } 653 654 /** 655 * Creates a JNDI attribute using an attribute name and a set of values. 656 * @param attrName the attribute name. 657 * @param values the values. 658 * @return a JNDI attribute using an attribute name and a set of values. 659 */ 660 private static Attribute createAttribute(String attrName, List<ByteString> values) { 661 Attribute attribute = new BasicAttribute(attrName); 662 for (ByteString value : values) 663 { 664 attribute.add(value.toByteArray()); 665 } 666 return attribute; 667 } 668 669 /** 670 * Creates a ByteString for an attribute and a value (the one we got using JNDI). 671 * @param value the value found using JNDI. 672 * @return a ByteString object. 673 */ 674 private static ByteString createAttributeValue(Object value) 675 { 676 if (value instanceof String) 677 { 678 return ByteString.valueOf((String) value); 679 } 680 else if (value instanceof byte[]) 681 { 682 return ByteString.wrap((byte[]) value); 683 } 684 return ByteString.valueOf(String.valueOf(value)); 685 } 686 687 /** 688 * Returns the set of ByteString that must be deleted. 689 * @param oldValues the old values of the entry. 690 * @param newValues the new values of the entry. 691 * @return the set of ByteString that must be deleted. 692 */ 693 private static List<ByteString> getValuesToDelete(List<Object> oldValues, 694 List<ByteString> newValues) 695 { 696 List<ByteString> valuesToDelete = new ArrayList<>(); 697 for (Object o : oldValues) 698 { 699 ByteString oldValue = createAttributeValue(o); 700 if (!newValues.contains(oldValue)) 701 { 702 valuesToDelete.add(oldValue); 703 } 704 } 705 return valuesToDelete; 706 } 707 708 /** 709 * Returns the set of ByteString that must be added. 710 * @param oldValues the old values of the entry. 711 * @param newValues the new values of the entry. 712 * @return the set of ByteString that must be added. 713 */ 714 private static List<ByteString> getValuesToAdd(List<Object> oldValues, 715 List<ByteString> newValues) 716 { 717 List<ByteString> valuesToAdd = new ArrayList<>(); 718 for (ByteString newValue : newValues) 719 { 720 if (!contains(oldValues, newValue)) 721 { 722 valuesToAdd.add(newValue); 723 } 724 } 725 return valuesToAdd; 726 } 727 728 private static boolean contains(List<Object> oldValues, ByteString newValue) 729 { 730 for (Object o : oldValues) 731 { 732 if (createAttributeValue(o).equals(newValue)) 733 { 734 return true; 735 } 736 } 737 return false; 738 } 739}