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}