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