You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ant.apache.org by bo...@apache.org on 2001/04/04 14:03:47 UTC

cvs commit: jakarta-ant/src/main/org/apache/tools/ant/taskdefs Recorder.java RecorderEntry.java defaults.properties

bodewig     01/04/04 05:03:47

  Modified:    .        WHATSNEW
               docs/manual coretasklist.html
               src/main/org/apache/tools/ant/taskdefs defaults.properties
  Added:       docs/manual/CoreTasks recorder.html
               src/main/org/apache/tools/ant/taskdefs Recorder.java
                        RecorderEntry.java
  Log:
  New task <recorder>
  
  Submitted by:	Jay Glanville <di...@nortelnetworks.com>
  
  Revision  Changes    Path
  1.99      +1 -1      jakarta-ant/WHATSNEW
  
  Index: WHATSNEW
  ===================================================================
  RCS file: /home/cvs/jakarta-ant/WHATSNEW,v
  retrieving revision 1.98
  retrieving revision 1.99
  diff -u -r1.98 -r1.99
  --- WHATSNEW	2001/04/04 09:12:45	1.98
  +++ WHATSNEW	2001/04/04 12:03:45	1.99
  @@ -13,7 +13,7 @@
   Other changes:
   --------------
   
  -* New tasks: ear, p4counter
  +* New tasks: ear, p4counter, recorder
   
   * Ant now uses JAXP 1.1
   
  
  
  
  1.6       +1 -0      jakarta-ant/docs/manual/coretasklist.html
  
  Index: coretasklist.html
  ===================================================================
  RCS file: /home/cvs/jakarta-ant/docs/manual/coretasklist.html,v
  retrieving revision 1.5
  retrieving revision 1.6
  diff -u -r1.5 -r1.6
  --- coretasklist.html	2001/03/20 19:28:20	1.5
  +++ coretasklist.html	2001/04/04 12:03:45	1.6
  @@ -49,6 +49,7 @@
   <a href="CoreTasks/move.html">Move</a><br>
   <a href="CoreTasks/patch.html">Patch</a><br>
   <a href="CoreTasks/property.html">Property</a><br>
  +<a href="CoreTasks/recorder.html">Recorder</a><br>
   <a href="CoreTasks/rename.html"><i>Rename</i></a><br>
   <a href="CoreTasks/replace.html">Replace</a><br>
   <a href="CoreTasks/rmic.html">Rmic</a><br>
  
  
  
  1.1                  jakarta-ant/docs/manual/CoreTasks/recorder.html
  
  Index: recorder.html
  ===================================================================
  <html>
  
  <head>
  <meta http-equiv="Content-Language" content="en-us">
  <title>Ant User Manual</title>
  </head>
  
  <body>
  
  <h2><a name="log">Recorder</a></h2>
  <h3>Description</h3>
  <p>A recorder is a listener to the current build process that records the
  output to a file.
  
  <p>Several recorders can exist at the same time.  Each recorder is
  associated with a file.  The filename is used as a unique identifier for
  the recorders.  The first call to the recorder task with an unused filename
  will create a recorder (using the parameters provided) and add it to the
  listeners of the build.  All subsiquent calls to the recorder task using
  this filename will modify that recorders state (recording or not) or other
  properties (like logging level).
  
  <p>Some technical issues: the file's print stream is flushed for "finished"
  events (buildFinished, targetFinished and taskFinished), and is closed on
  a buildFinished event.
  
  <h3>Parameters</h3>
  <table border="1" cellpadding="2" cellspacing="0">
    <tr>
      <td valign="top"><b>Attribute</b></td>
      <td valign="top"><b>Description</b></td>
      <td align="center" valign="top"><b>Required</b></td>
    </tr>
    <tr>
      <td valign="top">name</td>
      <td valign="top">The name of the file this logger is associated with.</td>
      <td align="center" valign="middle">yes</td>
    </tr>
    <tr>
      <td valign="top">action</td>
      <td valign="top">This tells the logger what to do: should it start
      recording or stop?  The first time that the recorder task is called for
      this logfile, and if this attribute is not provided, then the default
      for this attribute is "start".  If this attribute is not provided on
      subsiquest calls, then the state remains as previous.
      [Values = {start|stop}, Default = no state change]</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">append</td>
      <td valign="top">Should the recorder append to a file, or create a new
      one? This is only applicable the first time this task is called for
      this file.  [Vaules = {yes|no}, Default=yes]</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">loglevel</td>
      <td valign="top">At what logging level should this recorder instance
      record to?  This is not a once only parameter (like <code>append</code>
      is) -- you can increase or decrease the logging level as the build process
      continues.  [Vaules= {error|warn|info|verbose|debug}, Default = no change]
      </td>
      <td align="center" valign="middle">no</td>
    </tr>
  </table>
  
  <h3>Examples</h3>
  <p>The following build.xml snippit is an example of how to use the recorder
  to record just the <code>&lt;javac&gt;</code> task:
  <pre>
      ...
      &lt;compile &gt;
          &lt;recorder name="log.txt" action="start" /&gt;
          &lt;javac ...
          &lt;recorder name="log.txt" action="stop" /&gt;
      &lt;compile/&gt;
      ...
  </pre>
  
  <p>The following two calls to <code>&lt;recorder&gt;</code> set up two
  recorders: one to file "records-simple.log" at logging level <code>info</code>
  (the default) and one to file "ISO.log" using logging level of
  <code>verbose</code>.
  <pre>
      ...
      &lt;recorder name="records-simple.log" /&gt;
      &lt;recorder name="ISO.log" loglevel="verbose" /&gt;
      ...
  </pre>
  
  <h3>Notes</h3>
  <p>There is some funtionality that I would like to be able to add in the
  future.  They include things like the following:
  <table border="1" cellpadding="2" cellspacing="0">
    <tr>
      <td valign="top"><b>Attribute</b></td>
      <td valign="top"><b>Description</b></td>
      <td align="center" valign="top"><b>Required</b></td>
    </tr>
    <tr>
      <td valign="top">messageprefix</td>
      <td valign="top">Whether or not to include the message prefixes (things
      like the name of the tasks or targets, etc). This has the same effect as
      the <code>-emacs</code> command line parameter does to the screen output.
      [yes|no]</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">listener</td>
      <td valign="top">A classname of a build listener to use from this point
      on instead of the default listener.</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">includetarget</td>
      <td valign="top" rowspan=2>A comma-separated list of targets to automaticly
      record.  If this value is "all", then all targets are recorded.
      [Default = all]</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">excludetarget</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">includetask</td>
      <td valign="top" rowspan=2>A comma-separated list of task to automaticly
      record or not.  This could be difficult as it could conflict with the
      <code>includetarget/excludetarget</code>.  (e.g.:
      <code>includetarget="compile" exlcudetask="javac"</code>, what should
      happen?)</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">excludetask</td>
      <td align="center" valign="middle">no</td>
    </tr>
    <tr>
      <td valign="top">action</td>
      <td valign="top">add greater flexability to the action attribute.  Things
      like <code>close</code> to close the print stream.</td>
      <td align="center" valign="top">no</td>
    </tr>
    <tr>
      <td valign="top"></td>
      <td valign="top"></td>
      <td align="center" valign="top"></td>
    </tr>
  </table>
  
  
  
  <hr><p align="center">Copyright &copy; 2000,2001 Apache Software Foundation. All rights
  Reserved.</p>
  
  </body>
  </html>
  
  
  
  
  1.67      +1 -0      jakarta-ant/src/main/org/apache/tools/ant/taskdefs/defaults.properties
  
  Index: defaults.properties
  ===================================================================
  RCS file: /home/cvs/jakarta-ant/src/main/org/apache/tools/ant/taskdefs/defaults.properties,v
  retrieving revision 1.66
  retrieving revision 1.67
  diff -u -r1.66 -r1.67
  --- defaults.properties	2001/04/04 09:12:47	1.66
  +++ defaults.properties	2001/04/04 12:03:46	1.67
  @@ -43,6 +43,7 @@
   war=org.apache.tools.ant.taskdefs.War
   uptodate=org.apache.tools.ant.taskdefs.UpToDate
   apply=org.apache.tools.ant.taskdefs.Transform
  +recorder=org.apache.tools.ant.taskdefs.Recorder
   
   # optional tasks
   script=org.apache.tools.ant.taskdefs.optional.Script
  
  
  
  1.1                  jakarta-ant/src/main/org/apache/tools/ant/taskdefs/Recorder.java
  
  Index: Recorder.java
  ===================================================================
  /*
   * The Apache Software License, Version 1.1
   *
   * Copyright (c) 2001 The Apache Software Foundation.  All rights
   * reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions
   * are met:
   *
   * 1. Redistributions of source code must retain the above copyright
   *    notice, this list of conditions and the following disclaimer.
   *
   * 2. Redistributions in binary form must reproduce the above copyright
   *    notice, this list of conditions and the following disclaimer in
   *    the documentation and/or other materials provided with the
   *    distribution.
   *
   * 3. The end-user documentation included with the redistribution, if
   *    any, must include the following acknowlegement:
   *       "This product includes software developed by the
   *        Apache Software Foundation (http://www.apache.org/)."
   *    Alternately, this acknowlegement may appear in the software itself,
   *    if and wherever such third-party acknowlegements normally appear.
   *
   * 4. The names "The Jakarta Project", "Ant", and "Apache Software
   *    Foundation" must not be used to endorse or promote products derived
   *    from this software without prior written permission. For written
   *    permission, please contact apache@apache.org.
   *
   * 5. Products derived from this software may not be called "Apache"
   *    nor may "Apache" appear in their names without prior written
   *    permission of the Apache Group.
   *
   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   * SUCH DAMAGE.
   * ====================================================================
   *
   * This software consists of voluntary contributions made by many
   * individuals on behalf of the Apache Software Foundation.  For more
   * information on the Apache Software Foundation, please see
   * <http://www.apache.org/>.
   */
  
  package org.apache.tools.ant.taskdefs;
  
  import org.apache.tools.ant.BuildException;
  import org.apache.tools.ant.DirectoryScanner;
  import org.apache.tools.ant.Project;
  import org.apache.tools.ant.types.*;
  import org.apache.tools.ant.util.*;
  import org.apache.tools.ant.taskdefs.compilers.*;
  import org.apache.tools.ant.Task;
  
  import java.io.*;
  
  import java.util.*;
  
  /**
   * This task is the manager for RecorderEntry's.  It is this class
   * that holds all entries, modifies them every time the <recorder>
   * task is called, and addes them to the build listener process.
   * @see RecorderEntry
   * @author <a href="mailto:jayglanville@home.com">J D Glanville</a>
   * @version 0.5
   *
   */
  public class Recorder extends Task {
  
      //////////////////////////////////////////////////////////////////////
      // ATTRIBUTES
  
      /** The name of the file to record to. */
      private String filename = null;
      /** Whether or not to append.  Need Boolean to record an unset
       *  state (null).
       */
      private Boolean append = null;
      /** Whether to start or stop recording.  Need Boolean to record an
       *  unset state (null).
       */
      private Boolean start = null;
      /** What level to log?  -1 means not initialized yet. */
      private int loglevel = -1;
      /** The list of recorder entries. */
      private static HashMap recorderEntries = new HashMap();
  
      //////////////////////////////////////////////////////////////////////
      // CONSTRUCTORS / INITIALIZERS
  
      //////////////////////////////////////////////////////////////////////
      // ACCESSOR METHODS
  
      /**
       * Sets the name of the file to log to, and the name of the recorder entry.
       * @param fname File name of logfile.
       */
      public void setName( String fname ) {
          filename = fname;
      }
  
      /**
       * Sets the action for the associated recorder entry.
       * @param action The action for the entry to take: start or stop.
       */
      public void setAction( ActionChoices action ) {
          if ( action.getValue().equalsIgnoreCase( "start" ) ) {
              start = Boolean.TRUE;
          } else {
              start = Boolean.FALSE;
          }
      }
  
      /**
       * Whether or not the logger should append to a previous file.
       */
      public void setAppend( boolean append ) {
          this.append = new Boolean(append);
      }
  
      /**
       * Sets the level to which this recorder entry should log to.
       * @see VerbosityLevelChoices
       */
      public void setLoglevel( VerbosityLevelChoices level ){
          //I hate cascading if/elseif clauses !!!
          String lev = level.getValue();
          if ( lev.equalsIgnoreCase("error") ) {
              loglevel = Project.MSG_ERR;
          } else if ( lev.equalsIgnoreCase("warn") ){
              loglevel = Project.MSG_WARN;
          } else if ( lev.equalsIgnoreCase("info") ){
              loglevel = Project.MSG_INFO;
          } else if ( lev.equalsIgnoreCase("verbose") ){
              loglevel = Project.MSG_VERBOSE;
          } else if ( lev.equalsIgnoreCase("debug") ){
              loglevel = Project.MSG_DEBUG;
          }
      }
  
      //////////////////////////////////////////////////////////////////////
      // CORE / MAIN BODY
  
      /**
       * The main execution.
       */
      public void execute() throws BuildException {
          if ( filename == null )
              throw new BuildException( "No filename specified" );
  
          getProject().log( "setting a recorder for name " + filename,
              Project.MSG_DEBUG );
  
          // get the recorder entry
          RecorderEntry recorder = getRecorder( filename, getProject() );
          // set the values on the recorder
          recorder.setMessageOutputLevel( loglevel );
          recorder.setRecordState( start );
      }
  
      //////////////////////////////////////////////////////////////////////
      // INNER CLASSES
  
      /**
       * A list of possible values for the <code>setAction()</code> method.
       * Possible values include: start and stop.
       */
      public static class ActionChoices extends EnumeratedAttribute {
          private static final String[] values = {"start", "stop"};
          public String[] getValues() {
              return values;
          }
      }
  
      /**
       * A list of possible values for the <code>setLoglevel()</code> method.
       * Possible values include: error, warn, info, verbose, debug.
       */
      public static class VerbosityLevelChoices extends EnumeratedAttribute {
          private static final String[] values = { "error", "warn", "info",
              "verbose", "debug"};
          public String[] getValues() {
              return values;
          }
      }
  
      /**
       * Gets the recorder that's associated with the passed in name.
       * If the recorder doesn't exist, then a new one is created.
       */
      protected RecorderEntry getRecorder( String name, Project proj ) throws BuildException {
          Object o = recorderEntries.get(name);
          RecorderEntry entry;
          if ( o == null ) {
              // create a recorder entry
              try {
                  entry = new RecorderEntry( name );
                  PrintStream out = null;
                  if ( append == null ) {
                      out = new PrintStream(
                          new FileOutputStream(name));
                  } else {
                      out = new PrintStream(
                          new FileOutputStream(name, append.booleanValue()));
                  }
                  entry.setErrorPrintStream(out);
                  entry.setOutputPrintStream(out);
              } catch ( IOException ioe ) {
                  throw new BuildException( "Problems creating a recorder entry",
                      ioe );
              }
              proj.addBuildListener(entry);
              recorderEntries.put(name, entry);
          } else {
              entry = (RecorderEntry) o;
          }
          return entry;
      }
  
  }
  
  
  
  1.1                  jakarta-ant/src/main/org/apache/tools/ant/taskdefs/RecorderEntry.java
  
  Index: RecorderEntry.java
  ===================================================================
  /*
   * The Apache Software License, Version 1.1
   *
   * Copyright (c) 2001 The Apache Software Foundation.  All rights
   * reserved.
   *
   * Redistribution and use in source and binary forms, with or without
   * modification, are permitted provided that the following conditions
   * are met:
   *
   * 1. Redistributions of source code must retain the above copyright
   *    notice, this list of conditions and the following disclaimer.
   *
   * 2. Redistributions in binary form must reproduce the above copyright
   *    notice, this list of conditions and the following disclaimer in
   *    the documentation and/or other materials provided with the
   *    distribution.
   *
   * 3. The end-user documentation included with the redistribution, if
   *    any, must include the following acknowlegement:
   *       "This product includes software developed by the
   *        Apache Software Foundation (http://www.apache.org/)."
   *    Alternately, this acknowlegement may appear in the software itself,
   *    if and wherever such third-party acknowlegements normally appear.
   *
   * 4. The names "The Jakarta Project", "Ant", and "Apache Software
   *    Foundation" must not be used to endorse or promote products derived
   *    from this software without prior written permission. For written
   *    permission, please contact apache@apache.org.
   *
   * 5. Products derived from this software may not be called "Apache"
   *    nor may "Apache" appear in their names without prior written
   *    permission of the Apache Group.
   *
   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
   * SUCH DAMAGE.
   * ====================================================================
   *
   * This software consists of voluntary contributions made by many
   * individuals on behalf of the Apache Software Foundation.  For more
   * information on the Apache Software Foundation, please see
   * <http://www.apache.org/>.
   */
  
  package org.apache.tools.ant.taskdefs;
  
  import org.apache.tools.ant.*;
  import java.io.*;
  import java.util.*;
  
  /**
   * This is a class that represents a recorder.  This is the listener
   * to the build process.
   * @author <a href="mailto:jayglanville@home.com">J D Glanville</a>
   * @version 0.5
   *
   */
  public class RecorderEntry implements BuildLogger  {
  
      //////////////////////////////////////////////////////////////////////
      // ATTRIBUTES
  
      /**
       * The name of the file associated with this recorder entry.
       */
      private String filename = null;
      /**
       * The state of the recorder (recorder on or off).
       */
      private boolean record = true;
      /**
       * The current verbosity level to record at.
       */
      private int loglevel = Project.MSG_INFO;
      /**
       * The output PrintStream to record to.
       */
      private PrintStream out = null;
      /**
       * The start time of the last know target.
       */
      private long targetStartTime = 0l;
      /**
       * Line separator.
       */
      private static String lSep = System.getProperty("line.separator");
  
      //////////////////////////////////////////////////////////////////////
      // CONSTRUCTORS / INITIALIZERS
  
      /**
       * @param name The name of this recorder (used as the filename).
       *
       */
      protected RecorderEntry( String name ) {
          filename = name;
      }
  
      //////////////////////////////////////////////////////////////////////
      // ACCESSOR METHODS
  
      /**
       * @return the name of the file the output is sent to.
       */
      public String getFilename() {
          return filename;
      }
  
      /**
       * Turns off or on this recorder.
       * @param state true for on, false for off, null for no change.
       */
      public void setRecordState( Boolean state ) {
          if ( state != null )
              record = state.booleanValue();
      }
  
      public void buildStarted(BuildEvent event) {
          log( "> BUILD STARTED", Project.MSG_DEBUG );
      }
  
      public void buildFinished(BuildEvent event) {
          log( "< BUILD FINISHED", Project.MSG_DEBUG );
  
          Throwable error = event.getException();
          if (error == null) {
              out.println(lSep + "BUILD SUCCESSFUL");
          } else {
              out.println(lSep + "BUILD FAILED" + lSep);
              error.printStackTrace(out);
          }
          out.flush();
          out.close();
      }
  
      public void targetStarted(BuildEvent event) {
          log( ">> TARGET STARTED -- " + event.getTarget(), Project.MSG_DEBUG );
          log( lSep + event.getTarget().getName() + ":", Project.MSG_INFO );
          targetStartTime = System.currentTimeMillis();
      }
  
      public void targetFinished(BuildEvent event) {
          log( "<< TARGET FINISHED -- " + event.getTarget(), Project.MSG_DEBUG );
          String time = formatTime( System.currentTimeMillis() - targetStartTime );
          log( event.getTarget() + ":  duration " + time, Project.MSG_VERBOSE );
          out.flush();
      }
  
      public void taskStarted(BuildEvent event) {
          log( ">>> TAST STARTED -- " + event.getTask(), Project.MSG_DEBUG );
      }
  
      public void taskFinished(BuildEvent event) {
          log( "<<< TASK FINISHED -- " + event.getTask(), Project.MSG_DEBUG );
          out.flush();
      }
  
      public void messageLogged(BuildEvent event) {
          log( "--- MESSAGE LOGGED", Project.MSG_DEBUG );
  
          StringBuffer buf = new StringBuffer();
          if ( event.getTask() != null ) {
              String name = "[" + event.getTask().getTaskName() + "]";
              /** @todo replace 12 with DefaultLogger.LEFT_COLUMN_SIZE */
              for ( int i = 0; i < (12 - name.length()); i++ ) {
                  buf.append( " " );
              } // for
              buf.append( name );
          } // if
          buf.append( event.getMessage() );
  
          log( buf.toString(), event.getPriority() );
      }
  
      /**
       * The thing that actually sends the information to the output.
       * @param mesg The message to log.
       * @param level The verbosity level of the message.
       */
      private void log( String mesg, int level ) {
          if ( record && (level <= loglevel) ) {
                  out.println(mesg);
          }
      }
  
      public void setMessageOutputLevel(int level) {
          if ( level >= Project.MSG_ERR  &&  level <= Project.MSG_DEBUG )
              loglevel = level;
      }
  
      public void setOutputPrintStream(PrintStream output) {
          out = output;
      }
  
      public void setEmacsMode(boolean emacsMode) {
          throw new java.lang.UnsupportedOperationException("Method setEmacsMode() not yet implemented.");
      }
  
      public void setErrorPrintStream(PrintStream err) {
          out = err;
      }
  
      private static String formatTime(long millis) {
          long seconds = millis / 1000;
          long minutes = seconds / 60;
  
  
          if (minutes > 0) {
              return Long.toString(minutes) + " minute"
                  + (minutes == 1 ? " " : "s ")
                  + Long.toString(seconds%60) + " second"
                  + (seconds%60 == 1 ? "" : "s");
          }
          else {
              return Long.toString(seconds) + " second"
                  + (seconds%60 == 1 ? "" : "s");
          }
  
      }
  }