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