Retrieving the links in an HTML document

LinkExtractor.java:

package htmltools;
 
import java.net.URL;
import java.net.InetAddress;
import java.net.MalformedURLException;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;

import java.util.Collection;
import java.util.ArrayList;

import javax.swing.*;
import javax.swing.text.*;
import javax.swing.text.html.*;
import javax.swing.text.html.HTMLEditorKit;
import javax.swing.text.html.parser.*;

/**
 * This class takes a URL and, if it is valid, extracts all the external 
 * and local links and stores them in distinct ArrayLists.
 * It provides accessors to the two lists.
 */
public class LinkExtractor
{
    private URL m_zURL = null;
    private CallbackHandler m_zHandler;
    
    /**
     * Initialize the URL. 
     * You can provide URLs in the following form:<br>
     * <font color="blue">
     * http://www.something.ext<br>
     * www.something.ext<br>
     * something.ext<br>
     * </font>
     * In the last case the extractor assumes the URL is on the local
     * host and tries to open it at the local host
     */
    public LinkExtractor(String sURL) throws MalformedURLException, IOException {
	/* End-users don't like typing http, so 
	   we'll give them a hand */
	if (sURL.startsWith("www.")) {
	    sURL = "http://"+sURL;
	} else if (!sURL.startsWith("http")) {
	    /* there is neither an http protocol specified,
	       and the address does not start with www.
	       We will try to find this document on the local host.
	       Of course, this behavior does not cover all cases.
	       For example the user may try an ftp protocol, 
	       or, accostomed to modern day browsers, omit www altogether.
	       Oh well ...
	    */
	    InetAddress zAddr = InetAddress.getLocalHost();
	    sURL = "http://"+zAddr.getHostName()+"/"+sURL;
	}
	
	m_zURL = new URL(sURL);
	m_zHandler = new CallbackHandler();
	parse();
    }

    /**
     * return and ArrayList of all external links
     */
    public Collection getExternalLinks()  {
	if (null == m_zURL)
	    return null;
	return m_zHandler.m_clExternalLinks;    
    }
    
    /**
     * return and ArrayList of all local links
     */ 
    public Collection getLocalLinks()  {
	if (null == m_zURL)
	    return null;
	return m_zHandler.m_clLocalLinks;    
    }
  
  private void parse() throws IOException {
    // establish connection to site
      BufferedReader zReader = new BufferedReader
	  (new InputStreamReader(m_zURL.openStream()));
      // parse it to get the links
      new ParserDelegator().parse(zReader, m_zHandler, true);
      zReader.close();
  }
    
    private class CallbackHandler extends HTMLEditorKit.ParserCallback 
    {
	ArrayList m_clExternalLinks;
	ArrayList m_clLocalLinks;
	
	public CallbackHandler() {
	    
	    m_clExternalLinks = new ArrayList();
	    m_clLocalLinks = new ArrayList();
	}
	
	/**
	 * Invoked when text in the html document is encountered. Based on
	 * the current state, this will either do nothing
	 * or add an href attribute
	 */
	public void handleText(char[] data, int pos) {
	    // System.out.println(new String(data));
	}
	/**
	 * Invoked when a start tag is encountered. 
	 */
	public void handleStartTag(HTML.Tag zTag, 
				   MutableAttributeSet zAttributes,
				   int iPosition) {
	    String sLink = null;
	    
	    if (zTag.equals(HTML.Tag.A) ||
		zTag.equals(HTML.Tag.ADDRESS)) {
		
		sLink = (String)zAttributes.getAttribute(HTML.Attribute.HREF);
		if (null == sLink) { 
		    
		} else if (sLink.startsWith("http")) {
		    if (!m_clExternalLinks.contains((String)sLink)) {
			m_clExternalLinks.add((String)sLink);
		    }
		} else if (!m_clLocalLinks.contains((String)sLink)) {
		    m_clLocalLinks.add((String)sLink);
		} 
	    }
	}
	
	
	/**
	 * Invoked when the end of a tag is encountered. 
	 */
	public void handleEndTag(HTML.Tag t, int pos) {
	    
	}	    
   }

    /*
     * The main method is provided only for testing.
     */
    static void main(String[] asArgs) throws Exception {
	if (asArgs.length < 1) {
	    System.out.println("Usage: java GetLinks <URL>");
	    System.exit(0);
	}
	String sURL = asArgs[0];

	LinkExtractor gl = new LinkExtractor(sURL);
	ArrayList clLinks = (ArrayList) gl.getExternalLinks();
	
	for (int i=0;i<clLinks.size();i++) {
	    System.out.println((String)clLinks.get(i));
	}
	
	ArrayList clLocalLinks = (ArrayList) gl.getLocalLinks();	
	for (int i=0;i<clLocalLinks.size();i++) {
	    System.out.println((String)clLocalLinks.get(i));
	}
    }
}

Getting the list of network interfaces installed on your machine

If you have multiple IP addresses on one machine, it may be useful to specify which one to use for your networking stuff. Since JDK1.4, you can enumerate them and select one with the class java.net.NetworkInterface.

Main.java:

import java.util.*;
import java.net.*;
 
public class Main
{
   public static void main(String []args) {
      try {
         Enumeration enum = NetworkInterface.getNetworkInterfaces();
         while (enum.hasMoreElements()) {
            NetworkInterface ni = (NetworkInterface) enum.nextElement();
            System.out.println(ni);
         }
      }
      catch(SocketException e) {
         e.printStackTrace();
      }
   }
}

outputs on my Win2000 machine:

name:lan0 (Intel DC21140 PCI Fast Ethernet Adapter) index: 1 addresses:
/192.168.2.10;

name:lan1 (Intel DC21140 PCI Fast Ethernet Adapter) index: 2 addresses:

name:lo0 (MS TCP Loopback interface) index: 3 addresses:
/127.0.0.1;

Performance gain of using NIO channels as opposed to regular streams for doing file I/O

To test things out, I wrote a couple small Java programs to copy a file:

CopyFile1.java: uses the old approach using BufferedInput/BufferedOutputStreams with the default buffer size of 2048 bytes
CopyFile2.java: uses the NIO libraries with different buffer sizes
CopyFile3.java: uses the NIO FileChannel method transferTo

CopyFile1.java uses BufferedInputStream and BufferedOutputStream. If you look at the source code for these files, the default buffer is an array of 2048 bytes. The first version of the NIO CopyFile2 program is also set to use an internal buffer of 2048 bytes. Other tests have been done with larger buffer sizes. As expected, the more buffer space allocated, the higher the performance (at a cost of more memory usage).

The result of copying the Java SDK file j2sdk-1_4_0-win.exe(37Meg) using JDK1.2.2, JDK1.3 and JDK1.4 on a Win2000/450Mhz machine is plotted here:


(All numbers are seconds)

CopyFile1.java:

import java.io.*;
 
public class CopyFile1
{
   public static void main(String []args) throws IOException {
      if (args.length != 2) {
         System.err.println("Usage: java CopyFile1 source dest");
         System.exit(1);
      }
 
      String source = args[0];
      String dest = args[1];
 
      long start = System.currentTimeMillis();
 
      BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
      BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest));
 
      int k;
      while ((k = bis.read()) != -1) {
         bos.write(k);
      }
   
      bis.close();
      bos.close();
 
      long stop = System.currentTimeMillis();
 
      System.out.println("Total time it took to copy: " + (stop - start) + "ms.");
   }
}

CopyFile2.java:

import java.nio.*;
import java.nio.channels.*;
import java.io.*;
 
public class CopyFile2
{
   public static void main(String []args) throws IOException {
      if (args.length != 2) {
         System.err.println("Usage: java CopyFile2 source dest");
         System.exit(1);
      }
 
      String source = args[0];
      String dest = args[1];
 
      long start = System.currentTimeMillis();
 
      FileInputStream fis = new FileInputStream(source);
      FileOutputStream fos = new FileOutputStream(dest);
      
      FileChannel channelIn = fis.getChannel();
      FileChannel channelOut = fos.getChannel();
 
      ByteBuffer buffer = ByteBuffer.allocateDirect(2048); 
 
      int n = channelIn.read(buffer);
      while (n > -1) {
         buffer.flip();
         channelOut.write(buffer);
         buffer.clear();
  
         n = channelIn.read(buffer);
      }      
 
      long stop = System.currentTimeMillis();
 
      System.out.println("Total time it took to copy: " + (stop - start) + "ms.");
   }
}

CopyFile3.java:

import java.nio.*;    
import java.nio.channels.*;    
import java.io.*;        
 
public class CopyFile3 
{        
   public static void main(String args[]) throws IOException {                
      if (args.length != 2) {
         System.err.println("Usage: java CopyFile3 source dest");
         System.exit(1);
      }             
 
      String source = args[0];
      String dest = args[1];
 
      long start = System.currentTimeMillis();
 
      FileInputStream fis = new FileInputStream(source);            
      FileOutputStream fos = new FileOutputStream(dest);            
      
      FileChannel channelIn = fis.getChannel();
      FileChannel channelOut = fos.getChannel();
 
      channelIn.transferTo(0, channelIn.size(), channelOut);                
 
      channelIn.close();            
      channelOut.close();            
 
      long stop = System.currentTimeMillis();
 
      System.out.println("Total time it took to copy: " + (stop - start) + "ms.");
   }    
}

Native byte order on your machine

Simple, in JDK1.4+, use java.nio.ByteOrder.nativeOrder()

Main.java:

import java.nio.*;
 
public class Main
{
   public static void main(String []args) {
      ByteOrder bo = ByteOrder.nativeOrder();
      
      System.out.println("Native byte order of your platform is " + bo);
   }
}

outputs on my win machine:

Native byte order of your platform is LITTLE_ENDIAN

What is an NIO Buffer?

A buffer is used to transfer data between channels.

Internal state

Internally, a buffer is an array of data, where the type is determined by the subclass of java.nio.Buffer that is used: ShortBuffer, IntBuffer, FloatBuffer, DoubleBuffer, LongBuffer, CharBuffer and ByteBuffer.

A buffer maintains three essential properties: capacity, limit and position. Look at the example below for how these properties behave.

  • capacity: is the number of elements the buffer can contain. Once a buffer is allocated, its capacity never changes.

  • limit: the index of the first element that should not be read or written. In other words, elements are usable and can be read up to the index limit-1. For writing, the limit is typically equal to the capacity of the Buffer.
  • position: the index of the next element to be read or written. It is never greater than the limit.

    Creating Buffers

    You can’t directly create Buffers using a constructor.

    Example:

       // allocates memory that can contain 128 bytes
       ByteBuffer byteBuffer = ByteBuffer.allocate(128);
       // prints out 128
       System.out.println(byteBuffer.capacity());
     
       // allocates memory that can contain 128 integers
       IntBuffer intBuffer = IntBuffer.allocate(128);
       // prints out 128
       System.out.println(intBuffer.capacity());
    
  • You can also create a view on an existing ByteBuffer. The two buffers, the original and the view will operate on the same memory, so changing in one buffer will have direct effect in the other one.

    Example:

       // allocates memory that can contain 128 bytes
       ByteBuffer byteBuffer = ByteBuffer.allocate(128);
       // prints out 128
       System.out.println(byteBuffer.capacity());
    
       // Create an intBuffer view on the byteBuffer
       IntBuffer intBuffer = byteBuffer.asIntBuffer();
       // prints out 32, because 128 bytes is 32 integers of 4 bytes each
       System.out.println(intBuffer.capacity());
    
  • To create a Buffer from an existing array, use the wrap method. Changing the original array will change the Buffer.

    WrapExample.java:

    import java.nio.*;
     
    public class WrapExample
    {
       public static void main(String []args) {
          long la[] = { 10, 20, 30, 40 };  
          LongBuffer lb = LongBuffer.wrap(la);
     
          System.out.println("LongBuffer before changing original array");
          printBuffer(lb);
     
          // change the original array
          la[2] = 123456;
     
          System.out.println("LongBuffer after changing original array");
          printBuffer(lb);
       }
       
       public static void printBuffer(LongBuffer lb) {
          for (int i=0; i<lb.limit(); i++) {
             System.out.print(lb.get(i) + " ");
          }
          System.out.println();
       }
    }
    

    Reading/writing from and to a Buffer

    To read and write values from a buffer, you use that buffer’s specific get and put methods, that you can use in relative or absolute mode.

    The following example creates a CharBuffer and writes data to it.

    CharBufferTest.java:

    import java.nio.*;
     
    public class CharBufferTest 
    {
       public static void main(String []args) {
          CharBuffer cb = CharBuffer.allocate(10);
     
          cb.put('a'); 	// use relative put
          cb.put(3, 'b');	// use absolute put
          cb.put('c');	// use relative put
    
          printBuffer(cb);
       }
      
       public static void printBuffer(CharBuffer cb) {
          for (int i=0; i<cb.limit(); i++) {
             System.out.print(cb.get(i) + " ");
          }
          System.out.println();
       }
    } 
    

    outputs:

    a c   b
    

    Buffer Operations

    • flip(): Sets the limit to the current position and the position to zero. This method is typically used after a channel-read (put-operations in Buffer terms) and before a channel-write (get-operations in Buffer terms).

    Example buffer:
     
                          position     limit
                             |           |
           +---+---+---+---+---+---+---+
           | 2 | 4 | 3 | 9 | 0 | 0 | 0 |
           +---+---+---+---+---+---+---+
     
    After flip():
     
          position         limit
             |               |
           +---+---+---+---+---+---+---+
           | 2 | 4 | 3 | 9 | 0 | 0 | 0 |
           +---+---+---+---+---+---+---+
     
    Typical usage:
     
       inChannel.read(buffer);      // transfers data from the channel to the buffer
       flip();                      // prepares buffer for relative get-operations
       outChannel.write(buffer);    // transfers data from the buffer to the channel
    
  • clear(): Sets the limit to the Buffer’s capacity and the position to zero. This method is typically used after a channel-write (get-operations in Buffer terms) and before a channel-read (put-operations in Buffer terms).
    Example buffer that was just used by an out channel:
     
                          position
                           limit
                             |
           +---+---+---+---+---+---+---+
           | 2 | 4 | 3 | 9 | 0 | 0 | 0 |
           +---+---+---+---+---+---+---+
     
    After clear():
     
         position                      limit
             |                           |
           +---+---+---+---+---+---+---+
           | 2 | 4 | 3 | 9 | 0 | 0 | 0 |
           +---+---+---+---+---+---+---+
      
    Typical usage:
      
       outChannel.write(buffer);    // transfers data from the buffer to the channel
       clear();                     // prepares buffer for relative put-operations
       inChannel.read(buffer);      // transfers data from the channel to the buffer
    
  • rewind(): Sets the position to zero
  • position(int): with this method, you can assign a new index to the position. If the position is larger than the current limit, an IllegalArgumentException is thrown.
  • limit(int): with this method, you can assign a new index to the limit. The position is set to this limit if it is larger (the position is never larger than the limit).

    Here is an example of the different operations.

    Main.java:

    import java.nio.*;
     
    public class Main
    {
       public static void main(String []args) {
          ByteBuffer bb = ByteBuffer.allocate(10);
     
          printVars("Initial state", bb);
     
          bb.put((byte) 1);
          printVars("bb.put((byte) 1)", bb);
     
          bb.putFloat(3.4f);
          printVars("bb.putFloat(3.4f)", bb);
     
          bb.flip();
          printVars("flip()", bb);
     
          bb.put((byte) 2);
          printVars("bb.put((byte) 2)", bb);
     
          bb.position(5);
          printVars("bb.position(5)", bb);
     
          bb.clear();
          printVars("clear()", bb);
       }
     
       public static void printVars(String state, ByteBuffer bb) {
          System.out.println(state);
          printTag("limit", bb.limit());
          printTag("position", bb.position());
          printLine(bb);
          System.out.print("t| ");
          for (int i=0; i<bb.capacity(); i++) {
             String hexValue = getHexValue(bb, i);
             System.out.print(" " + hexValue.toUpperCase() + " | ");
          }
          System.out.println();
          printLine(bb);
          System.out.println("tCapacity: " + bb.capacity() + 
                             ", remaining: " + bb.remaining());
          System.out.println();
       }
     
       public static void printLine(ByteBuffer bb) {
          System.out.print("t+");
          for (int i=0; i<bb.capacity(); i++) {
             System.out.print("-----+");
          }
          System.out.println();
       }
     
       public static void printTag(String tag, int pos) {
          System.out.print("t   ");
          for (int i=0; i<pos; i++) {
             System.out.print("      ");
          }
          System.out.println(tag);
          System.out.print("t   ");
          for (int i=0; i<pos; i++) {
             System.out.print("      ");
          }
          System.out.println("|");
       }
     
       public static String getHexValue(ByteBuffer bb, int pos) {
          String hexValue = "??";
          if (pos >= bb.limit()) {
             hexValue = "??";
          }
          else {
             hexValue = Integer.toHexString(bb.get(pos));
          }
          if (hexValue.length() > 2) {
             hexValue = hexValue.substring(hexValue.length()-2);
          }
          else if (hexValue.length() == 1) {
             hexValue = "0" + hexValue;
          }
          return hexValue;
       }
    }
    

    outputs:

    Initial state
    	                                                               limit
    	                                                               |
    	   position
    	   |
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	|  00 |  00 |  00 |  00 |  00 |  00 |  00 |  00 |  00 |  00 | 
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	Capacity: 10, remaining: 10
     
    bb.put((byte) 1)
    	                                                               limit
    	                                                               |
    	         position
    	         |
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	|  01 |  00 |  00 |  00 |  00 |  00 |  00 |  00 |  00 |  00 | 
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	Capacity: 10, remaining: 9
     
    bb.putFloat(3.4f)
    	                                                               limit
    	                                                               |
    	                                 position
    	                                 |
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	|  01 |  40 |  59 |  99 |  9A |  00 |  00 |  00 |  00 |  00 | 
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	Capacity: 10, remaining: 5
     
    flip()
    	                                 limit
    	                                 |
    	   position
    	   |
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	|  01 |  40 |  59 |  99 |  9A |  ?? |  ?? |  ?? |  ?? |  ?? | 
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	Capacity: 10, remaining: 5
     
    bb.put((byte) 2)
    	                                 limit
    	                                 |
    	         position
    	         |
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	|  02 |  40 |  59 |  99 |  9A |  ?? |  ?? |  ?? |  ?? |  ?? | 
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	Capacity: 10, remaining: 4
     
    bb.position(5)
    	                                 limit
    	                                 |
    	                                 position
    	                                 |
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	|  02 |  40 |  59 |  99 |  9A |  ?? |  ?? |  ?? |  ?? |  ?? | 
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	Capacity: 10, remaining: 0
     
    clear()
    	                                                               limit
    	                                                               |
    	   position
    	   |
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	|  02 |  40 |  59 |  99 |  9A |  00 |  00 |  00 |  00 |  00 | 
    	+-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
    	Capacity: 10, remaining: 10
    

  • In NIO, what is the difference between a mandatory lock and an advisory lock?

    A lock makes it possible to lock an entire file or a region of a file. Some filesystems implement advisory locking, which means that programs must all work together in order to preserve file integrity. It’s similar to having a synchronized block, which doesn’t really prevent your data from being corrupted, but just prevents threads to acquire the same lock.
    If mandatory locking is implemented in a filesystem, a process cannot access a file or a region on a file in a way that would violate the lock.

    Configuring Tomcat to use SSL

    SSL stands for Secure Socket Layer. It allows the communication between your browser and the webserver to be encrypted. Tomcat is a Servlet/JSP container but is just a simple webserver as well. Typically, the Apache web server is used to serve static pages while the servlet/JSP requests are redirected to the Tomcat container. Then you can simply configure SSL on the Apache Web server level, in which case you would not need to do SSL configuration on Tomcat itself. It is useful however if you are running Tomcat standalone.

    1) Install JSSE

    If you are not using JDK1.4+, you need to install JSSE separately. I was using JDK1.3.1 to create this example, and to make it easy, I copied to files jcert.jar, jnet.jar, jsse.jar into c:\jdk1.3.1\jre\lib\ext. If you do this, you don’t need to fiddle around with the classpath. You can d/l JSSE here: http://java.sun.com/products/jsse/.

    2) Create a keystore file that contains your certificate:

    C:\Program Files\Apache Tomcat 4.0>keytool -genkey -alias tomcat -keyalg RSA 
                                               -keystore tomcatkeystore.kst
    Enter keystore password:  123456
    What is your first and last name?
      [Unknown]:  Joris Van den Bogaert
    What is the name of your organizational unit?
      [Unknown]:  Esus
    What is the name of your organization?
      [Unknown]:  Esus
    What is the name of your City or Locality?
      [Unknown]:  Brussels
    What is the name of your State or Province?
      [Unknown]:
    What is the two-letter country code for this unit?
      [Unknown]:  BE
    Is <CN=Joris Van den Bogaert, OU=Esus, O=Esus, L=Brussels, ST=Unknown, C=BE> correct?
      [no]:  yes
     
    Enter key password for <tomcat>
            (RETURN if same as keystore password):  123456
    

    3) Modify TOMCAT-HOME/bin/server.xml

    You can just uncomment the SSL connector. Modify it so that it points to the keystore file that you just created:

        <!-- Define an SSL HTTP/1.1 Connector on port 8443 -->
        <Connector className="org.apache.catalina.connector.http.HttpConnector"
                   port="8443" minProcessors="5" maxProcessors="75"
                   enableLookups="true"
    	       acceptCount="10" debug="0" scheme="https" secure="true">
          <Factory className="org.apache.catalina.net.SSLServerSocketFactory"
                   clientAuth="false" protocol="TLS"
                   keystoreFile="tomcatkeystore.kst" 
                   keystorePass="123456"/>
        </Connector>
    

    Note 1: 8443 is the port number that Tomcat will listen to for secure connections. If you want
    to use another port number, make sure you also change the redirectPort attribute in the non-SSL connector to point to the port you choose.

    Note 2: If you didn’t specify the option -keystore when creating your certificate, the keystore would have been stored in your home directory as .keystore. In that case, you don’t need to specify the keystoreFile nor keystorePass.

    4) Restart Tomcat

    5) Check it out: https://localhost:8443

    Changing the default session timeout with Tomcat

    You can specify the session timeout in your web.xml deployment descriptor. The <session-timeout%gt; tag specifies the number of minutes of inactivity that the container will allow before the HttpSession object becomes invalid.

    eg.

        <session-config>
            <session-timeout>
                30
            </session-timeout>
        </session-config>
    

    You can also set your own timeout interval in your servlet or jsp:

    <html>
    <body>
    <% 
       session.setMaxInactiveInterval(30);
    %>
    </body>
    </html>
    

    Using the JDBCRealm

    Tomcat supports Container Managed Security. You don’t need to package your Servlets or JSPs with ugly authentication code, let Tomcat do it for you!

    A Realm is a simply a collection of usernames, passwords and roles. This Q/A deals with how to store this information in a database table and integrate it with Tomcat.

    A user has a password and can have multiple roles. You can give access to a resource (or a set of resources, eg. *.jsp) by associating it with one or more roles.

    For example: suppose you want all JSP’s under the subdirectory /protected to be accessible by only the users that have the (custom) role member. You can declaratively do this in web.xml:

    <web-app>   
       <security-constraint>      
          <web-resource-collection>         
             <web-resource-name>admin</web-resource-name>         
             <url-pattern>/protected/*.jsp</url-pattern>      
          </web-resource-collection>      
          <auth-constraint>         
             <role-name>member</role-name>         
          </auth-constraint>   
       </security-constraint>   
    </web-app>
    

    So, where are the users/passwords that have the member role? That depends on the type of Realm you’re using. A MemoryRealm stores this information in a file (see TOMCAT-HOME/conf/tomcat-users.xml). A JDBCRealm uses a database.

    So let’s create a web application with the protected directory protected. All members having either role silver or role gold are allowed to access the resources in that directory.

    Here is how our new web.xml looks like:

    <?xml version="1.0" encoding="ISO-8859-1"?>
    
    <!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/j2ee/dtds/web-app_2_3.dtd">
    
    <web-app>   
       <security-constraint>      
          <web-resource-collection>         
             <web-resource-name>admin</web-resource-name>         
             <url-pattern>/protected/*</url-pattern>      
          </web-resource-collection>      
          <auth-constraint>         
             <role-name>Silver</role-name>         
             <role-name>Gold</role-name>         
          </auth-constraint>   
       </security-constraint>
       
       <login-config>
          <auth-method>BASIC</auth-method>      
          <realm-name>Log In</realm-name>
       </login-config>
    </web-app>
    

    Note the login-config tag. It specifies how a user should authenticate. We use BASIC authentication which will cause a popup to appear whenever an unauthenticated user tries to access one of the protected resources:

    Authentication information will be stored in a database and accessed with a JDBC driver. In this example, we use mysql with the driver MySQL Connector/J (formerly mm.mysql).
    Download the JAR file (mm.mysql-2.0.4-bin.jar) and store it in [TOMCAT-HOME]/common/lib, if it will be only visible by your web applications or [TOMCAT-HOME]/server/lib if it’s used by Tomcat 4 as well.

    Modify [TOMCAT-HOME]/conf/server.xml to configure Tomcat to use the database as an authentication mechanism. You can host the following tag in the Engine, Host or Context element, depending on the scope you want (inside Engine = share across all web applications and all virtual hosts, inside Host = share accross all web applications of that particular host, inside Context = use this Realm only for this web application).

          <Realm className="org.apache.catalina.realm.JDBCRealm" debug="00"
                 driverName="org.gjt.mm.mysql.Driver"
                 connectionURL="jdbc:mysql://localhost/esusdb"
                 connectionName=""
                 connectionPassword=""
                 userTable="members"
                 userNameCol="username"
                 userCredCol="password"
                 userRoleTable="memberroles"
                 roleNameCol="role" /> 
    
    • driverName specifies the JDBC driver to be used (added previously to the lib directory).
    • connectionURL specifies the database URL to connect to.
    • connectionName and connectionPassword are needed if your database is password protected.
    • userTable specifies the table in your database in which authentication information is stored. It should contain a user per row, with the username/password stored in the columns specified in userNameCol and userCredCol.
    • userRoleTable specifies the table in your database that contains an association between the users and their roles. It should contain at least the columns specified in userNameCol and roleNameCol.

    Now create the necessary mysql database tables:

    C:mysqlbin>mysql
    Welcome to the MySQL monitor.  Commands end with ; or g.
    Your MySQL connection id is 5 to server version: 3.23.47-nt
     
    Type 'help;' or 'h' for help. Type 'c' to clear the buffer.
     
    mysql> create database esusdb;
    Query OK, 1 row affected (0.00 sec)
     
    mysql> use esusdb;
    Database changed
    mysql> create table members (uid int(10) not null primary key, username varchar(
    20) not null, password varchar(20) not null);
    Query OK, 0 rows affected (0.00 sec)
     
    mysql> create table memberroles (username varchar(20) not null, role varchar(10)
     not null, primary key (username, role));
    Query OK, 0 rows affected (0.01 sec)
     
    mysql> desc members;
    +----------+-------------+------+-----+---------+-------+
    | Field    | Type        | Null | Key | Default | Extra |
    +----------+-------------+------+-----+---------+-------+
    | uid      | int(10)     |      | PRI | 0       |       |
    | username | varchar(20) |      |     |         |       |
    | password | varchar(20) |      |     |         |       |
    +----------+-------------+------+-----+---------+-------+
    3 rows in set (0.00 sec)
     
    mysql> desc memberroles;
    +----------+-------------+------+-----+---------+-------+
    | Field    | Type        | Null | Key | Default | Extra |
    +----------+-------------+------+-----+---------+-------+
    | username | varchar(20) |      | PRI |         |       |
    | role     | varchar(10) |      | PRI |         |       |
    +----------+-------------+------+-----+---------+-------+
    2 rows in set (0.02 sec)
     
    mysql>
    

    …and insert some data:

     
    mysql> insert into members values (1, 'John', '123456');
    Query OK, 1 row affected (0.00 sec)
     
    mysql> insert into members values (2, 'Lisa', '654321');
    Query OK, 1 row affected (0.00 sec)
     
    mysql> insert into memberroles values ('John', 'Gold');
    Query OK, 1 row affected (0.00 sec)
     
    mysql> insert into memberroles values ('Lisa', 'Silver');
    Query OK, 1 row affected (0.00 sec)
     
    mysql>
    

    We have created two users: John with role Gold and Lisa with role Silver.

    Our web application will use the JDBCRealm to connect to this database and authenticate users.
    To create the example web application, create a directory structure as follows:

    loginexample2
       |
       +- index.jsp
       |
       +--- protected
       |       |
       |       +- confidential.jsp
       |
       +--- WEB-INF
               |
               +- web.xml
    

    Use the deployment descriptor web.xml that is shown above. The rest of the example JSP’s can be cut’n’pasted here:

    loginexample2/index.jsp:

    <html>
    <body>
    Access <a href="./protected/confidential.jsp">protected resource</a>
    </body>
    </html>
    

    loginexample2/protected/confidential.jsp:

    <html>
    <body>
    <%
       if (request.isUserInRole("Gold")) {
    %>
       You have the GOLD role<br>
    <% 
       }
       if (request.isUserInRole("Silver")) {
    %>
       You have the SILVER role<br>
    <%
       }
    %>
    <br> 
    You have successfully accessed the protected resource!
    </body>
    </html>
    

    Go to http://localhost:8080/loginexample2/index.jsp and try to access the protected resource. A popup window will show up. Notice that, once you’re logged in, you cannot log out, except by closing the browser window.

    Running Tomcat with a security manager

    Create a web application that contains the following jsp:

    <html>
    <body>
    Trying to shutdown Tomcat, please press reload.
    <%
       System.exit(1);
    %>
    </body>
    </html>
    

    Run Catalina (eg. catalina run) and load up the jsp. Notice in the Tomcat console that Tomcat has exited. What happens is that, by default, Tomcat is started without a security manager. The JSP, that was compiled into a servlet, runs in the same Virtual Machine as Tomcat itself, and System.exit causes the currently running VM to exit.

    To prevent this from happening, run Tomcat with a Security Manager to not permit web applications to perform these kinds of operations. The security policy file used by Catalina is catalina.policy located in the [TOMCAT-HOME]/conf directory.

    If you start Catalina again with the option -security (eg. catalina run -security, or startup -security), catalina.policy is taken into account.

    If you then load the jsp, you would get the following error message in your browser window:

    java.security.AccessControlException: access denied (java.lang.RuntimePermission exitVM)
    	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:272)
    	at java.security.AccessController.checkPermission(AccessController.java:399)
    	at java.lang.SecurityManager.checkPermission(SecurityManager.java:545)
    	at java.lang.SecurityManager.checkExit(SecurityManager.java:765)
    	at java.lang.Runtime.exit(Runtime.java:91)
    	at java.lang.System.exit(System.java:701)
    	at org.apache.jsp.ExitTomcat$jsp._jspService(ExitTomcat$jsp.java:59)
    	at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:107)
    	at javax.servlet.http.HttpServlet.service(HttpServlet.java:853)
    . . .