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-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.guitools.controlpanel.task;
018
019import static com.forgerock.opendj.cli.Utils.*;
020import static com.forgerock.opendj.util.OperatingSystem.*;
021
022import static org.opends.messages.AdminToolMessages.*;
023
024import java.io.File;
025import java.util.ArrayList;
026import java.util.Collection;
027import java.util.List;
028import java.util.Map;
029import java.util.Objects;
030import java.util.Set;
031
032import javax.naming.NamingException;
033import javax.naming.directory.Attribute;
034import javax.naming.directory.DirContext;
035import javax.naming.directory.ModificationItem;
036import javax.naming.ldap.InitialLdapContext;
037
038import org.forgerock.i18n.LocalizableMessage;
039import org.forgerock.opendj.ldap.ByteString;
040import org.opends.admin.ads.util.ConnectionUtils;
041import org.opends.guitools.controlpanel.datamodel.ControlPanelInfo;
042import org.opends.guitools.controlpanel.datamodel.ServerDescriptor;
043import org.opends.guitools.controlpanel.event.ConfigurationElementCreatedEvent;
044import org.opends.guitools.controlpanel.event.ConfigurationElementCreatedListener;
045import org.opends.guitools.controlpanel.event.PrintStreamListener;
046import org.opends.guitools.controlpanel.ui.ColorAndFontConstants;
047import org.opends.guitools.controlpanel.ui.ProgressDialog;
048import org.opends.guitools.controlpanel.util.ApplicationPrintStream;
049import org.opends.guitools.controlpanel.util.ConfigReader;
050import org.opends.guitools.controlpanel.util.ProcessReader;
051import org.opends.guitools.controlpanel.util.Utilities;
052import org.opends.quicksetup.Installation;
053import org.opends.quicksetup.UserData;
054import org.forgerock.opendj.ldap.DN;
055import org.opends.server.types.Schema;
056import org.opends.server.util.Base64;
057import org.opends.server.util.SetupUtils;
058
059import com.forgerock.opendj.cli.CommandBuilder;
060
061/**
062 * The class used to define a number of common methods and mechanisms for the
063 * tasks that are run in the Control Panel.
064 */
065public abstract class Task
066{
067  private static String localHostName = UserData.getDefaultHostName();
068  private String binDir;
069  /**
070   * The different task types.
071   */
072  public enum Type
073  {
074    /**
075     * New Base DN creation.
076     */
077    NEW_BASEDN,
078    /**
079     * New index creation.
080     */
081    NEW_INDEX,
082    /**
083     * Modification of indexes.
084     */
085    MODIFY_INDEX,
086    /**
087     * Deletion of indexes.
088     */
089    DELETE_INDEX,
090    /**
091     * Creation of VLV indexes.
092     */
093    NEW_VLV_INDEX,
094    /**
095     * Modification of VLV indexes.
096     */
097    MODIFY_VLV_INDEX,
098    /**
099     * Deletion of VLV indexes.
100     */
101    DELETE_VLV_INDEX,
102    /**
103     * Import of an LDIF file.
104     */
105    IMPORT_LDIF,
106    /**
107     * Export of an LDIF file.
108     */
109    EXPORT_LDIF,
110    /**
111     * Backup.
112     */
113    BACKUP,
114    /**
115     * Restore.
116     */
117    RESTORE,
118    /**
119     * Verification of indexes.
120     */
121    VERIFY_INDEXES,
122    /**
123     * Rebuild of indexes.
124     */
125    REBUILD_INDEXES,
126    /**
127     * Enabling of Windows Service.
128     */
129    ENABLE_WINDOWS_SERVICE,
130    /**
131     * Disabling of Windows Service.
132     */
133    DISABLE_WINDOWS_SERVICE,
134    /**
135     * Starting the server.
136     */
137    START_SERVER,
138    /**
139     * Stopping the server.
140     */
141    STOP_SERVER,
142    /**
143     * Updating the java settings for the different command-lines.
144     */
145    JAVA_SETTINGS_UPDATE,
146    /**
147     * Creating a new element in the schema.
148     */
149    NEW_SCHEMA_ELEMENT,
150    /**
151     * Deleting an schema element.
152     */
153    DELETE_SCHEMA_ELEMENT,
154    /**
155     * Modify an schema element.
156     */
157    MODIFY_SCHEMA_ELEMENT,
158    /**
159     * Modifying an entry.
160     */
161    MODIFY_ENTRY,
162    /**
163     * Creating an entry.
164     */
165    NEW_ENTRY,
166    /**
167     * Deleting an entry.
168     */
169    DELETE_ENTRY,
170    /**
171     * Deleting a base DN.
172     */
173    DELETE_BASEDN,
174    /**
175     * Deleting a backend.
176     */
177    DELETE_BACKEND,
178    /**
179     * Other task.
180     */
181    OTHER
182  }
183
184  /**
185   * The state on which the task can be.
186   */
187  public enum State
188  {
189    /**
190     * The task is not started.
191     */
192    NOT_STARTED,
193    /**
194     * The task is running.
195     */
196    RUNNING,
197    /**
198     * The task finished successfully.
199     */
200    FINISHED_SUCCESSFULLY,
201    /**
202     * The task finished with error.
203     */
204    FINISHED_WITH_ERROR
205  }
206
207  /**
208   * Returns the names of the backends that are affected by the task.
209   * @return the names of the backends that are affected by the task.
210   */
211  public abstract Set<String> getBackends();
212
213  /**
214   * The current state of the task.
215   */
216  protected State state = State.NOT_STARTED;
217  /**
218   * The return code of the task.
219   */
220  protected Integer returnCode;
221  /**
222   * The last exception encountered during the task execution.
223   */
224  protected Throwable lastException;
225  /**
226   * The progress logs of the task.  Note that the user of StringBuffer is not
227   * a bug, because of the way the contents of logs is updated, using
228   * StringBuffer instead of StringBuilder is required.
229   */
230  protected StringBuffer logs = new StringBuffer();
231  /**
232   * The error logs of the task.
233   */
234  protected StringBuilder errorLogs = new StringBuilder();
235  /**
236   * The standard output logs of the task.
237   */
238  protected StringBuilder outputLogs = new StringBuilder();
239  /**
240   * The print stream for the error logs.
241   */
242  protected ApplicationPrintStream errorPrintStream =
243    new ApplicationPrintStream();
244  /**
245   * The print stream for the standard output logs.
246   */
247  protected ApplicationPrintStream outPrintStream =
248    new ApplicationPrintStream();
249
250  /**
251   * The process (if any) that the task launched.  For instance if this is a
252   * start server task, the process generated executing the start-ds
253   * command-line.
254   */
255  protected Process process;
256  private ControlPanelInfo info;
257
258  private ServerDescriptor server;
259
260  private ProgressDialog progressDialog;
261
262  private ArrayList<ConfigurationElementCreatedListener> confListeners = new ArrayList<>();
263
264  private static int MAX_BINARY_LENGTH_TO_DISPLAY = 1024;
265
266  /**
267   * Constructor of the task.
268   * @param info the control panel information.
269   * @param progressDialog the progress dialog where the task progress will be
270   * displayed.
271   */
272  protected Task(ControlPanelInfo info, ProgressDialog progressDialog)
273  {
274    this.info = info;
275    this.progressDialog = progressDialog;
276    outPrintStream.addListener(new PrintStreamListener()
277    {
278      /**
279       * Add a new line to the logs.
280       * @param msg the new line.
281       */
282      @Override
283      public void newLine(String msg)
284      {
285        outputLogs.append(msg).append("\n");
286        logs.append(msg).append("\n");
287      }
288    });
289    errorPrintStream.addListener(new PrintStreamListener()
290    {
291      /**
292       * Add a new line to the error logs.
293       * @param msg the new line.
294       */
295      @Override
296      public void newLine(String msg)
297      {
298        errorLogs.append(msg).append("\n");
299        logs.append(msg).append("\n");
300      }
301    });
302    server = info.getServerDescriptor();
303  }
304
305  /**
306   * Returns the ControlPanelInfo object.
307   * @return the ControlPanelInfo object.
308   */
309  public ControlPanelInfo getInfo()
310  {
311    return info;
312  }
313
314  /**
315   * Returns the logs of the task.
316   * @return the logs of the task.
317   */
318  public String getLogs()
319  {
320    return logs.toString();
321  }
322
323  /**
324   * Returns the error logs of the task.
325   * @return the error logs of the task.
326   */
327  public String getErrorLogs()
328  {
329    return errorLogs.toString();
330  }
331
332  /**
333   * Returns the output logs of the task.
334   * @return the output logs of the task.
335   */
336  public String getOutputLogs()
337  {
338    return outputLogs.toString();
339  }
340
341  /**
342   * Returns the state of the task.
343   * @return the state of the task.
344   */
345  public State getState()
346  {
347    return state;
348  }
349
350  /**
351   * Returns last exception encountered during the task execution.
352   * Returns <CODE>null</CODE> if no exception was found.
353   * @return last exception encountered during the task execution.
354   */
355  public Throwable getLastException()
356  {
357    return lastException;
358  }
359
360  /**
361   * Returns the return code (this makes sense when the task launches a
362   * command-line, it will return the error code returned by the command-line).
363   * @return the return code.
364   */
365  public Integer getReturnCode()
366  {
367    return returnCode;
368  }
369
370  /**
371   * Returns the process that the task launched.
372   * Returns <CODE>null</CODE> if not process was launched.
373   * @return the process that the task launched.
374   */
375  public Process getProcess()
376  {
377    return process;
378  }
379
380  /**
381   * Returns the progress dialog.
382   * @return the progress dialog.
383   */
384  protected ProgressDialog getProgressDialog()
385  {
386    return progressDialog;
387  }
388
389  /**
390   * Tells whether a new server descriptor should be regenerated when the task
391   * is over.  If the task has an influence in the configuration or state of
392   * the server (for instance the creation of a base DN) this method should
393   * return <CODE>true</CODE> so that the configuration will be re-read and
394   * all the ConfigChangeListeners will receive a notification with the new
395   * configuration.
396   * @return <CODE>true</CODE> if a new server descriptor must be regenerated
397   * when the task is over and <CODE>false</CODE> otherwise.
398   */
399  public boolean regenerateDescriptor()
400  {
401    return true;
402  }
403
404  /**
405   * Method that is called when everything is finished after updating the
406   * progress dialog.  It is called from the event thread.
407   */
408  public void postOperation()
409  {
410  }
411
412  /**
413   * The description of the task.  It is used in both the incompatibility
414   * messages and in the warning message displayed when the user wants to
415   * quit and there are tasks running.
416   * @return the description of the task.
417   */
418  public abstract LocalizableMessage getTaskDescription();
419
420  /**
421   * Adds a configuration element created listener.
422   * @param listener the listener.
423   */
424  public void addConfigurationElementCreatedListener(
425      ConfigurationElementCreatedListener listener)
426  {
427    confListeners.add(listener);
428  }
429
430  /**
431   * Removes a configuration element created listener.
432   * @param listener the listener.
433   */
434  public void removeConfigurationElementCreatedListener(
435      ConfigurationElementCreatedListener listener)
436  {
437    confListeners.remove(listener);
438  }
439
440  /**
441   * Notifies the configuration element created listener that a new object has
442   * been created.
443   * @param configObject the created object.
444   */
445  protected void notifyConfigurationElementCreated(Object configObject)
446  {
447    for (ConfigurationElementCreatedListener listener : confListeners)
448    {
449      listener.elementCreated(
450          new ConfigurationElementCreatedEvent(this, configObject));
451    }
452  }
453
454  /**
455   * Returns a String representation of a value.  In general this is called
456   * to display the command-line equivalent when we do a modification in an
457   * entry.  But since some attributes must be obfuscated (like the user
458   * password) we pass through this method.
459   * @param attrName the attribute name.
460   * @param o the attribute value.
461   * @return the obfuscated String representing the attribute value to be
462   * displayed in the logs of the user.
463   */
464  protected String obfuscateAttributeStringValue(String attrName, Object o)
465  {
466    if (Utilities.mustObfuscate(attrName,
467        getInfo().getServerDescriptor().getSchema()))
468    {
469      return OBFUSCATED_VALUE;
470    }
471    else if (o instanceof byte[])
472    {
473      byte[] bytes = (byte[])o;
474      if (displayBase64(attrName))
475      {
476        if (bytes.length > MAX_BINARY_LENGTH_TO_DISPLAY)
477        {
478          return INFO_CTRL_PANEL_VALUE_IN_BASE64.get().toString();
479        }
480        else
481        {
482          return Base64.encode(bytes);
483        }
484      }
485      else
486      {
487        if (bytes.length > MAX_BINARY_LENGTH_TO_DISPLAY)
488        {
489          return INFO_CTRL_PANEL_BINARY_VALUE.get().toString();
490        }
491        else
492        {
493          // Get the String value
494          ByteString v = ByteString.wrap(bytes);
495          return v.toString();
496        }
497      }
498    }
499    else
500    {
501      return String.valueOf(o);
502    }
503  }
504
505  /**
506   * Obfuscates (if required) the attribute value in an LDIF line.
507   * @param line the line of the LDIF file that must be treated.
508   * @return the line obfuscated.
509   */
510  protected String obfuscateLDIFLine(String line)
511  {
512    int index = line.indexOf(":");
513    if (index != -1)
514    {
515      String attrName = line.substring(0, index).trim();
516      if (Utilities.mustObfuscate(attrName,
517          getInfo().getServerDescriptor().getSchema()))
518      {
519        return attrName + ": " + OBFUSCATED_VALUE;
520      }
521    }
522    return line;
523  }
524
525  /**
526   * Executes a command-line synchronously.
527   * @param commandLineName the command line full path.
528   * @param args the arguments for the command-line.
529   * @return the error code returned by the command-line.
530   */
531  protected int executeCommandLine(String commandLineName, String[] args)
532  {
533    returnCode = -1;
534    String[] cmd = new String[args.length + 1];
535    cmd[0] = commandLineName;
536    System.arraycopy(args, 0, cmd, 1, args.length);
537
538    ProcessBuilder pb = new ProcessBuilder(cmd);
539    // Use the java args in the script.
540    Map<String, String> env = pb.environment();
541    //env.put(SetupUtils.OPENDJ_JAVA_ARGS, "");
542    env.remove(SetupUtils.OPENDJ_JAVA_ARGS);
543    env.remove("CLASSPATH");
544    ProcessReader outReader = null;
545    ProcessReader errReader = null;
546    try {
547      process = pb.start();
548
549      outReader = new ProcessReader(process, outPrintStream, false);
550      errReader = new ProcessReader(process, errorPrintStream, true);
551
552      outReader.startReading();
553      errReader.startReading();
554
555      returnCode = process.waitFor();
556    } catch (Throwable t)
557    {
558      lastException = t;
559    }
560    finally
561    {
562      if (outReader != null)
563      {
564        outReader.interrupt();
565      }
566      if (errReader != null)
567      {
568        errReader.interrupt();
569      }
570    }
571    return returnCode;
572  }
573
574  /**
575   * Informs of whether the task to be launched can be launched or not. Every
576   * task must implement this method so that we avoid launching in paralel two
577   * tasks that are not compatible.  Note that in general if the current task
578   * is not running this method will return <CODE>true</CODE>.
579   *
580   * @param taskToBeLaunched the Task that we are trying to launch.
581   * @param incompatibilityReasons the list of incompatibility reasons that
582   * must be updated.
583   * @return <CODE>true</CODE> if the task that we are trying to launch can be
584   * launched in parallel with this task and <CODE>false</CODE> otherwise.
585   */
586  public abstract boolean canLaunch(Task taskToBeLaunched,
587      Collection<LocalizableMessage> incompatibilityReasons);
588
589  /**
590   * Execute the task.  This method is synchronous.
591   *
592   */
593  public abstract void runTask();
594
595  /**
596   * Returns the type of the task.
597   * @return the type of the task.
598   */
599  public abstract Type getType();
600
601
602  /**
603   * Returns the binary/script directory.
604   * @return the binary/script directory.
605   */
606  protected String getBinaryDir()
607  {
608    if (binDir == null)
609    {
610      File f = Installation.getLocal().getBinariesDirectory();
611      try
612      {
613        binDir = f.getCanonicalPath();
614      }
615      catch (Throwable t)
616      {
617        binDir = f.getAbsolutePath();
618      }
619      if (binDir.lastIndexOf(File.separatorChar) != binDir.length() - 1)
620      {
621        binDir += File.separatorChar;
622      }
623    }
624
625    return binDir;
626  }
627
628  /**
629   * Check whether the provided task and this task run on the same server.
630   * @param task the task the task to be analyzed.
631   * @return <CODE>true</CODE> if both tasks run on the same server and
632   * <CODE>false</CODE> otherwise.
633   */
634  protected boolean runningOnSameServer(Task task)
635  {
636    if (getServer().isLocal() && task.getServer().isLocal())
637    {
638      return true;
639    }
640
641    // Compare the host name and the instance path. This is safer than
642    // comparing ports: we might be running locally on a stopped instance with
643    // the same configuration as a "remote" (though located on the same machine) server.
644    String host1 = getServer().getHostname();
645    String host2 = task.getServer().getHostname();
646    boolean runningOnSameServer = host1 == null ? host2 == null : host1.equalsIgnoreCase(host2);
647    if (runningOnSameServer)
648    {
649      String f1 = getServer().getInstancePath();
650      String f2 = task.getServer().getInstancePath();
651      return Objects.equals(f1, f2);
652    }
653    return runningOnSameServer;
654  }
655
656  /**
657   * Returns the server descriptor on which the task was launched.
658   * @return the server descriptor on which the task was launched.
659   */
660  public ServerDescriptor getServer()
661  {
662    return server;
663  }
664
665  /**
666   * Returns the full path of the command-line associated with this task or
667   * <CODE>null</CODE> if there is not a command-line (or a single command-line)
668   * associated with the task.
669   * @return the full path of the command-line associated with this task.
670   */
671  protected abstract String getCommandLinePath();
672
673  /**
674   * Returns the full path of the command-line for a given script name.
675   * @param scriptBasicName the script basic name (with no extension).
676   * @return the full path of the command-line for a given script name.
677   */
678  protected String getCommandLinePath(String scriptBasicName)
679  {
680    if (isWindows())
681    {
682      return getBinaryDir() + scriptBasicName + ".bat";
683    }
684    return getBinaryDir() + scriptBasicName;
685  }
686
687  /**
688   * Returns the list of command-line arguments.
689   * @return the list of command-line arguments.
690   */
691  protected abstract List<String> getCommandLineArguments();
692
693
694
695  /**
696   * Returns the list of obfuscated command-line arguments.  This is called
697   * basically to display the equivalent command-line to the user.
698   * @param clearArgs the arguments in clear.
699   * @return the list of obfuscated command-line arguments.
700   */
701  protected List<String> getObfuscatedCommandLineArguments(List<String> clearArgs)
702  {
703    String[] toObfuscate = { "--bindPassword", "--currentPassword", "--newPassword" };
704    ArrayList<String> args = new ArrayList<>(clearArgs);
705    for (int i=1; i<args.size(); i++)
706    {
707      for (String argName : toObfuscate)
708      {
709        if (args.get(i-1).equalsIgnoreCase(argName))
710        {
711          args.set(i, OBFUSCATED_VALUE);
712          break;
713        }
714      }
715    }
716    return args;
717  }
718
719  /**
720   * Returns the command-line arguments that correspond to the configuration.
721   * This method is called to remove them when we display the equivalent
722   * command-line.  In some cases we run the methods of the command-line
723   * directly (on this JVM) instead of launching the script in another process.
724   * When we call this methods we must add these arguments, but they are not
725   * to be included as arguments of the command-line (when is launched as a
726   * script).
727   * @return the command-line arguments that correspond to the configuration.
728   */
729  protected List<String> getConfigCommandLineArguments()
730  {
731    List<String> args = new ArrayList<>();
732    args.add("--configClass");
733    args.add(org.opends.server.extensions.ConfigFileHandler.class.getName());
734    args.add("--configFile");
735    args.add(ConfigReader.configFile);
736    return args;
737  }
738
739  /**
740   * Returns the list of arguments related to the connection (host, port, bind
741   * DN, etc.).
742   * @return the list of arguments related to the connection.
743   */
744  protected List<String> getConnectionCommandLineArguments()
745  {
746    return getConnectionCommandLineArguments(true, false);
747  }
748
749  /**
750   * Returns the list of arguments related to the connection (host, port, bind
751   * DN, etc.).
752   * @param useAdminConnector use the administration connector to generate
753   * the command line.
754   * @param addConnectionTypeParameters add the connection type parameters
755   * (--useSSL or --useStartTLS parameters: for ldapadd, ldapdelete, etc.).
756   * @return the list of arguments related to the connection.
757   */
758  protected List<String> getConnectionCommandLineArguments(
759      boolean useAdminConnector, boolean addConnectionTypeParameters)
760  {
761    ArrayList<String> args = new ArrayList<>();
762    InitialLdapContext ctx;
763
764    if (useAdminConnector)
765    {
766      ctx = getInfo().getDirContext();
767    }
768    else
769    {
770      ctx = getInfo().getUserDataDirContext();
771    }
772    if (isServerRunning() && ctx != null)
773    {
774      String hostName = localHostName;
775      if (hostName == null || !getInfo().getServerDescriptor().isLocal())
776      {
777        hostName = ConnectionUtils.getHostName(ctx);
778      }
779      int port = ConnectionUtils.getPort(ctx);
780      boolean isSSL = ConnectionUtils.isSSL(ctx);
781      boolean isStartTLS = ConnectionUtils.isStartTLS(ctx);
782      String bindDN = ConnectionUtils.getBindDN(ctx);
783      String bindPwd = ConnectionUtils.getBindPassword(ctx);
784      args.add("--hostName");
785      args.add(hostName);
786      args.add("--port");
787      args.add(String.valueOf(port));
788      args.add("--bindDN");
789      args.add(bindDN);
790      args.add("--bindPassword");
791      args.add(bindPwd);
792      if (isSSL || isStartTLS)
793      {
794        args.add("--trustAll");
795      }
796      if (isSSL && addConnectionTypeParameters)
797      {
798        args.add("--useSSL");
799      }
800      else if (isStartTLS && addConnectionTypeParameters)
801      {
802        args.add("--useStartTLS");
803      }
804    }
805    return args;
806  }
807
808  /**
809   * Returns the noPropertiesFile argument.
810   * @return the noPropertiesFile argument.
811   */
812  protected String getNoPropertiesFileArgument()
813  {
814    return "--noPropertiesFile";
815  }
816
817  /**
818   * Returns the command-line to be displayed (when we display the equivalent
819   * command-line).
820   * @return the command-line to be displayed.
821   */
822  public String getCommandLineToDisplay()
823  {
824    String cmdLineName = getCommandLinePath();
825    if (cmdLineName != null)
826    {
827      List<String> args =
828        getObfuscatedCommandLineArguments(getCommandLineArguments());
829      args.removeAll(getConfigCommandLineArguments());
830      return getEquivalentCommandLine(cmdLineName, args);
831    }
832    return null;
833  }
834
835  /**
836   * Commodity method to know if the server is running or not.
837   * @return <CODE>true</CODE> if the server is running and <CODE>false</CODE>
838   * otherwise.
839   */
840  protected boolean isServerRunning()
841  {
842    return getInfo().getServerDescriptor().getStatus() ==
843      ServerDescriptor.ServerStatus.STARTED;
844  }
845
846  /**
847   *
848   * Returns the print stream for the error logs.
849   * @return the print stream for the error logs.
850   */
851  public ApplicationPrintStream getErrorPrintStream()
852  {
853    return errorPrintStream;
854  }
855
856  /**
857  *
858  * Returns the print stream for the output logs.
859  * @return the print stream for the output logs.
860  */
861  public ApplicationPrintStream getOutPrintStream()
862  {
863    return outPrintStream;
864  }
865
866  /**
867   * Prints the equivalent modify command line in the progress dialog.
868   * @param dn the dn of the modified entry.
869   * @param mods the modifications.
870   * @param useAdminCtx use the administration connector.
871   */
872  protected void printEquivalentCommandToModify(DN dn,
873      Collection<ModificationItem> mods, boolean useAdminCtx)
874  {
875    printEquivalentCommandToModify(dn.toString(), mods, useAdminCtx);
876  }
877
878  /**
879   * Prints the equivalent modify command line in the progress dialog.
880   * @param dn the dn of the modified entry.
881   * @param mods the modifications.
882   * @param useAdminCtx use the administration connector.
883   */
884  protected void printEquivalentCommandToModify(String dn,
885      Collection<ModificationItem> mods, boolean useAdminCtx)
886  {
887    ArrayList<String> args = new ArrayList<>(getObfuscatedCommandLineArguments(
888        getConnectionCommandLineArguments(useAdminCtx, true)));
889    args.add(getNoPropertiesFileArgument());
890    String equiv = getEquivalentCommandLine(getCommandLinePath("ldapmodify"), args);
891
892    StringBuilder sb = new StringBuilder();
893    sb.append(INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_MODIFY.get()).append("<br><b>");
894    sb.append(equiv);
895    sb.append("<br>");
896    sb.append("dn: ").append(dn);
897    boolean firstChangeType = true;
898    for (ModificationItem mod : mods)
899    {
900      if (firstChangeType)
901      {
902        sb.append("<br>changetype: modify<br>");
903      }
904      else
905      {
906        sb.append("-<br>");
907      }
908      firstChangeType = false;
909      Attribute attr = mod.getAttribute();
910      String attrName = attr.getID();
911      if (mod.getModificationOp() == DirContext.ADD_ATTRIBUTE)
912      {
913        sb.append("add: ").append(attrName).append("<br>");
914      }
915      else if (mod.getModificationOp() == DirContext.REPLACE_ATTRIBUTE)
916      {
917        sb.append("replace: ").append(attrName).append("<br>");
918      }
919      else
920      {
921        sb.append("delete: ").append(attrName).append("<br>");
922      }
923      for (int i=0; i<attr.size(); i++)
924      {
925        try
926        {
927          Object o = attr.get(i);
928          // We are systematically adding the values in binary mode.
929          // Use the attribute names to figure out the value to be displayed.
930          if (displayBase64(attr.getID()))
931          {
932            sb.append(attrName).append(":: ");
933          }
934          else
935          {
936            sb.append(attrName).append(": ");
937          }
938          sb.append(obfuscateAttributeStringValue(attrName, o));
939          sb.append("<br>");
940        }
941        catch (NamingException ne)
942        {
943          // Bug
944          throw new RuntimeException(
945              "Unexpected error parsing modifications: "+ne, ne);
946        }
947      }
948    }
949    sb.append("</b><br><br>");
950
951    getProgressDialog().appendProgressHtml(Utilities.applyFont(
952        sb.toString(), ColorAndFontConstants.progressFont));
953  }
954
955  /**
956   * The separator used to link the lines of the resulting command-lines.
957   */
958  private static final String LINE_SEPARATOR = CommandBuilder.HTML_LINE_SEPARATOR;
959
960  /**
961   * Returns the equivalent command line in HTML without font properties.
962   * @param cmdName the command name.
963   * @param args the arguments for the command line.
964   * @return the equivalent command-line in HTML.
965   */
966  public static String getEquivalentCommandLine(String cmdName,
967      List<String> args)
968  {
969    StringBuilder sb = new StringBuilder(cmdName);
970    for (String arg : args)
971    {
972      if (arg.charAt(0) == '-')
973      {
974        sb.append(LINE_SEPARATOR);
975      }
976      else
977      {
978        sb.append(" ");
979      }
980      sb.append(CommandBuilder.escapeValue(arg));
981    }
982    return sb.toString();
983  }
984
985  /**
986   * Prints the equivalent command line.
987   * @param cmdName the command name.
988   * @param args the arguments for the command line.
989   * @param msg the message associated with the command line.
990   */
991  protected void printEquivalentCommandLine(String cmdName, List<String> args,
992      LocalizableMessage msg)
993  {
994    getProgressDialog().appendProgressHtml(Utilities.applyFont(msg+"<br><b>"+
995        getEquivalentCommandLine(cmdName, args)+"</b><br><br>",
996        ColorAndFontConstants.progressFont));
997  }
998
999  /**
1000   * Tells whether the provided attribute's values must be displayed using
1001   * base 64 when displaying the equivalent command-line or not.
1002   * @param attrName the attribute name.
1003   * @return <CODE>true</CODE> if the attribute must be displayed using base 64
1004   * and <CODE>false</CODE> otherwise.
1005   */
1006  protected boolean displayBase64(String attrName)
1007  {
1008    Schema schema = null;
1009    if (getInfo() != null)
1010    {
1011      schema = getInfo().getServerDescriptor().getSchema();
1012    }
1013    return Utilities.hasBinarySyntax(attrName, schema);
1014  }
1015
1016  /**
1017   * Prints the equivalent rename command line in the progress dialog.
1018   * @param oldDN the old DN of the entry.
1019   * @param newDN the new DN of the entry.
1020   * @param useAdminCtx use the administration connector.
1021   */
1022  protected void printEquivalentRenameCommand(DN oldDN, DN newDN,
1023      boolean useAdminCtx)
1024  {
1025    ArrayList<String> args = new ArrayList<>(getObfuscatedCommandLineArguments(
1026        getConnectionCommandLineArguments(useAdminCtx, true)));
1027    args.add(getNoPropertiesFileArgument());
1028    String equiv = getEquivalentCommandLine(getCommandLinePath("ldapmodify"), args);
1029    StringBuilder sb = new StringBuilder();
1030    sb.append(INFO_CTRL_PANEL_EQUIVALENT_CMD_TO_RENAME.get()).append("<br><b>");
1031    sb.append(equiv);
1032    sb.append("<br>");
1033    sb.append("dn: ").append(oldDN);
1034    sb.append("<br>");
1035    sb.append("changetype: moddn<br>");
1036    sb.append("newrdn: ").append(newDN.rdn()).append("<br>");
1037    sb.append("deleteoldrdn: 1");
1038    sb.append("</b><br><br>");
1039    getProgressDialog().appendProgressHtml(
1040        Utilities.applyFont(sb.toString(),
1041        ColorAndFontConstants.progressFont));
1042  }
1043
1044  /**
1045   * Returns the incompatible message between two tasks.
1046   * @param taskRunning the task that is running.
1047   * @param taskToBeLaunched the task that we are trying to launch.
1048   * @return the incompatible message between two tasks.
1049   */
1050  protected LocalizableMessage getIncompatibilityMessage(Task taskRunning,
1051      Task taskToBeLaunched)
1052  {
1053    return INFO_CTRL_PANEL_INCOMPATIBLE_TASKS.get(
1054        taskRunning.getTaskDescription(), taskToBeLaunched.getTaskDescription());
1055  }
1056}