Pinging a host in Java

Why can’t I write ping in Java?
Ping requires ICMP packets. These packets can only be created via a socket of the SOCK_RAW type. Currently, Java only allows SOCK_STREAM (TCP) and SOCK_DGRAM (UDP) sockets. It seems unlikely that this will be added very soon, since many Unix versions only allow SOCK_RAW sockets to be created by root, and winsock does not address ICMP packets (win32 includes an unsupported and undocumented ICMP.DLL).

For a full discussion of socket types, see Stevens’ book (in the bibliography).

Rolling your own SecurityManager

If you look in the source code for FileOutputStream, you’ll notice that, before a file is created and written to, a check is performed to see whether it is allowed to do so.

java.io.FileOutputStream.java:

    public FileOutputStream(String name, boolean append)
        throws FileNotFoundException
    {
	SecurityManager security = System.getSecurityManager();
	if (security != null) {
	    security.checkWrite(name);
	}
      . . .

If no SecurityManager is installed, System.getSecurityManager will return null and no security checks will be performed. We can see that happening by just creating an application that writes to a file.

Main.java:

import java.io.*;
 
public class Main {
   public static void main(String []args) throws Exception {
      FileOutputStream fos = new FileOutputStream("testfile.txt");
      fos.write("hey".getBytes());
      fos.close();
      System.out.println("testfile.txt successfully written!");
   }
}

Running this code with java Main will write the file testfile.txt. If you run this code with java -Djava.security.manager Main, notice that an exception is thrown:

 Exception in thread "main" java.security.AccessControlException: access denied (
java.io.FilePermission testfile.txt write)
        at java.security.AccessControlContext.checkPermission(AccessControlConte
xt.java:195)
        at java.security.AccessController.checkPermission(AccessController.java:
403)
        at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
        at java.lang.SecurityManager.checkWrite(SecurityManager.java:958)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:96)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:62)
        at Main.main(Main.java:5)

We could write our own SecurityManager that allows to write to testfile.txt and restricts everything else to the security sandbox.

MySecurityManager.java:

import java.io.*;
 
public class MySecurityManager extends SecurityManager {
   public void checkWrite(String file) {
      if (!file.equals("testfile.txt")) {
         throw new SecurityException("Only allowed to write to file testfile.txt.");
      }
   }
}

Now we can use our new security manager by either specifying it at command line:

C:> java -Djava.security.manager=MySecurityManager Main
testfile.txt successfully written!

or by doing so programmatically:

import java.io.*;
 
public class Main {
   public static void main(String []args) throws Exception {
      System.setSecurityManager(new MySecurityManager()); 
 
      FileOutputStream fos = new FileOutputStream("testfile.txt");
      fos.write("hey".getBytes());
      fos.close();
      System.out.println("testfile.txt successfully written!");
   }
}

Running:

C:> java Main
testfile.txt successfully written!

A custom security manager can be useful in JDK1.1 applications. However, it becomes a real pain (lots of programming) when you want to specify security restrictions based on who is running the code or where the code is coming from.

From JDK1.2, you can use policy files to customize the security manager in a fine-grained fashion. Let’s use the original program:
Main.java:

import java.io.*;
 
public class Main {
   public static void main(String []args) throws Exception {
      FileOutputStream fos = new FileOutputStream("testfile.txt");
      fos.write("hey".getBytes());
      fos.close();
      System.out.println("testfile.txt successfully written!");
   }
}

and write a policy file that allows writing to only the file testfile.txt:
mypolicy.txt

grant {
   permission java.io.FilePermission "testfile.txt", "write";
};

Run the code and specify the policy file to be used at runtime:

C:> java -Djava.security.policy=mypolicy.txt -Djava.security.manager Main
testfile.txt successfully written!

Install the free Bouncy Castle JCE Provider

Bouncy Castle is a free provider for JCE. These following steps will explain how to add this security provider.

  1. Download the latest release for your JDK version at http://www.bouncycastle.org/latest_releases.html
  2. Extract the zip file in your home directory
  3. BouncyCastle doens’t come with a JAR file. In order to add it to the bootclasspath, create one:
         c:jce-jdk12-107classes> jar cvf bouncycastle.jar *
         c:jce-jdk12-107classes> copy bouncycastle.jar c:jdk1.2.2jrelibext
    
  4. Go to your JAVA_HOME/jre/lib/security directory and edit the file java.security. Look at the configuration, in my case it says something like:
       security.provider.1=sun.security.provider.Sun
       security.provider.2=com.sun.crypto.provider.SunJCE
    

    Add the line security.provider.3=org.bouncycastle.jce.provider.BouncyCastleProvider

  5. Run one of the examples to see if it is working (eg. How do I crypt/decrypt a message with the Blowfish algorithm?)

Update:

This information is outdated. Now, you can download JAR versions for your JDK version.

For 1.3.1, I downloaded jce-jdk13-118.jar and bcprov-jdk13-118.jar and added them to my classpath (you can also put them in jdk1.3.1/jre/lib/ext and NOT add them to your classpath).

You can add a static provider to jdk1.3.1/jre/lib/security/java.security as described above or you can do this programmatically in your code:

...
   java.security.Provider provider = new org.bouncycastle.jce.provider.BouncyCastleProvider();
   Security.addProvider(provider);

Encrypting/decrypting with DES/CBC/PKCS5Padding

Look at the other questions for more information about the CBC mode and PKCS5Padding. Essentially, a CBC needs a initialization vector (IV) that is basically the seed for encrypting the first block of plaintext. A byte array is filled up with random bytes. You need the same initialization vector for decrypting. You can transmit or save this IV along with your encrypted message. The secret symmetric encryption/decryption key in our case is “password”.

Main.java:

import javax.crypto.spec.*;
import java.security.*;
import javax.crypto.*;
 
public class Main
{
   static IvParameterSpec iv;
 
   public static void main(String []args) throws Exception {
      String toEncrypt = "The shorter you live, the longer you're dead!";
 
      System.out.println("Encrypting...");
      byte[] encrypted = encrypt(toEncrypt, "password");
 
      System.out.println("Decrypting...");
      String decrypted = decrypt(encrypted, "password");
    
      System.out.println("Decrypted text: " + decrypted);
   } 
 
   public static byte[] encrypt(String toEncrypt, String key) throws Exception {
      // create a binary key from the argument key (seed)
      SecureRandom sr = new SecureRandom(key.getBytes());
      KeyGenerator kg = KeyGenerator.getInstance("DES");
      kg.init(sr);
      SecretKey sk = kg.generateKey();
 
      // create an instance of cipher
      Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
 
      // generate an initialization vector (IV)
      SecureRandom secureRandom = new SecureRandom();
      byte[] ivspec = new byte[cipher.getBlockSize()];
      secureRandom.nextBytes(ivspec);
      iv = new IvParameterSpec(ivspec);
 
      // initialize the cipher with the key and IV
      cipher.init(Cipher.ENCRYPT_MODE, sk, iv);
 
      // enctypt!
      byte[] encrypted = cipher.doFinal(toEncrypt.getBytes());
 
      return encrypted;
   }
 
   public static String decrypt(byte[] toDecrypt, String key) throws Exception {
      // create a binary key from the argument key (seed)
      SecureRandom sr = new SecureRandom(key.getBytes());
      KeyGenerator kg = KeyGenerator.getInstance("DES");
      kg.init(sr);
      SecretKey sk = kg.generateKey();
 
      // do the decryption with that key
      Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
      cipher.init(Cipher.DECRYPT_MODE, sk, iv);
      byte[] decrypted = cipher.doFinal(toDecrypt);
 
      return new String(decrypted);
   }
}

(tested with the BouncyCastle JCE provider, http://www.bouncycastle.org)

Downloading a secure page (HTTPS) with Java

If you try to get data from an HTTPS-enabled page, you will get a MalformedURLException, as shown in following example:

Main.java:

import java.net.*;
import java.io.*;

public class Main {
   public static void main(String[] args) throws Exception {
      URL url = new URL("https://www.sun.com");
      BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
      String line;

      while ((line = br.readLine()) != null) {
         System.out.println(line);
      }
      br.close();
   }
}

outputs:

C:>java Main
Exception in thread "main" java.net.MalformedURLException: unknown protocol: https
        at java.net.URL.<init>(URL.java:497)
        at java.net.URL.<init>(URL.java:364)
        at java.net.URL.<init>(URL.java:308)
        at Main.main(Main.java, Compiled Code)

This is because Sun has no implementation for the HTTPS protocol in their core libraries. However, they created an reference implementation called Java Secure Socket Extension (JSSE) available as seperate download at http://java.sun.com/products/jsse. Include jcert.jar, jnet.jar and jsse.jar in your classpath and add the following two lines of code to the previous program:

      System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");
      Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());

Here’s the updated example that works:

import java.security.*;
import java.net.*;
import java.io.*;

public class Main {
   public static void main(String[] args) throws Exception {
      // create a new protocol handler
      System.setProperty("java.protocol.handler.pkgs", "com.sun.net.ssl.internal.www.protocol");

      // protocol handler uses this security provider
      Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider());
      URL url = new URL("https://www.sun.com");
      BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()));
      String line;

      while ((line = br.readLine()) != null) {
         System.out.println(line);
      }

      br.close();
   }
}

A lenient DateFormat

If DateFormat is not lenient, the Date supplied has to be a valid one. If it is lenient, you can work with dates that are out of range. Eg. Dec. 40th, 2000 will refer to the date Jan 9th, 2001.

Main.java:

import java.util.*;
import java.text.*;
    
public class Main {
   public static void main(String args[]) {
      try {
         Date date;
 
         DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
         date = df.parse("12/40/00");   
         System.out.println("default:t" + date);
 
         df.setLenient(true);
         date = df.parse("12/40/00");   
         System.out.println("lenient:t" + date);
 
         df.setLenient(false);
         date = df.parse("12/40/00");   
         System.out.println("not lenient:t" + date);
      }
      catch(ParseException e) {
         e.printStackTrace();
      }
   }
}

outputs:

default:        Tue Jan 09 00:00:00 CET 2001
lenient:        Tue Jan 09 00:00:00 CET 2001
java.text.ParseException: Unparseable date: "12/40/00"
        at java.text.DateFormat.parse(DateFormat.java:331)
        at Main.main(Main.java:18)

Getting the number of days between two Dates

You can create two date objects, get the number of milliseconds that have passed since Jan 1, 1970 for each of them, make the difference and divide that back by the number of milliseconds per day (= 3600sec/hr * 24hrs * 1000ms)

Main.java:

import java.util.*;
 
public class Main {
   public static void main(String []args) {
      Calendar date1 = Calendar.getInstance();
      date1.set(2001, 6, 4);
 
      Calendar date2 = Calendar.getInstance();
      date2.set(1972, 12, 20);
 
      long diff = (date1.getTime().getTime() - date2.getTime().getTime()) / (3600*24*1000);
      System.out.println("Difference in time: " + diff);
   }
} 

Setting the default Locale in Java

Use Locale.setDefault.

Main.java:

import java.util.*;
 
public class Main {
   public static void main(String []args) {
      // for default locale 
      Locale locale = Locale.getDefault();
      System.out.println(locale);  
 
      Locale.setDefault(new Locale("nl", "BE")); 
      locale = Locale.getDefault();
      System.out.println(locale);  
   }
}

outputs:

en_US
nl_BE

Accessing the Windows registry through the preferences API

On Windows, the preferences API makes use of a DLL to access program-specific properties in the Windows Registry. The following example uses reflection to access private native methods in the preferences API and allows you to get REG_SZ values from the Windows Registry.

For example, to get the IE start page on my machine, I executed:

C:registrytest> c:jdk1.4binjava Main "HKEY_CURRENT_USER\Softw --
are\Microsoft\Internet Explorer\Main" "start page"

http://www.google.com/

It’s a demonstration and works only with JDK1.4. Use at your own risk.

Main.java:

import java.lang.reflect.*;
import java.util.prefs.*;
import java.util.*;
import java.io.*;
 
public class Main
{
   /* Windows security masks */
   private final static int KEY_QUERY_VALUE = 1;
 
   /* Constants used to interpret returns of native functions    */
   private final static int NATIVE_HANDLE = 0;
   private final static int ERROR_CODE = 1;
 
   /*  Windows error codes. */
   private final static int ERROR_SUCCESS = 0;
 
   public static void main(String[] args) {
      if (args.length != 2) {
         System.out.println("Usage: java Main path key");
         System.out.print("  eg.: java Main "HKEY_CURRENT_USER\");
         System.out.print("\Software\\Microsoft\\Internet Explorer\\Main"");
         System.out.println(" "start page"");
         System.exit(1);
      }
 
      try {
         int hkey = getHKEY(args[0]);
         byte[] WINDOWS_ROOT_PATH = stripHKEY(args[0]);
         String key = args[1];
         System.out.println(getValue(hkey, WINDOWS_ROOT_PATH, key));
      }
      catch(Exception e) {
         System.out.println(e.getMessage());
      }
   }
 
   public static int getHKEY(String path) throws Exception {
      if (path.startsWith("HKEY_CURRENT_USER")) {
         return 0x80000001;
      }
      else if (path.startsWith("HKEY_LOCAL_MACHINE")) {
         return 0x80000002;
      }
      else {
         throw new Exception("Path should start with HKEY_CURRENT_USER " + 
                                                 "or HKEY_LOCAL_MACHINE");
      }
   }
 
   public static byte[] stripHKEY(String path) {
      int beginIndex = path.indexOf("\\");
      return stringToByteArray(path.substring(beginIndex+2));
   }
 
   public static String getValue(int hkey, byte[] WINDOWS_ROOT_PATH, String key) 
                                             throws Exception {
      Class theClass = Class.forName("java.util.prefs.WindowsPreferences");
 
      int[] result = openKey1(hkey, windowsAbsolutePath(WINDOWS_ROOT_PATH),
                              KEY_QUERY_VALUE);
      if (result[ERROR_CODE] != ERROR_SUCCESS) {
         throw new Exception("Path not found!");
      }
      int nativeHandle = result[NATIVE_HANDLE];
 
      Method m = theClass.getDeclaredMethod("WindowsRegQueryValueEx",
                                            new Class[]{int.class, byte[].class});
      m.setAccessible(true);
      byte[] windowsName = toWindowsName(key);
      Object value = m.invoke(null, new Object[]{new Integer(nativeHandle), windowsName});
      WindowsRegCloseKey(nativeHandle);
      if (value == null) {
         throw new Exception("Path found.  Key not found.");
      }
 
      byte[] origBuffer = (byte[]) value;
      byte[] destBuffer = new byte[origBuffer.length - 1];
      System.arraycopy(origBuffer, 0, destBuffer, 0, origBuffer.length - 1);
 
      return new String(destBuffer);
   }
 
   public static int WindowsRegCloseKey(int nativeHandle) 
                                         throws Exception {
      Class theClass = Class.forName("java.util.prefs.WindowsPreferences");
      Method m = theClass.getDeclaredMethod("WindowsRegCloseKey", new Class[]{int.class});
      m.setAccessible(true);
      Object ret = m.invoke(null, new Object[]{new Integer(nativeHandle)});
      return ((Integer) ret).intValue();
   }
 
   public static int openKey(byte[] windowsAbsolutePath, int securityMask) 
                                         throws Exception {
      Class theClass = Class.forName("java.util.prefs.WindowsPreferences");
      Method m = theClass.getDeclaredMethod("openKey", new Class[]{byte[].class, int.class});
      m.setAccessible(true);
      Object ret = m.invoke(null, new Object[]{windowsAbsolutePath, new Integer(securityMask)});
      return ((Integer) ret).intValue();
   }
 
   public static int[] openKey1(int hkey, byte[] windowsAbsolutePath, int securityMask) 
                                         throws Exception {
      Class theClass = Class.forName("java.util.prefs.WindowsPreferences");
      Method m = theClass.getDeclaredMethod("WindowsRegOpenKey", new Class[]{int.class, 
                                                                             byte[].class, 
                                                                             int.class});
      m.setAccessible(true);
      Object ret = m.invoke(null, new Object[]{new Integer(hkey), 
                                               windowsAbsolutePath, 
                                               new Integer(securityMask)});
      return (int[]) ret;
   }
 
   private static byte[] stringToByteArray(String str) {
      byte[] result = new byte[str.length() + 1];
      for (int i = 0; i < str.length(); i++) {
         result[i] = (byte) str.charAt(i);
      }
      result[str.length()] = 0;
      return result;
   } 
 
   private static byte[] windowsAbsolutePath(byte[] WINDOWS_ROOT_PATH) {
      ByteArrayOutputStream bstream = new ByteArrayOutputStream();
      bstream.write(WINDOWS_ROOT_PATH, 0, WINDOWS_ROOT_PATH.length - 1);
      StringTokenizer tokenizer = new StringTokenizer(absolutePath(), "/");
      while (tokenizer.hasMoreTokens()) {
         bstream.write((byte) '\');
         String nextName = tokenizer.nextToken();
         byte[] windowsNextName = toWindowsName(nextName);
         bstream.write(windowsNextName, 0, windowsNextName.length - 1);
      }
      bstream.write(0);
      return bstream.toByteArray();
   }
 
   private static String absolutePath() {
      return "/";
   }
 
   private static byte[] toWindowsName(String javaName) {
      StringBuffer windowsName = new StringBuffer();
      for (int i = 0; i < javaName.length(); i++) {
         char ch = javaName.charAt(i);
         if ((ch < 0x0020) || (ch > 0x007f)) {
            throw new RuntimeException("Unable to convert to Windows name");
         }
         if (ch == '\') {
            windowsName.append("//");
         } else if (ch == '/') {
            windowsName.append('\');
         } else if ((ch >= 'A') && (ch <= 'Z')) {
            windowsName.append("/" + ch);
         } else {
            windowsName.append(ch);
         }
      }
      return stringToByteArray(windowsName.toString());
   }
 
   private static String toJavaValueString(byte[] windowsNameArray) {
      // Use modified native2ascii algorithm
      String windowsName = byteArrayToString(windowsNameArray);
      StringBuffer javaName = new StringBuffer();
      char ch;
      for (int i = 0; i < windowsName.length(); i++) {
         if ((ch = windowsName.charAt(i)) == '/') {
            char next = ' ';
 
            if (windowsName.length() > i + 1 &&
                  (next = windowsName.charAt(i + 1)) == 'u') {
               if (windowsName.length() < i + 6) {
                  break;
               } else {
                  ch = (char) Integer.parseInt
                        (windowsName.substring(i + 2, i + 6), 16);
                  i += 5;
               }
            } else
                  if ((windowsName.length() > i + 1) &&
                  ((windowsName.charAt(i + 1)) >= 'A') && (next <= 'Z')) {
               ch = next;
               i++;
            } else if ((windowsName.length() > i + 1) &&
                  (next == '/')) {
               ch = '\';
               i++;
            }
         } else if (ch == '\') {
            ch = '/';
         }
         javaName.append(ch);
      }
      return javaName.toString();
   }
 
   private static String byteArrayToString(byte[] array) {
      StringBuffer result = new StringBuffer();
      for (int i = 0; i < array.length - 1; i++) {
         result.append((char) array[i]);
      }
      return result.toString();
   }
}

Tokenizing text with a StreamTokenizer

Instantiate a StreamTokenizer, pass it a Reader instance and loop through the available tokens with nextToken. This method returns an integer that refers to the type of token that was read. These are the possibilities:

   TT_EOF		end of file
   TT_EOL		end of line
   TT_NUMBER	numeric value, the actual value is stored in nval
   TT_WORD		word value, the actual value is stored in sval
   "			a quoted string, the actual value is stored in sval
   x			a character token, x replaced by the character converted to an int

This simple example shows you how to read in a text file and print out its tokens.

Main.java:

import java.io.*;
 
public class Main
{
   public static void main(String []args) throws Exception{
      if (args.length != 1) {
         System.out.println("Usage: java Main <file>");
         System.exit(1);
      }
 
      BufferedReader br = new BufferedReader(new FileReader(args[0]));
      StreamTokenizer st = new StreamTokenizer(br);
 
      int t = st.nextToken();
      while (t != StreamTokenizer.TT_EOF) {
         switch(t) {
            case StreamTokenizer.TT_EOL:
               System.out.println("TT_EOL");
               break;
            case StreamTokenizer.TT_NUMBER:
               System.out.println("TT_NUMBER: " + st.nval);
               break;
            case StreamTokenizer.TT_WORD:
               System.out.println("TT_WORD: " + st.sval);
               break;
            case '"':
               System.out.println("quoted string: " + st.sval);
               break;
            default:
               System.out.println("tokentype: " + (char) t);
         }
  
         t = st.nextToken();
      }
   }
}

If we run it on the following text file:

/* 
 * simple program in Java
 */
 
public class Main {
   public static void Main(String []args) {
      // make calculation
      int a = 4 / 2;
 
      System.out.println("result: " + a);
   }
}

it produces the following result.

tokentype: *
TT_WORD: simple
TT_WORD: program
TT_WORD: in
TT_WORD: Java
tokentype: *
TT_WORD: public
TT_WORD: class
TT_WORD: Main
tokentype: {
TT_WORD: public
TT_WORD: static
TT_WORD: void
TT_WORD: Main
tokentype: (
TT_WORD: String
tokentype: [
tokentype: ]
TT_WORD: args
tokentype: )
tokentype: {
TT_WORD: int
TT_WORD: a
tokentype: =
TT_NUMBER: 4.0
TT_WORD: System.out.println
tokentype: (
quoted string: result: 
tokentype: +
TT_WORD: a
tokentype: )
tokentype: ;
tokentype: }
tokentype: }

Notice that /* , / , // and whitespace seem to be left out! In addition, anything that comes after a / is left out too! The reason for this is that StreamTokenizer has a initial setup:

   - 'A' to 'Z' and 'a' to 'z' and u00A0 till u00FF
	are considered wordchars
   - u0000 till u0020 is considered whitespace
   - / is a comment character
   - ' and " are considered quote characters
   - Numbers are parsed (notice 4 has become 4.0)
   - EOL is considered whitespace
   - C/C++ comments are not recognized.

You can customize the StreamTokenizer in a number of ways:

1. wordChars(int lo, int hi)

The lo and hi parameters specify the unicode range of characters that you would like to see treated as part of a word. You can call this method several times to include several ranges. Try this after you have instantiated the StreamTokenizer:

      // consider all values in the range '{' and '}' as whitespace
      st.wordChars('{', '}');

2. whitespaceChars(int lo, int hi)

The lo and hi parameters specify the unicode range of characters that you would like to see treated as whitespace. You can call this method several times to include several ranges. Try this:

      // consider all values in the range '{' and '}' as whitespace
      st.whitespaceChars('{', '}'); 

3. ordinaryChars(int lo, int hi)

The lo and hi parameters specify the unicode range of characters that you would like to see treated as being an ordinary character, meaning it’s not part of a word, number, whitespace, etc. It will be returned by nextToken as a single character. There’s a variation on this method that takes only one parameter. Try this:

      // consider all values in the range 'a' to 'g' as ordinary char
      st.ordinaryChars('a', 'g');

4. commentChar(int ch)

Specifies that the value ch should be treated as a comment character, meaning the character plus the rest of the line is ignored. Try this:

      // treat 'p' as being a comment
      st.commentChar('p');

5. quoteChar(int ch)

Tells the tokenizer that all characters between this delimiter ch are treated as a string constant. Try this:

      st.quoteChar('/');

6. parseNumbers

This tells the tokenizer that characters from 0 to 9, the period and the minus sign should be recognized as being part of a TT_NUMBER token, if it can be constructed. By default, parseNumbers is set. You can have . and – treated otherwise but then you would have to use the methods ordinaryChar or wordChars.

7. eolIsSignificant(boolean b)

If b is set, TT_EOL will be returned whenever an end-of-line is encountered. Otherwise, they are ignored. Try this:

      st.eolIsSignificant(true);

8. slashStarComments(boolean b)

If b is set, all characters between /* and */ are ignored (C style comments)

9. slashSlashComments(boolean b)

If b is set, // is recognized as being comments (the rest of the line is ignored). (C++ style comments)

10. lowerCaseMode(boolean lc)

if lc is set, all word tokens are lowercased when returned.

11. pushBack()

“Pushes” the last token that was returned back on the stream. Next time nextToken is invoked, the same token will be returned as the last one.

Then there’s another member variable lineno that you may invoke at any time to get the current linenumber.