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 ("userPassword".equalsIgnoreCase(mod.getAttribute().getID())) 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 @Override 142 public Type getType() 143 { 144 return Type.MODIFY_ENTRY; 145 } 146 147 @Override 148 public Set<String> getBackends() 149 { 150 return backendSet; 151 } 152 153 @Override 154 public LocalizableMessage getTaskDescription() 155 { 156 return INFO_CTRL_PANEL_MODIFY_ENTRY_TASK_DESCRIPTION.get(oldEntry.getDN()); 157 } 158 159 @Override 160 protected String getCommandLinePath() 161 { 162 return null; 163 } 164 165 @Override 166 protected ArrayList<String> getCommandLineArguments() 167 { 168 return new ArrayList<>(); 169 } 170 171 @Override 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 @Override 194 public boolean regenerateDescriptor() 195 { 196 return false; 197 } 198 199 @Override 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 @Override 220 public void run() 221 { 222 printEquivalentCommandToModify(newEntry.getName(), modifications, 223 useAdminCtx); 224 getProgressDialog().appendProgressHtml( 225 Utilities.getProgressWithPoints( 226 INFO_CTRL_PANEL_MODIFYING_ENTRY.get(oldEntry.getDN()), 227 ColorAndFontConstants.progressFont)); 228 } 229 }); 230 231 ctx.modifyAttributes(Utilities.getJNDIName(oldEntry.getDN()), mods); 232 233 SwingUtilities.invokeLater(new Runnable() 234 { 235 @Override 236 public void run() 237 { 238 getProgressDialog().appendProgressHtml( 239 Utilities.getProgressDone( 240 ColorAndFontConstants.progressFont)); 241 controller.notifyEntryChanged( 242 controller.getNodeInfoFromPath(treePath)); 243 controller.getTree().removeSelectionPath(treePath); 244 controller.getTree().setSelectionPath(treePath); 245 } 246 }); 247 } 248 } 249 else 250 { 251 modifyAndRename(ctx, oldDn, oldEntry, newEntry, modifications); 252 } 253 state = State.FINISHED_SUCCESSFULLY; 254 } 255 catch (Throwable t) 256 { 257 lastException = t; 258 state = State.FINISHED_WITH_ERROR; 259 } 260 } 261 262 @Override 263 public void postOperation() 264 { 265 if (lastException == null 266 && state == State.FINISHED_SUCCESSFULLY 267 && passwordModification != null) 268 { 269 try 270 { 271 Object o = passwordModification.getAttribute().get(); 272 String sPwd; 273 if (o instanceof byte[]) 274 { 275 try 276 { 277 sPwd = new String((byte[])o, "UTF-8"); 278 } 279 catch (Throwable t) 280 { 281 throw new RuntimeException("Unexpected error: "+t, t); 282 } 283 } 284 else 285 { 286 sPwd = String.valueOf(o); 287 } 288 ResetUserPasswordTask newTask = new ResetUserPasswordTask(getInfo(), 289 getProgressDialog(), (BasicNode)treePath.getLastPathComponent(), 290 controller, sPwd.toCharArray()); 291 if (!modifications.isEmpty() || mustRename) 292 { 293 getProgressDialog().appendProgressHtml("<br><br>"); 294 } 295 StatusGenericPanel.launchOperation(newTask, 296 INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUMMARY.get(), 297 INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_SUMMARY.get(), 298 INFO_CTRL_PANEL_RESETTING_USER_PASSWORD_SUCCESSFUL_DETAILS.get(), 299 ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_SUMMARY.get(), 300 ERR_CTRL_PANEL_RESETTING_USER_PASSWORD_ERROR_DETAILS.get(), 301 null, 302 getProgressDialog(), 303 false, 304 getInfo()); 305 getProgressDialog().setVisible(true); 306 } 307 catch (NamingException ne) 308 { 309 // This should not happen 310 throw new RuntimeException("Unexpected exception: "+ne, ne); 311 } 312 } 313 } 314 315 /** 316 * Modifies and renames the entry. 317 * @param ctx the connection to the server. 318 * @param oldDN the oldDN of the entry. 319 * @param originalEntry the original entry. 320 * @param newEntry the new entry. 321 * @param originalMods the original modifications (these are required since 322 * we might want to update them). 323 * @throws CannotRenameException if we cannot perform the modification. 324 * @throws NamingException if an error performing the modification occurs. 325 */ 326 private void modifyAndRename(DirContext ctx, final DN oldDN, 327 CustomSearchResult originalEntry, final Entry newEntry, 328 final ArrayList<ModificationItem> originalMods) 329 throws CannotRenameException, NamingException 330 { 331 RDN oldRDN = oldDN.rdn(); 332 RDN newRDN = newEntry.getName().rdn(); 333 334 if (rdnTypeChanged(oldRDN, newRDN) 335 && userChangedObjectclass(originalMods) 336 /* See if the original entry contains the new naming attribute(s) if it does we will be able 337 to perform the renaming and then the modifications without problem */ 338 && !entryContainsRdnTypes(originalEntry, newRDN)) 339 { 340 throw new CannotRenameException(AdminToolMessages.ERR_CANNOT_MODIFY_OBJECTCLASS_AND_RENAME.get()); 341 } 342 343 SwingUtilities.invokeLater(new Runnable() 344 { 345 @Override 346 public void run() 347 { 348 printEquivalentRenameCommand(oldDN, newEntry.getName(), useAdminCtx); 349 getProgressDialog().appendProgressHtml( 350 Utilities.getProgressWithPoints( 351 INFO_CTRL_PANEL_RENAMING_ENTRY.get(oldDN, newEntry.getName()), 352 ColorAndFontConstants.progressFont)); 353 } 354 }); 355 356 ctx.rename(Utilities.getJNDIName(oldDn.toString()), 357 Utilities.getJNDIName(newEntry.getName().toString())); 358 359 final TreePath[] newPath = {null}; 360 361 SwingUtilities.invokeLater(new Runnable() 362 { 363 @Override 364 public void run() 365 { 366 getProgressDialog().appendProgressHtml( 367 Utilities.getProgressDone(ColorAndFontConstants.progressFont)); 368 getProgressDialog().appendProgressHtml("<br>"); 369 TreePath parentPath = controller.notifyEntryDeleted( 370 controller.getNodeInfoFromPath(treePath)); 371 newPath[0] = controller.notifyEntryAdded( 372 controller.getNodeInfoFromPath(parentPath), 373 newEntry.getName().toString()); 374 } 375 }); 376 377 ModificationItem[] mods = new ModificationItem[originalMods.size()]; 378 originalMods.toArray(mods); 379 if (mods.length > 0) 380 { 381 SwingUtilities.invokeLater(new Runnable() 382 { 383 @Override 384 public void run() 385 { 386 DN dn = newEntry.getName(); 387 printEquivalentCommandToModify(dn, originalMods, useAdminCtx); 388 getProgressDialog().appendProgressHtml( 389 Utilities.getProgressWithPoints( 390 INFO_CTRL_PANEL_MODIFYING_ENTRY.get(dn), 391 ColorAndFontConstants.progressFont)); 392 } 393 }); 394 395 ctx.modifyAttributes(Utilities.getJNDIName(newEntry.getName().toString()), mods); 396 397 SwingUtilities.invokeLater(new Runnable() 398 { 399 @Override 400 public void run() 401 { 402 getProgressDialog().appendProgressHtml( 403 Utilities.getProgressDone(ColorAndFontConstants.progressFont)); 404 if (newPath[0] != null) 405 { 406 controller.getTree().setSelectionPath(newPath[0]); 407 } 408 } 409 }); 410 } 411 } 412 413 private boolean rdnTypeChanged(RDN oldRDN, RDN newRDN) 414 { 415 if (newRDN.size() != oldRDN.size()) 416 { 417 return true; 418 } 419 420 for (AVA ava : newRDN) 421 { 422 if (!find(oldRDN, ava.getAttributeType())) 423 { 424 return true; 425 } 426 } 427 return false; 428 } 429 430 private boolean find(RDN rdn, AttributeType attrType) 431 { 432 for (AVA ava : rdn) 433 { 434 if (attrType.equals(ava.getAttributeType())) 435 { 436 return true; 437 } 438 } 439 return false; 440 } 441 442 private boolean userChangedObjectclass(final ArrayList<ModificationItem> mods) 443 { 444 for (ModificationItem mod : mods) 445 { 446 if (ATTR_OBJECTCLASS.equalsIgnoreCase(mod.getAttribute().getID())) 447 { 448 return true; 449 } 450 } 451 return false; 452 } 453 454 private boolean entryContainsRdnTypes(CustomSearchResult entry, RDN rdn) 455 { 456 for (AVA ava : rdn) 457 { 458 List<Object> values = entry.getAttributeValues(ava.getAttributeName()); 459 if (values.isEmpty()) 460 { 461 return false; 462 } 463 } 464 return true; 465 } 466 467 /** 468 * Gets the modifications to apply between two entries. 469 * @param newEntry the new entry. 470 * @param oldEntry the old entry. 471 * @param info the ControlPanelInfo, used to retrieve the schema for instance. 472 * @return the modifications to apply between two entries. 473 */ 474 public static ArrayList<ModificationItem> getModifications(Entry newEntry, 475 CustomSearchResult oldEntry, ControlPanelInfo info) { 476 ArrayList<ModificationItem> modifications = new ArrayList<>(); 477 Schema schema = info.getServerDescriptor().getSchema(); 478 479 List<org.opends.server.types.Attribute> newAttrs = newEntry.getAttributes(); 480 newAttrs.add(newEntry.getObjectClassAttribute()); 481 for (org.opends.server.types.Attribute attr : newAttrs) 482 { 483 AttributeDescription attrDesc = attr.getAttributeDescription(); 484 String attrName = attrDesc.toString(); 485 if (!ViewEntryPanel.isEditable(attrName, schema)) 486 { 487 continue; 488 } 489 List<ByteString> newValues = new ArrayList<>(); 490 Iterator<ByteString> it = attr.iterator(); 491 while (it.hasNext()) 492 { 493 newValues.add(it.next()); 494 } 495 List<Object> oldValues = oldEntry.getAttributeValues(attrName); 496 497 ByteString rdnValue = null; 498 for (AVA ava : newEntry.getName().rdn()) 499 { 500 if (ava.getAttributeType().equals(attrDesc.getAttributeType())) 501 { 502 rdnValue = ava.getAttributeValue(); 503 } 504 } 505 boolean isAttributeInNewRdn = rdnValue != null; 506 507 /* Check the attributes of the old DN. If we are renaming them they 508 * will be deleted. Check that they are on the new entry but not in 509 * the new RDN. If it is the case we must add them after the renaming. 510 */ 511 ByteString oldRdnValueToAdd = null; 512 /* Check the value in the RDN that will be deleted. If the value was 513 * on the previous RDN but not in the new entry it will be deleted. So 514 * we must avoid to include it as a delete modification in the 515 * modifications. 516 */ 517 ByteString oldRdnValueDeleted = null; 518 RDN oldRDN = DN.valueOf(oldEntry.getDN()).rdn(); 519 for (AVA ava : oldRDN) 520 { 521 if (ava.getAttributeType().equals(attrDesc.getAttributeType())) 522 { 523 ByteString value = ava.getAttributeValue(); 524 if (attr.contains(value)) 525 { 526 if (rdnValue == null || !rdnValue.equals(value)) 527 { 528 oldRdnValueToAdd = value; 529 } 530 } 531 else 532 { 533 oldRdnValueDeleted = value; 534 } 535 break; 536 } 537 } 538 if (oldValues == null) 539 { 540 Set<ByteString> vs = new HashSet<>(newValues); 541 if (rdnValue != null) 542 { 543 vs.remove(rdnValue); 544 } 545 if (!vs.isEmpty()) 546 { 547 modifications.add(new ModificationItem( 548 DirContext.ADD_ATTRIBUTE, 549 createAttribute(attrName, newValues))); 550 } 551 } else { 552 List<ByteString> toDelete = getValuesToDelete(oldValues, newValues); 553 if (oldRdnValueDeleted != null) 554 { 555 toDelete.remove(oldRdnValueDeleted); 556 } 557 List<ByteString> toAdd = getValuesToAdd(oldValues, newValues); 558 if (oldRdnValueToAdd != null) 559 { 560 toAdd.add(oldRdnValueToAdd); 561 } 562 if (toDelete.size() + toAdd.size() >= newValues.size() && 563 !isAttributeInNewRdn) 564 { 565 modifications.add(new ModificationItem( 566 DirContext.REPLACE_ATTRIBUTE, 567 createAttribute(attrName, newValues))); 568 } 569 else 570 { 571 if (!toDelete.isEmpty()) 572 { 573 modifications.add(new ModificationItem( 574 DirContext.REMOVE_ATTRIBUTE, 575 createAttribute(attrName, toDelete))); 576 } 577 if (!toAdd.isEmpty()) 578 { 579 List<ByteString> vs = new ArrayList<>(toAdd); 580 if (rdnValue != null) 581 { 582 vs.remove(rdnValue); 583 } 584 if (!vs.isEmpty()) 585 { 586 modifications.add(new ModificationItem( 587 DirContext.ADD_ATTRIBUTE, 588 createAttribute(attrName, vs))); 589 } 590 } 591 } 592 } 593 } 594 595 /* Check if there are attributes to delete */ 596 for (String attrName : oldEntry.getAttributeNames()) 597 { 598 if (!ViewEntryPanel.isEditable(attrName, schema)) 599 { 600 continue; 601 } 602 List<Object> oldValues = oldEntry.getAttributeValues(attrName); 603 AttributeDescription attrDesc = AttributeDescription.valueOf(attrName); 604 605 List<org.opends.server.types.Attribute> attrs = newEntry.getAttribute(attrDesc.getNameOrOID()); 606 if (!find(attrs, attrName) && !oldValues.isEmpty()) 607 { 608 modifications.add(new ModificationItem( 609 DirContext.REMOVE_ATTRIBUTE, 610 new BasicAttribute(attrName))); 611 } 612 } 613 return modifications; 614 } 615 616 private static boolean find(List<org.opends.server.types.Attribute> attrs, String attrName) 617 { 618 // TODO JNR use Entry.hasAttribute(AttributeDescription) instead? 619 for (org.opends.server.types.Attribute attr : attrs) 620 { 621 if (attr.getAttributeDescription().toString().equalsIgnoreCase(attrName)) 622 { 623 return true; 624 } 625 } 626 return false; 627 } 628 629 /** 630 * Creates a JNDI attribute using an attribute name and a set of values. 631 * @param attrName the attribute name. 632 * @param values the values. 633 * @return a JNDI attribute using an attribute name and a set of values. 634 */ 635 private static Attribute createAttribute(String attrName, List<ByteString> values) { 636 Attribute attribute = new BasicAttribute(attrName); 637 for (ByteString value : values) 638 { 639 attribute.add(value.toByteArray()); 640 } 641 return attribute; 642 } 643 644 /** 645 * Creates a ByteString for an attribute and a value (the one we got using JNDI). 646 * @param value the value found using JNDI. 647 * @return a ByteString object. 648 */ 649 private static ByteString createAttributeValue(Object value) 650 { 651 if (value instanceof String) 652 { 653 return ByteString.valueOfUtf8((String) value); 654 } 655 else if (value instanceof byte[]) 656 { 657 return ByteString.wrap((byte[]) value); 658 } 659 return ByteString.valueOfUtf8(String.valueOf(value)); 660 } 661 662 /** 663 * Returns the set of ByteString that must be deleted. 664 * @param oldValues the old values of the entry. 665 * @param newValues the new values of the entry. 666 * @return the set of ByteString that must be deleted. 667 */ 668 private static List<ByteString> getValuesToDelete(List<Object> oldValues, 669 List<ByteString> newValues) 670 { 671 List<ByteString> valuesToDelete = new ArrayList<>(); 672 for (Object o : oldValues) 673 { 674 ByteString oldValue = createAttributeValue(o); 675 if (!newValues.contains(oldValue)) 676 { 677 valuesToDelete.add(oldValue); 678 } 679 } 680 return valuesToDelete; 681 } 682 683 /** 684 * Returns the set of ByteString that must be added. 685 * @param oldValues the old values of the entry. 686 * @param newValues the new values of the entry. 687 * @return the set of ByteString that must be added. 688 */ 689 private static List<ByteString> getValuesToAdd(List<Object> oldValues, 690 List<ByteString> newValues) 691 { 692 List<ByteString> valuesToAdd = new ArrayList<>(); 693 for (ByteString newValue : newValues) 694 { 695 if (!contains(oldValues, newValue)) 696 { 697 valuesToAdd.add(newValue); 698 } 699 } 700 return valuesToAdd; 701 } 702 703 private static boolean contains(List<Object> oldValues, ByteString newValue) 704 { 705 for (Object o : oldValues) 706 { 707 if (createAttributeValue(o).equals(newValue)) 708 { 709 return true; 710 } 711 } 712 return false; 713 } 714}