alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*   
 *  Copyright 1999-2004 The Apache Sofware Foundation.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.tomcat.modules.mappers;

import java.io.File;
import java.util.Enumeration;
import java.util.Hashtable;

import org.apache.tomcat.core.BaseInterceptor;
import org.apache.tomcat.core.Container;
import org.apache.tomcat.core.Context;
import org.apache.tomcat.core.ContextManager;
import org.apache.tomcat.core.Request;
import org.apache.tomcat.core.TomcatException;
import org.apache.tomcat.util.buf.MessageBytes;
import org.apache.tomcat.util.collections.SimpleHashtable;
import org.apache.tomcat.util.io.FileUtil;
/**
 *  This class will set up the data structures used by a simple patern matching
 *  algorithm and use it to extract the path components from the request URI.
 *
 *  This particular implementation does the following:
 *  - extract the information that is relevant to matching from the Request
 *   object. The current implementation deals with the Host header and the
 *   request URI.
 *  - Use an external mapper to find the best match.
 *  - Adjust the request paths
 * 
 *  SimpleMapper1 will set 2 context notes - "map.extensions" is a
 *  SimpleHashtable containing the extension mappings, and "tomcat.map.default"
 *  for the default map, if defined explicitely.
 *
 *  It will also maintain a global mapping structure for all prefix mappings,
 *  including contexts. 
 * 
 *  The execution time is proportional with the number of hosts, number of
 *  context, number of mappings and with the length of the request.
 *
 */
public class SimpleMapper1 extends  BaseInterceptor  {
    ContextManager cm;

    PrefixMapper map;

    // We store the extension maps as per/context notes.
    int ctExtMapNote=-1;
    int defaultMapNOTE=-1;
    
    // Property for the PrefixMapper - cache the mapping results
    boolean mapCacheEnabled=false;
    
    
    public SimpleMapper1() {
	map=new PrefixMapper();
	ignoreCase= (File.separatorChar  == '\\');
	map.setIgnoreCase( ignoreCase );
    }

    /* -------------------- Support functions -------------------- */
    /** Allow the mapper to cache mapping results - resulting in a
     *  faster match for frequent requests. ( treat this as experimental)
     */
    public void setMapCache( boolean v ) {
	mapCacheEnabled = v;
	map.setMapCache( v );
    }

    // -------------------- Ingore case --------------------
    boolean ignoreCase=false;

    /** Use case insensitive match, for windows and
	similar platforms
    */
    public void setIgnoreCase( boolean b ) {
	ignoreCase=b;
	map.setIgnoreCase( b );
    }

    /* -------------------- Initialization -------------------- */
    
    /** Set the context manager. To keep it simple we don't support
     *  dynamic add/remove for this interceptor. 
     */
    public void engineInit( ContextManager cm )
	throws TomcatException
    {
	this.cm=cm;
	// set-up a per/container note for maps
	ctExtMapNote = cm.getNoteId( ContextManager.CONTAINER_NOTE,
				     "map.extension");
	defaultMapNOTE=cm.getNoteId( ContextManager.CONTAINER_NOTE,
				     "tomcat.map.default");
    }

    /** Called when a context is added.
     */
    public void addContext( ContextManager cm, Context ctx )
	throws TomcatException
    {
	map.addMapping( ctx.getHost(), ctx.getPath(), ctx.getContainer());
        map.addMappings( ctx.getHostAliases(), ctx.getPath(), ctx.getContainer());
    }

    /** Called when a context is removed from a CM - we must ask the mapper to
	remove all the maps related with this context
     */
    public void removeContext( ContextManager cm, Context ctx )
	throws TomcatException
    {
	if(debug>0) log( "Removed from maps ");
	map.removeAllMappings( ctx.getHost(), ctx);

        Enumeration vhostAliases=ctx.getHostAliases();
        while( vhostAliases.hasMoreElements() )
            map.removeAllMappings( (String)vhostAliases.nextElement(), ctx );

	// extension mappings are local to ctx, no need to do something
	// about that
    }
    

    /**
     * Associate URL pattern  to a set of propreties.
     * 
     * Note that the order of resolution to handle a request is:
     *
     *    exact mapped servlet (eg /catalog)
     *    prefix mapped servlets (eg /foo/bar/*)
     *    extension mapped servlets (eg *jsp)
     *    default servlet
     *
     */
    public void addContainer( Container ct )
	throws TomcatException
    {
	Context ctx=ct.getContext();
	String vhost=ctx.getHost();
        Enumeration vhostAliases=ctx.getHostAliases();
	String path=ct.getPath();
	String ctxP=ctx.getPath();

	// Special containers ( the default is url-mapping ).
	if( ct.isSpecial() ) return;
	if( ct.getNote( "type" ) != null )  return;
	
	if(ct.getRoles() != null || ct.getTransport() != null ) {
	    // it was only a security map, no handler defined
	    return;
	}

	switch( ct.getMapType() ) {
	case Container.PREFIX_MAP:
	    // cut /* ( no need to do a string concat for every match )
	    // workaround for frequent bug in web.xml ( backw. compat )
            if( ! path.startsWith( "/" ) ) {
                log("WARNING: Correcting error in web.xml for context \"" + ctxP +
                        "\". Mapping for path \"" + path + "\" is missing a leading '/'.");
                path="/" + path;
            }
            String prefixPath=ctxP + path.substring( 0, path.length()-2 );
	    map.addMapping( vhost, prefixPath, ct);
	    map.addMappings( vhostAliases, prefixPath, ct);

	    if( debug>0 )
		log("SM: prefix map " + vhost + ":" +  ctxP +
		    path + " -> " + ct + " " );
	    break;
	case Container.DEFAULT_MAP:
	    // This will be used if no other map match.
	    // AVOID USING IT - STATIC FILES SHOULD BE HANDLED BY
	    // APACHE ( or tomcat )
	    Container defMapC=ct.getContext().getContainer();

	    defMapC.setNote( defaultMapNOTE, ct );
	    if( debug>0 )
		log("SM: default map " + vhost + ":" +  ctxP +
		    path + " -> " + ct + " " );
	    break;
	case Container.EXTENSION_MAP:
	    // Add it per/defaultContainer - as spec require ( it may also be
	    // possible to support type maps per/Container, i.e. /foo/*.jsp -
	    // but that would require changes in the spec.
	    Context mapCtx=ct.getContext();
	    Container defC=mapCtx.getContainer();
	    
	    SimpleHashtable eM=(SimpleHashtable) defC.getNote( ctExtMapNote );
	    if( eM==null ) {
		eM=new SimpleHashtable();
		defC.setNote( ctExtMapNote, eM );
	    }
	    // add it to the Container local maps
	    if( ignoreCase )
		eM.put( path.substring( 1 ).toLowerCase() , ct );
	    else
		eM.put( path.substring( 1 ), ct );
	    if(debug>0)
		log( "SM: extension map " + ctxP + "/" +
		     path + " " + ct + " " );
	    break;
	case Container.PATH_MAP:
	    // workaround for frequent bug in web.xml
            if( ! path.startsWith( "/" ) ) {
                log("WARNING: Correcting error in web.xml for context \"" + ctxP +
                        "\". Mapping for path \"" + path + "\" is missing a leading '/'.");
                path="/" + path;
            }
	    map.addExactMapping( vhost, ctxP + path, ct);
	    map.addExactMappings( vhostAliases, ctxP + path, ct);
	    if( debug>0 )
		log("SM: exact map " + vhost + ":" + ctxP +
		    path + " -> " + ct + " " );
	    break;
	}
    }

    // XXX not implemented - will deal with that after everything else works.
    // Remove context will still work
    public void removeContainer( Container ct )
	throws TomcatException
    {
	Context ctx=ct.getContext();
	String mapping=ct.getPath();
	String ctxP=ctx.getPath();
        mapping = mapping.trim();
	if(debug>0) log( "Remove mapping " + mapping );
    }


    /* -------------------- Request mapping -------------------- */


    /** First step of request processing is finding the Context.
     */
    public int contextMap( Request req ) {
	MessageBytes pathMB = req.requestURI();
	try {
	    //	    String host=null;
	    MessageBytes hostMB=req.serverName();

	    //	    host=req.serverName().toString();

	    if(debug>0) cm.log("Host = " + hostMB.toString());

	    Container container =(Container)map.
		getLongestPrefixMatch(  hostMB, pathMB);
	    
	    if( container == null ) {
		// It is too easy to configure Tomcat w/o a default context
		// Add this back in when we do better checking
		//throw new RuntimeException( "Assertion failed: " +
		//			    "container==null");
		cm.log("Assertion failed: " +
		       "container==null (no Default Context?)");
		return 404;
	    }

	    if(debug>0)
		cm.log("SM: Prefix match " + pathMB.toString() + " -> " +
		       container.getPath() + " " + container.getHandler()  +
		       " " + container.getRoles());

	    // Once - adjust for prefix and context path
	    // If cached - we don't need to do it again ( since it is the
	    // final Container,
	    // either prefix or extension )
	    fixRequestPaths( pathMB.toString() /*XXX*/, req, container );
	

	    // if it's default container - try extension match
	    //	    if (  container.getMapType() == Container.DEFAULT_MAP ) {
	    if (  container.getHandler() == null ) {
		Container extC = matchExtension( req );
	
		if( extC != null ) {
		    // change the handler
		    if( extC.getHandler() != null ) {
			fixRequestPaths( pathMB.toString(), req, extC );
			container=extC;
		    }
		    if( debug > 0 )
			log("SM: Found extension mapping " +
			    extC.getHandler());
		    // change security roles
		}
	    }
	    
	    // Default map - if present
	    if( container.getHandler() == null ) {
		Container ctxDef=req.getContext().getContainer();
		Container defC=(Container)ctxDef.getNote( defaultMapNOTE );
		if( defC != null && defC.getHandler() !=null ) {
		    fixRequestPaths( pathMB.toString(), req, defC );

		    if( debug > 0 )
			log("SM: Found default mapping " +
			    defC.getHandler() + " " + defC.getPath() +
			     " " + defC.getMapType());
		}
	    }

	    if(debug>0) log("SM: After mapping " + req + " " +
			    req.getHandler());

	} catch(Exception ex ) {
	    log("Mapping " + req, ex);
	    return 500;
	}
	return 0;
    }
    
    /** No need to do that - we finished everything in the first step.
     *  
     */
    //    public int requestMap(Request req) {
	// No op. All mapping is done in the first step - it's better because
	// the algorithm is more efficient. The only case where those 2 are
	// not called togheter is in getContext( "path" ). 
	// 
	// We can split it again later if that creates problems - but right
	// now it's important to have a clear algorithm. Note that requestMap
	// is _allways_ called after contextMap ( it was asserted in  all
	// implementations).
	
    // 	return 0;
    //     }

    // -------------------- Implementation methods --------------------
    
    /** Will match an extension - note that Servlet API use special rules
     *  for mapping extension, different from what is used in existing web
     * servers. That makes this code very easy ( only need to deal with
     * the last component of the name ), but it's hard to integrate and you
     * have no way to use pathInfo.
     */
    Container matchExtension( Request req ) {
	Context ctx=req.getContext();
	String ctxP=ctx.getPath();
	
	// we haven't matched any prefix,
	String path = req.servletPath().toString(); 
	if( path == null ) return null;

	String extension=FileUtil.getExtension( path );
	if( extension == null ) return null;

	if(debug>0)
	    cm.log("SM: Extension match " + ctxP +  " " +
		   path + " " + extension );

	// Find extension maps for the context
	SimpleHashtable extM=(SimpleHashtable)ctx.
	    getContainer().getNote( ctExtMapNote );
	if( extM==null ) return null;

	// Find the container associated with that extension
	if( ignoreCase ) extension=extension.toLowerCase();
	Container container= (Container)extM.get(extension);

	if (container == null)
	    return null;

	// This container doesn't change the mappings - it only 
	// has "other" properties ( in the current code security
	// constraints 
	if( container.getHandler() == null) return container;

	return container; 
    }

    /** Adjust the paths in request after matching a container
     */
    void fixRequestPaths( String path, Request req, Container container )
	throws Exception
    {
	// Set servlet path and path info
	// Found a match !
	// Adjust paths based on the match
	String s=container.getPath();
	String ctxP=container.getContext().getPath();
	int sLen=s.length();
	int pathLen=path.length();
	int ctxPLen=ctxP.length();
	String pathI=null;
		// Perform URL decoding only if necessary

	switch( container.getMapType()) {
	case  Container.PREFIX_MAP: 
	    s=s.substring( 0, sLen -2 );
	    pathI= path.substring( ctxPLen + sLen - 2, pathLen);
	    if( debug>0 ) log( "Adjust for prefix map " + s + " " + pathI );
	    break;
	case Container.DEFAULT_MAP:
            s = path.substring( ctxPLen );
            pathI = null;
	    if( debug>0 ) log( "Default map " + s + " " + pathI );
	    break;
	case Container.PATH_MAP:
	    pathI= null;
	    // For exact matching - can't have path info ( or it's 
	    // a prefix map )
	    //path.substring( ctxPLen + sLen , pathLen);
	    if( debug>0 ) log( "Adjust for path map " +
			       s + " " + pathI + container.getPath());
	    break; // keep the path
	case Container.EXTENSION_MAP:
	    /*  adjust paths */
	    s= path.substring( ctxPLen );
	    pathI=null;

	}
	req.servletPath().setString( s );

	if( ! "".equals(pathI)) 
	    req.pathInfo().setString(pathI);
	Context ctx=container.getContext();
	req.setContext(ctx);
	req.setHandler( container.getHandler() );
	req.setContainer( container );
    }
    
}


/** Prefix and exact mapping algorithm.
 *XXX finish factoring out the creation of the map ( right now direct field access is
 *  used, since the code was just cut out from SimpleMapper).
 *  XXX make sure the code is useable as a general path mapper - or at least a bridge
 *  can be created between SimpleMapper and a patern matcher like the one in XPath
 *
 * @author costin@costin.dnt.ro
 */
class PrefixMapper  {
    private static int debug=1;
    // host -> PrefixMapper for virtual hosts
    // hosts are stored in lower case ( the "common" case )
    Hashtable vhostMaps=new Hashtable();
    // host -> PrefixMapper for virtual hosts with leading '*'
    // host key has '*' removed
    // Can't use SimpleHashtable, more than a thread will use keys()
    Hashtable vhostMapsWC=new Hashtable();
    boolean hasWCMap=false; // to avoid getting the Enumeration

    Hashtable prefixMappedServlets;
    Hashtable exactMappedServlets;

        // Cache the most recent mappings
    // Disabled by default ( since we haven't implemented
    // capacity and remove ). 
    SimpleHashtable mapCache;
    // By using TreeMap instead of SimpleMap you go from 143 to 161 RPS
    // ( at least on my machine )
    // Interesting - even if SimpleHashtable is faster than Hashtable
    // most of the time, the average is very close for both - it seems
    // that while the synchronization in Hashtable is locking, GC have
    // a chance to work, while in SimpleHashtable case GC creates big
    // peeks. That will go away with more reuse, so we should use SH.

    // An alternative to explore after everything works is to use specialized
    // mappers ( extending this one for example ) using 1.2 collections
    // TreeMap mapCache;
    boolean mapCacheEnabled=false;
    boolean ignoreCase=false;
    
    public PrefixMapper() {
	prefixMappedServlets=new Hashtable();
	exactMappedServlets=new Hashtable();
	mapCache=new SimpleHashtable();
    }

    public void setMapCache( boolean v ) {
	mapCacheEnabled=v;
    }

    public void setIgnoreCase( boolean b ) {
	ignoreCase=b;
    }
    
    /** Remove all mappings matching path
     */
    public void removeAllMappings( String host, Context ctx ) {
	PrefixMapper vmap=this;
	if( host!=null ) {
	    host=host.toLowerCase();
            if( host.startsWith( "*" ) )
                vmap=(PrefixMapper)vhostMapsWC.get(host.substring( 1 ));
            else
                vmap=(PrefixMapper)vhostMaps.get(host);
	}
	
	// remove all paths starting with path
	Enumeration en=vmap.prefixMappedServlets.keys();
	while( en.hasMoreElements() ) {
	    String s=(String)en.nextElement();
	    Container ct=(Container)vmap.prefixMappedServlets.get( s );
	    if( ct!=null && ct.getContext() == ctx ) {
		if(debug > 0 )
		    ctx.log( "Remove mapping " + s ); 
		vmap.prefixMappedServlets.remove( s );
	    }
	}
	
	en=vmap.exactMappedServlets.keys();
	while( en.hasMoreElements() ) {
	    String s=(String)en.nextElement();
	    Container ct=(Container)vmap.exactMappedServlets.get( s );
	    if( ct.getContext() == ctx ) {
		if(debug > 0 )
		    ctx.log( "Remove mapping " + s ); 
		vmap.exactMappedServlets.remove( s );
	    }
	}
	// reset the cache
	mapCache=new SimpleHashtable();
	
    }

    /**
     */
    void addMapping( String path, Object target ) {
	prefixMappedServlets.put( path, target);
    }

    /**
     */
    void addExactMapping( String path, Object target ) {
	exactMappedServlets.put( path, target);
    }
    
    /**
     */
    public void addMapping( String host, String path, Object target ) {
	if( host == null ) {
	    if( ignoreCase )
		prefixMappedServlets.put( path.toLowerCase(), target);
	    else
		prefixMappedServlets.put( path, target);
	} else {
	    host=host.toLowerCase();
            Hashtable maps;
            if( host.startsWith( "*" ) ) {
                maps=vhostMapsWC;
                host=host.substring( 1 );
		hasWCMap=true;
            } else {
                maps=vhostMaps;
            }
	    PrefixMapper vmap=(PrefixMapper)maps.get( host );
	    if( vmap == null ) {
		vmap=new PrefixMapper();
		vmap.setIgnoreCase( ignoreCase );
                maps.put( host, vmap );
		vmap.setMapCache( mapCacheEnabled );
	    }
	    if( ignoreCase ) 
		vmap.addMapping( path.toLowerCase(), target );
	    else
		vmap.addMapping( path, target );
	}
    }

    /**
     */
    public void addMappings( Enumeration hostAliases, String path, Object target ) {
        while ( hostAliases.hasMoreElements() )
            addMapping( (String)hostAliases.nextElement(), path, target );
    }

    /**
     */
    public void addExactMapping( String host, String path, Object target ) {
        if( host==null ) {
            if ( ignoreCase )
                exactMappedServlets.put( path.toLowerCase(), target);
            else
                exactMappedServlets.put( path, target);
        } else {
	    host=host.toLowerCase();
            Hashtable maps;
            if( host.startsWith( "*" ) ) {
                maps = vhostMapsWC;
                host=host.substring( 1 );
		hasWCMap=true;
            } else {
                maps = vhostMaps;
            }
	    PrefixMapper vmap=(PrefixMapper)maps.get( host );
	    if( vmap == null ) {
		vmap=new PrefixMapper();
		maps.put( host, vmap );
	    }
	    if( ignoreCase ) 
		vmap.addExactMapping( path.toLowerCase(), target );
	    else
		vmap.addExactMapping( path, target );
	}
    }

    /**
     */
    public void addExactMappings( Enumeration hostAliases, String path, Object target ) {
        while ( hostAliases.hasMoreElements() )
            addExactMapping( (String)hostAliases.nextElement(), path, target );
    }
   
    
    // -------------------- Implementation --------------------

    /** Match a prefix rule - /foo/bar/index.html/abc
     */
    public Object getLongestPrefixMatch( MessageBytes hostMB,
					 MessageBytes pathMB )
    {
	// XXX fixme
	String host=hostMB.toString();
	String path=pathMB.toString();
	Object container = null;

	PrefixMapper myMap=null;
	if( host!=null ) {
	    myMap=(PrefixMapper)vhostMaps.get( host );
	    if( myMap==null ) {
		myMap=(PrefixMapper)vhostMaps.get( host.toLowerCase() );
	    }
        }
        if( myMap==null && hasWCMap ) {
            // Check host against virtual hosts that began with '*'
            Enumeration vhosts = vhostMapsWC.keys();
            while(vhosts.hasMoreElements()) {
                String vhostName = (String)vhosts.nextElement();
                if(host.endsWith(vhostName)) {
                    myMap = (PrefixMapper)vhostMapsWC.get(vhostName);
                    break;
                }
            }
	}
	
	if( myMap==null ) myMap = this; // default server

	if( ignoreCase ) path=path.toLowerCase();
	container=myMap.exactMappedServlets.get( path );
	if( container != null ) return container; // and we're done!

	/** Cache for request results - exploit the fact that few
	 *  request are more "popular" than other.
	 *  Disable it if you want to benchmark the mapper !!!
	 */
	if( myMap.mapCacheEnabled ) {
	    container=myMap.mapCache.get(path);
	    if( container!=null ) return container;
	}
		
        String s = path;
	while (s.length() >= 0) {
	    //if(debug>8) context.log( "Prefix: " + s  );
	    container = myMap.prefixMappedServlets.get(s);
	    
	    if (container == null) {
		// if empty string didn't map, time to give up
		if ( s.length() == 0 )
                    break;
		s=FileUtil.removeLast( s );
	    }  else {
		if( myMap.mapCacheEnabled ) {
		    // XXX implement LRU or another replacement algorithm
		    myMap.mapCache.put( path, container );
		}
		return container;
	    }
	}
	return container;
    }

}

 
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.