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}