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