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}