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 2012-2016 ForgeRock AS.
016 */
017
018package org.opends.quicksetup;
019
020import static com.forgerock.opendj.cli.Utils.*;
021
022import static org.opends.messages.QuickSetupMessages.*;
023
024import java.io.ByteArrayOutputStream;
025import java.io.PrintStream;
026import java.util.Map;
027import java.util.Set;
028
029import javax.naming.NamingException;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.LocalizableMessageBuilder;
033import org.forgerock.i18n.LocalizableMessageDescriptor.Arg2;
034import org.forgerock.i18n.slf4j.LocalizedLogger;
035import org.opends.admin.ads.ADSContext;
036import org.opends.admin.ads.ServerDescriptor;
037import org.opends.admin.ads.TopologyCacheException;
038import org.opends.admin.ads.TopologyCacheFilter;
039import org.opends.admin.ads.util.ApplicationTrustManager;
040import org.opends.admin.ads.util.ConnectionWrapper;
041import org.opends.admin.ads.util.PreferredConnection;
042import org.opends.admin.ads.util.ServerLoader;
043import org.opends.quicksetup.event.ProgressNotifier;
044import org.opends.quicksetup.event.ProgressUpdateListener;
045import org.opends.quicksetup.ui.GuiApplication;
046import org.opends.quicksetup.util.ProgressMessageFormatter;
047import org.opends.quicksetup.util.UIKeyStore;
048import org.opends.quicksetup.util.Utils;
049
050/**
051 * This class represents an application that can be run in the context of
052 * QuickSetup.  Examples of applications might be 'installer' and 'uninstaller'.
053 */
054public abstract class Application implements ProgressNotifier, Runnable {
055
056  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
057
058  /** Represents current install state. */
059  protected CurrentInstallStatus installStatus;
060
061  private UserData userData;
062
063  private Installation installation;
064
065  private ApplicationTrustManager trustManager;
066
067  private boolean notifyListeners = true;
068
069  /** Formats progress messages. */
070  protected ProgressMessageFormatter formatter;
071
072  /** Handler for listeners and event firing. */
073  private ProgressUpdateListenerDelegate listenerDelegate;
074
075  private final ErrorPrintStream err = new ErrorPrintStream();
076  private final OutputPrintStream out = new OutputPrintStream();
077
078  /** Temporary log file where messages will be logged. */
079  protected TempLogFile tempLogFile;
080
081  /**
082   * Creates an application by instantiating the Application class
083   * denoted by the System property
084   * <code>org.opends.quicksetup.Application.class</code>.
085   * @return Application object that was newly instantiated
086   * @throws RuntimeException if there was a problem
087   *  creating the new Application object
088   */
089  public static GuiApplication create() throws RuntimeException {
090    GuiApplication app;
091    String appClassName =
092        System.getProperty("org.opends.quicksetup.Application.class");
093    if (appClassName != null) {
094      Class<?> appClass = null;
095      try {
096        appClass = Class.forName(appClassName);
097        app = (GuiApplication) appClass.newInstance();
098      } catch (ClassNotFoundException e) {
099        logger.info(LocalizableMessage.raw("error creating quicksetup application", e));
100        String msg = "Application class " + appClass + " not found";
101        throw new RuntimeException(msg, e);
102      } catch (IllegalAccessException e) {
103        logger.info(LocalizableMessage.raw("error creating quicksetup application", e));
104        String msg = "Could not access class " + appClass;
105        throw new RuntimeException(msg, e);
106      } catch (InstantiationException e) {
107        logger.info(LocalizableMessage.raw("error creating quicksetup application", e));
108        String msg = "Error instantiating class " + appClass;
109        throw new RuntimeException(msg, e);
110      } catch (ClassCastException e) {
111        String msg = "The class indicated by the system property " +
112            "'org.opends.quicksetup.Application.class' must " +
113            " must be of type Application";
114        throw new RuntimeException(msg, e);
115      }
116    } else {
117      String msg = "System property 'org.opends.quicksetup.Application.class'" +
118          " must specify class quicksetup application";
119      throw new RuntimeException(msg);
120    }
121    return app;
122  }
123
124  /**
125   * Sets this instances user data.
126   * @param userData UserData this application will use
127   *        when executing
128   */
129  public void setUserData(UserData userData) {
130    this.userData = userData;
131  }
132
133  /**
134   * Creates a set of user data with default values.
135   * @return UserData empty set of UserData
136   */
137  public UserData createUserData() {
138    return new UserData();
139  }
140
141  /**
142   * Adds a ProgressUpdateListener that will be notified of updates in
143   * the install progress.
144   * @param l the ProgressUpdateListener to be added.
145   */
146  @Override
147  public void addProgressUpdateListener(ProgressUpdateListener l)
148  {
149    listenerDelegate.addProgressUpdateListener(l);
150  }
151
152  /**
153   * Removes a ProgressUpdateListener.
154   * @param l the ProgressUpdateListener to be removed.
155   */
156  @Override
157  public void removeProgressUpdateListener(ProgressUpdateListener l)
158  {
159    listenerDelegate.removeProgressUpdateListener(l);
160  }
161
162  /**
163   * Gets the OpenDJ installation associated with the execution of this
164   * command.
165   * @return Installation object representing the current OpenDS installation
166   */
167  public Installation getInstallation() {
168    if (installation == null) {
169      String installPath = getInstallationPath();
170      String instancePath = getInstancePath();
171      if (installPath != null) {
172        if (instancePath != null)
173        {
174          installation = new Installation(installPath, instancePath);
175        }
176        else
177        {
178          installation = new Installation(installPath, installPath);
179        }
180      }
181    }
182    return installation;
183  }
184
185  /**
186   * Sets the application's installation.
187   * @param installation describing the application's OpenDS installation
188   */
189  public void setInstallation(Installation installation) {
190    this.installation = installation;
191  }
192
193
194  /**
195   * Returns the UserData object representing the parameters provided by
196   * the user to do the installation.
197   *
198   * @return the UserData object representing the parameters provided
199   * by the user to do the installation.
200   */
201  public UserData getUserData()
202  {
203    if (userData == null) {
204      userData = createUserData();
205    }
206    return userData;
207  }
208
209  /**
210   * This method notifies the ProgressUpdateListeners that there was an
211   * update in the installation progress.
212   * @param ratio the integer that specifies which percentage of the whole
213   * installation has been completed.
214   */
215  public void notifyListenersDone(Integer ratio) {
216    notifyListeners(ratio,
217            getSummary(getCurrentProgressStep()),
218            getFormattedDoneWithLineBreak());
219  }
220
221  /**
222   * This method notifies the ProgressUpdateListeners that there was an
223   * update in the installation progress.
224   * @param ratio the integer that specifies which percentage of the whole
225   * installation has been completed.
226   */
227  public void notifyListenersRatioChange(Integer ratio) {
228    notifyListeners(ratio,
229            getSummary(getCurrentProgressStep()),
230            null);
231  }
232
233  /**
234   * This method notifies the ProgressUpdateListeners that there was an
235   * update in the installation progress.
236   * @param ratio the integer that specifies which percentage of
237   * the whole installation has been completed.
238   * @param currentPhaseSummary the localized summary message for the
239   * current installation progress in formatted form.
240   * @param newLogDetail the new log messages that we have for the
241   * installation in formatted form.
242   */
243  @Override
244  public void notifyListeners(Integer ratio, LocalizableMessage currentPhaseSummary,
245      LocalizableMessage newLogDetail)
246  {
247    if (notifyListeners)
248    {
249      listenerDelegate.notifyListeners(getCurrentProgressStep(),
250            ratio, currentPhaseSummary, newLogDetail);
251    }
252  }
253
254  /**
255   * This method notifies the ProgressUpdateListeners that there was an
256   * update in the installation progress.
257   * @param ratio the integer that specifies which percentage of
258   * the whole installation has been completed.
259   * @param newLogDetail the localized additional log message.
260   */
261  public void notifyListenersWithPoints(Integer ratio,
262      LocalizableMessage newLogDetail) {
263    notifyListeners(ratio, getSummary(getCurrentProgressStep()),
264        formatter.getFormattedWithPoints(newLogDetail));
265  }
266
267  /**
268   * Sets the formatter this instance should use to used
269   * to format progress messages.
270   * @param formatter ProgressMessageFormatter for formatting
271   * progress messages
272   */
273  public void setProgressMessageFormatter(ProgressMessageFormatter formatter) {
274    this.formatter = formatter;
275    this.listenerDelegate = new ProgressUpdateListenerDelegate();
276  }
277
278  /**
279   * Gets the formatter this instance is currently using.
280   * @return the progress message formatter currently used by this
281   * application
282   */
283  public ProgressMessageFormatter getProgressMessageFormatter() {
284    return formatter;
285  }
286
287  /**
288   * Returns the formatted representation of the text that is the summary of the
289   * installation process (the one that goes in the UI next to the progress
290   * bar).
291   * @param text the source text from which we want to get the formatted
292   * representation
293   * @return the formatted representation of an error for the given text.
294   */
295  protected LocalizableMessage getFormattedSummary(LocalizableMessage text)
296  {
297    return formatter.getFormattedSummary(text);
298  }
299
300  /**
301   * Returns the formatted representation of an error for a given text.
302   * @param text the source text from which we want to get the formatted
303   * representation
304   * @return the formatted representation of an error for the given text.
305   */
306  protected LocalizableMessage getFormattedError(LocalizableMessage text)
307  {
308    return formatter.getFormattedError(text, false);
309  }
310
311  /**
312   * Returns the formatted representation of an warning for a given text.
313   * @param text the source text from which we want to get the formatted
314   * representation
315   * @return the formatted representation of an warning for the given text.
316   */
317  public LocalizableMessage getFormattedWarning(LocalizableMessage text)
318  {
319    return formatter.getFormattedWarning(text, false);
320  }
321
322  /**
323   * Returns the formatted representation of a success message for a given text.
324   * @param text the source text from which we want to get the formatted
325   * representation
326   * @return the formatted representation of an success message for the given
327   * text.
328   */
329  protected LocalizableMessage getFormattedSuccess(LocalizableMessage text)
330  {
331    return formatter.getFormattedSuccess(text);
332  }
333
334  /**
335   * Returns the formatted representation of a log error message for a given
336   * text.
337   * @param text the source text from which we want to get the formatted
338   * representation
339   * @return the formatted representation of a log error message for the given
340   * text.
341   */
342  public LocalizableMessage getFormattedLogError(LocalizableMessage text)
343  {
344    return formatter.getFormattedLogError(text);
345  }
346
347  /**
348   * Returns the formatted representation of a log message for a given text.
349   * @param text the source text from which we want to get the formatted
350   * representation
351   * @return the formatted representation of a log message for the given text.
352   */
353  public LocalizableMessage getFormattedLog(LocalizableMessage text)
354  {
355    return formatter.getFormattedLog(text);
356  }
357
358  /**
359   * Returns the formatted representation of the 'Done' text string.
360   * @return the formatted representation of the 'Done' text string.
361   */
362  public LocalizableMessage getFormattedDone()
363  {
364    return LocalizableMessage.raw(formatter.getFormattedDone());
365  }
366
367  /**
368   * Returns the formatted representation of the 'Done' text string
369   * with a line break at the end.
370   * @return the formatted representation of the 'Done' text string.
371   */
372  public LocalizableMessage getFormattedDoneWithLineBreak() {
373    return new LocalizableMessageBuilder(formatter.getFormattedDone())
374            .append(formatter.getLineBreak()).toMessage();
375  }
376
377  /**
378   * Returns the formatted representation of the argument text to which we add
379   * points.  For instance if we pass as argument 'Configuring Server' the
380   * return value will be 'Configuring Server .....'.
381   * @param text the String to which add points.
382   * @return the formatted representation of the '.....' text string.
383   */
384  public LocalizableMessage getFormattedWithPoints(LocalizableMessage text)
385  {
386    return formatter.getFormattedWithPoints(text);
387  }
388
389  /**
390   * Returns the formatted representation of a progress message for a given
391   * text.
392   * @param text the source text from which we want to get the formatted
393   * representation
394   * @return the formatted representation of a progress message for the given
395   * text.
396   */
397  public LocalizableMessage getFormattedProgress(LocalizableMessage text)
398  {
399    return formatter.getFormattedProgress(text);
400  }
401
402  /**
403   * Returns the formatted representation of a progress message for a given
404   * text with a line break.
405   * @param text the source text from which we want to get the formatted
406   * representation
407   * @return the formatted representation of a progress message for the given
408   * text.
409   */
410  public LocalizableMessage getFormattedProgressWithLineBreak(LocalizableMessage text)
411  {
412    return new LocalizableMessageBuilder(formatter.getFormattedProgress(text))
413            .append(getLineBreak()).toMessage();
414  }
415
416  /**
417   * Returns the formatted representation of an error message for a given
418   * exception.
419   * This method applies a margin if the applyMargin parameter is
420   * <CODE>true</CODE>.
421   * @param t the exception.
422   * @param applyMargin specifies whether we apply a margin or not to the
423   * resulting formatted text.
424   * @return the formatted representation of an error message for the given
425   * exception.
426   */
427  protected LocalizableMessage getFormattedError(Throwable t, boolean applyMargin)
428  {
429    return formatter.getFormattedError(t, applyMargin);
430  }
431
432  /**
433   * Returns the line break formatted.
434   * @return the line break formatted.
435   */
436  public LocalizableMessage getLineBreak()
437  {
438    return formatter.getLineBreak();
439  }
440
441  /**
442   * Returns the task separator formatted.
443   * @return the task separator formatted.
444   */
445  protected LocalizableMessage getTaskSeparator()
446  {
447    return formatter.getTaskSeparator();
448  }
449
450  /**
451   * This method is called when a new log message has been received.  It will
452   * notify the ProgressUpdateListeners of this fact.
453   * @param newLogDetail the new log detail.
454   */
455  public void notifyListeners(LocalizableMessage newLogDetail)
456  {
457    Integer ratio = getRatio(getCurrentProgressStep());
458    LocalizableMessage currentPhaseSummary = getSummary(getCurrentProgressStep());
459    notifyListeners(ratio, currentPhaseSummary, newLogDetail);
460  }
461
462  /**
463   * Returns the installation path.
464   * @return the installation path.
465   */
466  public abstract String getInstallationPath();
467
468  /**
469   * Returns the instance path.
470   * @return the instance path.
471   */
472  public abstract String getInstancePath();
473
474
475  /**
476   * Gets the current step.
477   * @return ProgressStep representing the current step
478   */
479  public abstract ProgressStep getCurrentProgressStep();
480
481  /**
482   * Gets an integer representing the amount of processing
483   * this application still needs to perform as a ratio
484   * out of 100.
485   * @param step ProgressStop for which a summary is needed
486   * @return ProgressStep representing the current step
487   */
488  public abstract Integer getRatio(ProgressStep step);
489
490  /**
491   * Gets an i18n'd string representing the summary of
492   * a give ProgressStep.
493   * @param step ProgressStop for which a summary is needed
494   * @return String representing the summary
495   */
496  public abstract LocalizableMessage getSummary(ProgressStep step);
497
498  /**
499   * Sets the current install status for this application.
500   * @param installStatus for the current installation.
501   */
502  public void setCurrentInstallStatus(CurrentInstallStatus installStatus) {
503    this.installStatus = installStatus;
504  }
505
506  /**
507   * Returns whether the installer has finished or not.
508   * @return <CODE>true</CODE> if the install is finished or <CODE>false
509   * </CODE> if not.
510   */
511  public abstract boolean isFinished();
512
513  /**
514   * Returns the trust manager that can be used to establish secure connections.
515   * @return the trust manager that can be used to establish secure connections.
516   */
517  public ApplicationTrustManager getTrustManager()
518  {
519    if (trustManager == null)
520    {
521      if (!Utils.isCli())
522      {
523        try
524        {
525          trustManager = new ApplicationTrustManager(UIKeyStore.getInstance());
526        }
527        catch (Throwable t)
528        {
529          logger.warn(LocalizableMessage.raw("Error retrieving UI key store: "+t, t));
530          trustManager = new ApplicationTrustManager(null);
531        }
532      }
533      else
534      {
535        trustManager = new ApplicationTrustManager(null);
536      }
537    }
538    return trustManager;
539  }
540
541
542
543  /**
544   * Indicates whether this application is capable of cancelling
545   * the operation performed in the run method.  A cancellable operation
546   * should leave its environment in the same state as it was prior to
547   * running the operation (files deleted, changes backed out etc.).
548   *
549   * Marking an <code>Application</code> as cancellable may control UI
550   * elements like the presense of a cancel button while the operation
551   * is being performed.
552   *
553   * Applications marked as cancellable should override the
554   * <code>cancel</code> method in such a way as to undo whatever
555   * actions have taken place in the run method up to that point.
556   *
557   * @return boolean where true inidcates that the operation is cancellable
558   */
559  public abstract boolean isCancellable();
560
561  /**
562   * Signals that the application should cancel a currently running
563   * operation as soon as possible and return the environment to the
564   * state prior to running the operation.  When finished backing
565   * out changes the application should make sure that <code>isFinished</code>
566   * returns true so that the application can complete.
567   */
568  public abstract void cancel();
569
570  /**
571   * Checks whether the operation has been aborted.  If it has throws an
572   * ApplicationException.  All the applications that support abort must
573   * provide their implementation as the default implementation is empty.
574   *
575   * @throws ApplicationException thrown if the application was aborted.
576   */
577  public void checkAbort() throws ApplicationException
578  {
579    // no-op
580  }
581
582  /**
583   * Returns a localized representation of a TopologyCacheException object.
584   * @param e the exception we want to obtain the representation from.
585   * @return a localized representation of a TopologyCacheException object.
586   */
587  protected LocalizableMessage getMessage(TopologyCacheException e)
588  {
589    return Utils.getMessage(e);
590  }
591
592  /**
593   * Gets a connection based on the information that appears on the
594   * provided ServerDescriptor object.  Note that the server is assumed to be
595   * registered and that contains a Map with ADSContext.ServerProperty keys.
596   * @param server the object describing the server.
597   * @param dn the dn to be used to authenticate.
598   * @param pwd the pwd to be used to authenticate.
599   * @param timeout the timeout to establish the connection in milliseconds.
600   * Use {@code 0} to express no timeout.
601   * @param cnx the ordered list of preferred connections to connect to the
602   * server.
603   * @return the InitialLdapContext to the remote server.
604   * @throws ApplicationException if something goes wrong.
605   */
606  protected ConnectionWrapper getRemoteConnection(ServerDescriptor server, String dn, String pwd, int timeout,
607      Set<PreferredConnection> cnx) throws ApplicationException
608  {
609    Map<ADSContext.ServerProperty, Object> adsProperties =
610      server.getAdsProperties();
611    TopologyCacheFilter filter = new TopologyCacheFilter();
612    filter.setSearchMonitoringInformation(false);
613    filter.setSearchBaseDNInformation(false);
614    ServerLoader loader = new ServerLoader(adsProperties, dn, pwd, getTrustManager(), timeout, cnx, filter);
615
616    ConnectionWrapper connection;
617    try
618    {
619      connection = loader.createConnectionWrapper();
620    }
621    catch (NamingException ne)
622    {
623      Arg2<Object, Object> arg2 = isCertificateException(ne)
624          ? INFO_ERROR_READING_CONFIG_LDAP_CERTIFICATE_SERVER
625          : INFO_CANNOT_CONNECT_TO_REMOTE_GENERIC;
626      LocalizableMessage msg = arg2.get(server.getHostPort(true), ne.toString(true));
627      throw new ApplicationException(ReturnCode.CONFIGURATION_ERROR, msg, ne);
628    }
629    return connection;
630  }
631
632  /**
633   * Returns <CODE>true</CODE> if the application is running in verbose mode and
634   * <CODE>false</CODE> otherwise.
635   * @return <CODE>true</CODE> if the application is running in verbose mode and
636   * <CODE>false</CODE> otherwise.
637   */
638  public boolean isVerbose()
639  {
640    return getUserData().isVerbose();
641  }
642
643  /**
644   * Returns the error stream to be used by the application when launching
645   * command lines.
646   * @return  the error stream to be used by the application when launching
647   * command lines.
648   */
649  public ErrorPrintStream getApplicationErrorStream()
650  {
651    return err;
652  }
653
654  /**
655   * Returns the output stream to be used by the application when launching
656   * command lines.
657   * @return  the output stream to be used by the application when launching
658   * command lines.
659   */
660  public OutputPrintStream getApplicationOutputStream()
661  {
662    return out;
663  }
664
665
666  /**
667   * Tells whether we must notify the listeners or not of the message
668   * received.
669   * @param notifyListeners the boolean that informs of whether we have
670   * to notify the listeners or not.
671   */
672  public void setNotifyListeners(boolean notifyListeners)
673  {
674    this.notifyListeners = notifyListeners;
675  }
676
677  /**
678   * Method that is invoked by the printstreams with the messages received
679   * on operations such as start or import.  This is done so that the
680   * application can parse this messages and display them.
681   * @param message the message that has been received
682   */
683  protected void applicationPrintStreamReceived(String message)
684  {
685    // no-op
686  }
687
688  /**
689   * Sets the temporary log file where messages will be logged.
690   *
691   * @param tempLogFile
692   *            temporary log file where messages will be logged.
693   */
694  public void setTempLogFile(final TempLogFile tempLogFile)
695  {
696    this.tempLogFile = tempLogFile;
697  }
698
699  /**
700   * This class is used to notify the ProgressUpdateListeners of events
701   * that are written to the standard error.  It is used in Installer.
702   * These classes just create a ErrorPrintStream and
703   * then they do a call to System.err with it.
704   *
705   * The class just reads what is written to the standard error, obtains an
706   * formatted representation of it and then notifies the
707   * ProgressUpdateListeners with the formatted messages.
708   */
709  public class ErrorPrintStream extends ApplicationPrintStream {
710
711    /** Default constructor. */
712    public ErrorPrintStream() {
713      super();
714    }
715
716    @Override
717    protected LocalizableMessage formatString(String s) {
718      return getFormattedLogError(LocalizableMessage.raw(s));
719    }
720  }
721
722  /**
723   * This class is used to notify the ProgressUpdateListeners of events
724   * that are written to the standard output. It is used in WebStartInstaller
725   * and in Installer. These classes just create a OutputPrintStream and
726   * then they do a call to System.out with it.
727   *
728   * The class just reads what is written to the standard output, obtains an
729   * formatted representation of it and then notifies the
730   * ProgressUpdateListeners with the formatted messages.
731   */
732  public class OutputPrintStream extends ApplicationPrintStream
733  {
734    /** Default constructor. */
735    public OutputPrintStream() {
736      super();
737    }
738
739    @Override
740    protected LocalizableMessage formatString(String s) {
741      return getFormattedLog(LocalizableMessage.raw(s));
742    }
743  }
744
745  /**
746   * This class is used to notify the ProgressUpdateListeners of events
747   * that are written to the standard streams.
748   */
749  private abstract class ApplicationPrintStream extends PrintStream
750  {
751    private boolean isFirstLine;
752
753    /**
754     * Format a string before sending a listener notification.
755     * @param string to format
756     * @return formatted message
757     */
758    protected abstract LocalizableMessage formatString(String string);
759
760    /** Default constructor. */
761    public ApplicationPrintStream()
762    {
763      super(new ByteArrayOutputStream(), true);
764      isFirstLine = true;
765    }
766
767    @Override
768    public void println(String msg)
769    {
770      LocalizableMessageBuilder mb = new LocalizableMessageBuilder();
771      if (!isFirstLine && !Utils.isCli())
772      {
773        mb.append(getLineBreak());
774      }
775      mb.append(formatString(msg));
776
777      notifyListeners(mb.toMessage());
778      applicationPrintStreamReceived(msg);
779      logger.info(LocalizableMessage.raw(msg));
780      isFirstLine = false;
781    }
782
783    @Override
784    public void write(byte[] b, int off, int len)
785    {
786      if (b == null)
787      {
788        throw new NullPointerException("b is null");
789      }
790
791      if (off + len > b.length)
792      {
793        throw new IndexOutOfBoundsException(
794            "len + off are bigger than the length of the byte array");
795      }
796      println(new String(b, off, len));
797    }
798  }
799
800
801
802  /** Class used to add points periodically to the end of the logs. */
803  protected class PointAdder implements Runnable
804  {
805    private Thread t;
806    private boolean stopPointAdder;
807    private boolean pointAdderStopped;
808
809    /** Default constructor. */
810    public PointAdder()
811    {
812    }
813
814    /** Starts the PointAdder: points are added at the end of the logs periodically. */
815    public void start()
816    {
817      LocalizableMessageBuilder mb = new LocalizableMessageBuilder();
818      mb.append(formatter.getSpace());
819      for (int i=0; i< 5; i++)
820      {
821        mb.append(formatter.getFormattedPoint());
822      }
823      Integer ratio = getRatio(getCurrentProgressStep());
824      LocalizableMessage currentPhaseSummary = getSummary(getCurrentProgressStep());
825      listenerDelegate.notifyListeners(getCurrentProgressStep(),
826          ratio, currentPhaseSummary, mb.toMessage());
827      t = new Thread(this);
828      t.start();
829    }
830
831    /** Stops the PointAdder: points are no longer added at the end of the logs periodically. */
832    public synchronized void stop()
833    {
834      stopPointAdder = true;
835      while (!pointAdderStopped)
836      {
837        try
838        {
839          t.interrupt();
840          // To allow the thread to set the boolean.
841          Thread.sleep(100);
842        }
843        catch (Throwable t)
844        {
845          // do nothing
846        }
847      }
848    }
849
850    @Override
851    public void run()
852    {
853      while (!stopPointAdder)
854      {
855        try
856        {
857          Thread.sleep(3000);
858          Integer ratio = getRatio(getCurrentProgressStep());
859          LocalizableMessage currentPhaseSummary = getSummary(getCurrentProgressStep());
860          listenerDelegate.notifyListeners(getCurrentProgressStep(),
861              ratio, currentPhaseSummary, formatter.getFormattedPoint());
862        }
863        catch (Throwable t)
864        {
865          // do nothing
866        }
867      }
868      pointAdderStopped = true;
869
870      Integer ratio = getRatio(getCurrentProgressStep());
871      LocalizableMessage currentPhaseSummary = getSummary(getCurrentProgressStep());
872      listenerDelegate.notifyListeners(getCurrentProgressStep(),
873          ratio, currentPhaseSummary, formatter.getSpace());
874    }
875  }
876}