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