/*
 * UrlMapperHandler.java
 *
 * Brazil project web application toolkit,
 * export version: 2.3 
 * Copyright (c) 2000-2008 Sun Microsystems, Inc.
 *
 * Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License Version 
 * 1.0 (the "License"). You may not use this file except in compliance with 
 * the License. A copy of the License is included as the file "license.terms",
 * and also available at http://www.sun.com/
 * 
 * The Original Code is from:
 *    Brazil project web application toolkit release 2.3.
 * The Initial Developer of the Original Code is: suhler.
 * Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
 * All Rights Reserved.
 * 
 * Contributor(s): cstevens, suhler.
 *
 * Version:  2.8
 * Created by suhler on 00/02/14
 * Last modified by suhler on 08/12/19 11:14:55
 *
 * Version Histories:
 *
 * 2.8 08/12/19-11:14:55 (suhler)
 *   make inner class static
 *
 * 2.7 08/06/24-10:45:19 (suhler)
 *   do more useful ${} substitutions in replace ment strings.
 *   .
 *
 * 2.6 07/03/26-10:28:38 (suhler)
 *   - add remappinging arbitrary http request header (instead of just a url)
 *   - enable access to all http headers for ${} substitutions
 *
 * 2.5 07/03/21-15:57:50 (suhler)
 *   add "target" property to URL rewriter, so it can rewrite arbitrary HTTP
 *   headers.
 *
 * 2.4 05/12/21-10:26:19 (suhler)
 *   better diagnostics
 *
 * 2.3 04/11/03-08:29:49 (suhler)
 *   url.orig is now set in Request.java
 *
 * 2.2 04/10/26-11:28:15 (suhler)
 *   bug fix - make sure "export" ends in '.'.
 *
 * 2.1 02/10/01-16:36:36 (suhler)
 *   version change
 *
 * 1.20 02/02/04-14:33:04 (suhler)
 *   Make "host" available to mapper to allow better support for
 *   "wildcard" websites (e.g. <anything>.foo.com -> same machine)
 *
 * 1.19 01/11/28-13:08:37 (suhler)
 *   add ${user-agent} for "source" options, add docs
 *
 * 1.18 01/08/14-16:38:11 (suhler)
 *   doc lint
 *
 * 1.17 01/03/15-10:48:22 (suhler)
 *   set "url.orig" property if the url is modified
 *
 * 1.16 00/12/18-11:24:49 (suhler)
 *   allow ${...} substitutions in replacement strings
 *   .
 *
 * 1.15 00/12/11-13:27:36 (suhler)
 *   add class=props for automatic property extraction
 *
 * 1.14 00/12/08-16:46:49 (suhler)
 *   doc fixes
 *   .
 *
 * 1.13 00/11/06-10:47:56 (suhler)
 *   added source option
 *
 * 1.12 00/10/05-15:51:34 (cstevens)
 *   PropsTemplate.subst() and PropsTemplate.getProperty() moved to the Format
 *   class.
 *
 * 1.11 00/10/05-11:08:00 (suhler)
 *   sub-matches of the url can be saved in the request properties for later use.
 *   This will be more useful when the properties macro facility is integrated in.
 *   .
 *
 * 1.10 00/08/16-12:11:57 (suhler)
 *   changed Format to PRopsTemplate.getPRoperty
 *   .
 *
 * 1.9 00/06/20-08:56:02 (suhler)
 *   doc fix
 *
 * 1.8 00/06/05-11:03:08 (suhler)
 *   doc fixes
 *
 * 1.7 00/05/24-11:25:58 (suhler)
 *   add docs
 *
 * 1.6 00/05/19-11:48:59 (suhler)
 *   doc clean-ups
 *
 * 1.5 00/05/12-16:19:05 (suhler)
 *   added missong boundary check
 *
 * 1.4 00/04/20-11:50:33 (cstevens)
 *   copyright.
 *
 * 1.3 00/04/17-16:32:38 (cstevens)
 *   Parameter subst on url match.
 *
 * 1.2 00/02/27-19:51:15 (suhler)
 *   fixed typos
 *
 * 1.2 00/02/14-15:00:03 (Codemgr)
 *   SunPro Code Manager data about conflicts, renames, etc...
 *   Name history : 1 0 handlers/UrlMapperHandler.java
 *
 * 1.1 00/02/14-15:00:02 (suhler)
 *   date and time created 00/02/14 15:00:02 by suhler
 *
 */

package sunlabs.brazil.handler;

import sunlabs.brazil.server.Handler;
import sunlabs.brazil.server.Request;
import sunlabs.brazil.server.Server;
import sunlabs.brazil.util.regexp.Regexp;
import sunlabs.brazil.util.Format;

import java.util.Dictionary;
import java.util.Properties;
import java.io.IOException;

/**
 * Handler for mapping URL's or HTTP headers, or redirecting URLs
 * based on the contents of the current HTTP request.
 * Matches URL's (or arbitrary request properties) against a regexp pattern.
 * If there is a match, the URL
 * (or specified HTTP header)
 * is rewritten or the URL is redirected.
 * <p>
 * Properties:
 * <dl class=props>
 * <dt>match	<dd>The regexp to match a url.  May contain constructs
 *		of the form ${xxx}, which are replaced by the value of
 *		<code>request.props</code> for the key <i>xxx</i>
 * <dt>replace  <dd>The url to replace it with.  This may contain both
 *		regular expression sub-patterns, such as "\1", or
 *		variables of the form ${..} which are replaced with
 *		the equivalent augmented request properties. The
 *		request properties are augmented by looking first for
 *		the special variables:
 *		"method", "url", "protocol", "query", "serverUrl", "hostname",
 * 		"hostport".
 *		then in the HTTP headers, then in the request properties.
 *		The special variables "hostname" and "hostport" are derived
 *		from the http "host" header.
 * <dt>export   <dd>If set, use this as a properties prefix, and set 
 *		    request properties for each sub-expression in "match".
 *                  (E.g. [export]1 [export]2 ...).
 * <dt>redirect <dd>If set, the request is redirected instead of being rewritten
 * <dt>ignoreCase <dd>If set, the case of the expression is ignored.
 * <dt>source
 * <dd>
 * If set, then this string is used instead of the
 * url as the <i>source</i> of the match. Variable substitution
 * using ${xxx} is performed on <i>source</i>, which, if unset, 
 * defaults to "${url}".  If set, ${} substitutions 
 * are taken from the special variables (see "replace", above), followed
 * by any http request headers, followed by names in the current Request
 * object.
 * The <i>source</i> property
 * is obtained at init time, but evaluated (for ${...}) at every request.
 * <p>
 * As an example, the configuration:<br><code>
 * prefix.source=${user-agent}!${url}<br>
 * prefix.match=Lynx.*!(.*)<br>
 * prefix.replace=/text\\1<br></code>
 * could cause all browsers with "Lynx" in their user agent header
 * to the "text" sub-directory.
 * <dt>target</dt>
 * <dd>By default, this handler modifies the request URL.  If target
 * is specified, it names an HTTP header to be replaced instead of
 * the URL.  The "target" is ignored if "redirect" is specified, and
 * a new header is created if the "target" header doesn't already exist.
 * </dl>
 *
 * @author      Stephen Uhler
 * @version     2.8, 08/12/19
 */

public class UrlMapperHandler implements Handler {
    final static String MATCH = "match";
    final static String SOURCE = "source";
    final static String REPLACE = "replace";
    final static String REDIRECT = "redirect";
    final static String NOCASE = "ignoreCase";
    final static String EXPORT = "export";
    final static String TARGET = "target";

    Regexp re = null;		// the expression to match
    String replace;		// the replacement string
    String export;		// the prefix for exported submatches
    String source;		// the source for the match
    String prefix;		// our properties prefix
    String target;		// the replacement target (e.g. the URL)
    boolean redirect = false;	// True to redirect instead of rewrite
    Server server;

    public boolean
    init(Server server, String prefix) {
	this.server=server;
	this.prefix = prefix;
	boolean noCase = (server.props.getProperty(prefix + NOCASE) != null);
	redirect = (server.props.getProperty(prefix + REDIRECT) != null);
	replace = server.props.getProperty(prefix + REPLACE);
	export = server.props.getProperty(prefix + EXPORT);
	source = server.props.getProperty(prefix + SOURCE);
	target = server.props.getProperty(prefix + TARGET);

	if (replace == null) {
	    server.log(Server.LOG_WARNING, prefix, "missing: " + REPLACE);
	    return false;
	}
	String match = server.props.getProperty(prefix + MATCH);
	if (match == null) {
	    server.log(Server.LOG_WARNING, prefix, "missing: " + MATCH);
	    return false;
	}
	match = Format.subst(server.props, match);
	try {
	    re = new Regexp(match, noCase);
	} catch (Exception e) {
	    server.log(Server.LOG_WARNING, prefix, "Bad expression:" + e);
	}
	return (re != null);
    }

    /**
     * If this request matches the expression, rewrite it.
     */

    public boolean
    respond(Request request) throws IOException {
	String formatted;
	Properties extras = getExtras(request);
	if (source != null) {
	    formatted = Format.subst(extras, source);
	} else {
	    formatted = request.url;
	}
	request.log(Server.LOG_DIAGNOSTIC, prefix, "Matching url " + formatted);
	String sub = re.sub(formatted, replace);
	if (sub == null) {
	    return false;
	}
	sub = Format.subst(extras, sub);
	if (export != null) {
	    if (!export.endsWith(".")) {
		export += ".";
	    }
	    String[] parts = new String[re.subspecs()];
	    re.match(request.url, parts);
	    for (int i=0; i<parts.length; i++) {
		request.props.put(export + i, parts[i]);
	    }
	}

	request.log(Server.LOG_DIAGNOSTIC, prefix, "Mapping " +
		formatted + " => " + sub);
	if (redirect) {
	    request.redirect(sub,null);
	    return true;
	} else if (target != null) {
	    request.log(Server.LOG_DIAGNOSTIC, prefix, "Setting header: " +
		target + " => " + sub);
	    request.headers.put(target, sub);
	} else {
	    request.url = sub;
	}
	return false;
    }

    /**
     * Add extra properties ontpo the request for ${} substitutions
     */

    Properties getExtras(Request request) {
	MapProperties map = new MapProperties(request.props,
		request.headers);
	map.addItem("method", request.method);
	map.addItem("url", request.url);
	map.addItem("protocol", request.protocol);
	map.addItem("query", request.query);
	map.addItem("serverUrl", request.serverUrl());

	// XXX stolen from SetTemplate - should consolidate

	String host = request.headers.get("host");
	if (host == null) {
	    host = server.hostName;
	}
	if (host != null) {
	    int indx = host.indexOf(":");
	    if (indx < 0) {
		map.addItem("hostname", host);
		if (server.protocol.equals("http")) {
		    map.addItem( "hostport","80");
		}
		if (server.protocol.equals("https")) {
		    map.addItem("hostport","443");
		}
	    } else {
		map.addItem("hostname", host.substring(0,indx));
		map.addItem("hostport", host.substring(indx+1));
	    }
	}
	return map;
    }

    /**
     * Look in a dictionary first, then the provided properties.
     * XXX There are lots of little classes like this sprinkled
     * throught the code.  They should be consolidated. This is
     * for Format.subst, and is not a complete implementation.
     */

    public static class MapProperties extends Properties {
	Properties realProps;
	Dictionary dict;
	Properties extra = null;

	public MapProperties(Properties props, Dictionary dict) {
	    realProps = props;
	    this.dict = dict;
	}

	public void addItem(String name, String value) {
	    if (extra == null) {
		extra = new Properties();
	    }
	    extra.put(name, value);
	    // System.out.println("adding: " + name + "=(" + value + ")");
	}

	public String getProperty(String key, String dflt) {
	    String result = null;

	    if (extra != null) {
		result = extra.getProperty(key);
	    }
	    if (result == null) {
		result = (String) dict.get(key);
	    }
	    if (result == null) {
		result = realProps.getProperty(key, dflt);
	    }
	    return result;
	}

	public String getProperty(String key) {
	    return getProperty(key, null);
	}
    }
}