/*
 * ConfigFileHandler.java
 *
 * Brazil project web application toolkit,
 * export version: 2.3 
 * Copyright (c) 1999-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, rinaldo, suhler.
 *
 * Version:  2.3
 * Created by suhler on 99/06/28
 * Last modified by suhler on 08/12/19 11:13:30
 *
 * Version Histories:
 *
 * 2.3 08/12/19-11:13:30 (suhler)
 *   doc fixes
 *
 * 2.2 04/08/30-09:02:07 (suhler)
 *   "enum" became a reserved word, change to "enumer".
 *
 * 2.1 02/10/01-16:36:34 (suhler)
 *   version change
 *
 * 1.35 02/07/24-10:45:56 (suhler)
 *   doc updates
 *
 * 1.34 01/12/10-16:11:12 (suhler)
 *   doc lint
 *
 * 1.33 01/05/18-09:11:40 (suhler)
 *   work with either CookieSessionHAndler -or- SessionFilter
 *
 * 1.32 01/01/22-19:02:30 (cstevens)
 *   Remove RechainableProperties from Request.  In order to insert shared
 *   Properties objects, use request.addSharedProps() instead of manually
 *   rechaining the properties using props.setDefaults().
 *
 * 1.31 00/12/11-13:28:21 (suhler)
 *   add class=props for automatic property extraction
 *
 * 1.30 00/11/20-13:28:06 (suhler)
 *   lint
 *   D
 *
 * 1.29 00/11/20-13:21:35 (suhler)
 *   doc fixes
 *
 * 1.28 00/10/05-11:06:46 (suhler)
 *   The property "glob" can be used to match a patterns of request.props
 *   that may be set by the user that are NOT in the default config file.  If
 *   "glob" is specified, no default file is required.
 *
 * 1.27 00/05/31-13:47:26 (suhler)
 *   doc cleanup
 *
 * 1.26 00/05/24-11:35:36 (suhler)
 *   added a "copy" property to implement old behavior for backward compatibility
 *
 * 1.25 00/05/22-14:03:48 (suhler)
 *   doc updates
 *
 * 1.24 00/05/15-12:04:29 (suhler)
 *   added chaining back in, removing (impossible) behind option
 *   added more diags
 *
 * 1.23 00/05/12-15:25:10 (suhler)
 *   temporarily back out properties chaining
 *
 * 1.22 00/05/10-10:54:06 (suhler)
 *   No longer add config properties directly to request.props.
 *   Instead, use the ChainedProperties.
 *   added "noOverride" option to prevent properties from ovverriding
 *   values that already exist
 *
 * 1.21 00/04/20-11:50:15 (cstevens)
 *   copyright.
 *
 * 1.20 00/04/12-15:53:16 (cstevens)
 *   imports
 *
 * 1.19 00/03/29-14:35:55 (cstevens)
 *   pedantic use of Request.getQueryData()
 *
 * 1.18 00/02/11-12:42:44 (suhler)
 *   arguments to Match wrong
 *
 * 1.17 00/02/03-14:24:40 (suhler)
 *   added "no content" option
 *
 * 1.16 99/12/15-12:02:39 (suhler)
 *   Properties we not being passed on if a "set" was atempted, and no
 *   values were changed
 *
 * 1.15 99/12/07-10:55:17 (suhler)
 *   set request properties *after* processing SET query parameters
 *
 * 1.14 99/11/30-09:47:38 (suhler)
 *   Redo.  Only generate session based files when really needed
 *
 * 1.12.1.1 99/11/14-10:34:55 (rinaldo)
 *   Fix Error with Mime Type
 *
 * 1.13 99/11/01-11:56:37 (suhler)
 *   ConfigFile handler gets root property at each request
 *
 * 1.12 99/10/26-17:11:10 (cstevens)
 *   Get rid of public variables Request.server and Request.sock:
 *   A. In all cases, Request.server was not necessary; it was mainly used for
 *   constructing the absolute URL for a redirect, so Request.redirect() was
 *   rewritten to take an absolute or relative URL and do the right thing.
 *   B. Request.sock was changed to Request.getSock(); it is still rarely used
 *   for diagnostics and logging (e.g., ChainSawHandler).
 *
 * 1.10.1.1 99/10/23-19:47:18 (rinaldo)
 *
 * 1.11 99/10/11-12:30:44 (suhler)
 *   change POST to GET when done processing.
 *
 * 1.10 99/10/06-12:28:41 (suhler)
 *   fix log
 *
 * 1.9 99/10/06-12:20:32 (suhler)
 *   Merged changes between child workspace "/home/suhler/brazil/naws" and
 *   parent workspace "/net/mack.eng/export/ws/brazil/naws".
 *
 * 1.8 99/10/01-11:26:36 (cstevens)
 *   Change logging to show prefix of Handler generating the log message.
 *
 * 1.7.1.2 99/09/27-17:47:30 (suhler)
 *   Entirely new semantics.  This will break all existing uses:
 *   - the "config" file is always merged into the request properties
 *   - a separate url is used to set the properties
 *   - a default properties file is required, and the existing properties limit
 *   what is allowed to be set.
 *
 * 1.7.1.1 99/09/10-11:02:53 (suhler)
 *   ???
 *
 * 1.7 99/08/06-12:11:06 (suhler)
 *   Merged changes between child workspace "/home/suhler/brazil/naws" and
 *   parent workspace "/net/mack.eng/export/ws/brazil/naws".
 *
 * 1.5.1.2 99/08/06-12:05:18 (suhler)
 *   use glob in util directory
 *   ,
 *   .
 *   D
 *
 * 1.5.1.1 99/08/04-18:44:31 (suhler)
 *   Changes to allow session specific file names
 *
 * 1.6 99/07/27-22:02:32 (rinaldo)
 *   Stephen, you will probably not like what I did but it is late and I have to
 *   have the properties I just set be available to all sunsequent url requests tak
 *   a look at XXX.
 *
 * 1.5 99/07/14-11:37:20 (rinaldo)
 *   Allow absolute URLS.
 *
 * 1.4 99/07/13-09:37:41 (suhler)
 *   look up the redirect property in the form/query data
 *
 * 1.3 99/06/29-14:38:20 (suhler)
 *   use new redirector
 *
 * 1.2 99/06/29-09:37:12 (suhler)
 *   re-wrote config handler to be more useful.
 *   It now works in conjuction with the form template handler
 *   see config.config for an example
 *   .
 *
 * 1.2 99/06/28-10:46:26 (Codemgr)
 *   SunPro Code Manager data about conflicts, renames, etc...
 *   Name history : 1 0 handlers/ConfigFileHandler.java
 *
 * 1.1 99/06/28-10:46:25 (suhler)
 *   date and time created 99/06/28 10:46:25 by suhler
 *
 */

package sunlabs.brazil.handler;

import sunlabs.brazil.session.SessionManager;
import sunlabs.brazil.server.Handler;
import sunlabs.brazil.server.Request;
import sunlabs.brazil.server.Server;
import sunlabs.brazil.util.Glob;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Dictionary;
import java.util.Hashtable;
import java.util.Properties;

/**
 * Handler for manipulating per-user session state that can be
 * represented as ASCII name/value pairs.  The state for each session
 * is stored in a file, which is expected to be in java properties format.
 *
 * If "prefix" is matched, the contents of the (usually cached) config file
 * for the current session is added to the request properties.
 *
 * If the url matches the "set" property, the contents of the config
 * file are changed based on the supplied query parameters (either GET
 * of POST).  If no config file exists for the session, one is created
 * from a default properties file.  Only properties already in the
 * config file may be changed using the "set" method.
 *
 * If a "%" is specified in the file name, it is replaced by the
 * SessionID property, if any, or "common" if sessions aren't used.
 * This should be replaced with something more general, so we can have
 * more arbitrary mappings between request and the session info.
 * <p>
 * The following request properties are used:
 * <dl class=props>
 * <dt>prefix	<dd> The URL prefix required for all documents
 * <dt>set	<dd> The url pattern to match setting properties.
 *		     Currently, it must also match "prefix".
 * <dt>noContent<dd> a url, matching the "set" pattern that causes
 *		     a "204 no content" to be returned  to the client
 *			(not implemented).
 * <dt>name	<dd> The name of the config file. the first "%" is replaced
 *		     by the current SessionID.
 * <dt>default	<dd> The default properties file to "seed" session properties
 * <dt>glob	<dd> Properties that match this "glob" pattern may be set
 *		     using the "set" pattern.  If this property is specified,
 *		     the "default" property is optional.
 * <dt>root	<dd> The document root (no properties prefix required). If the
 *		     "name" or "default" properties don't start with a "/", 
 *		     this is used as the current directory.
 *		   
 * </dl>
 * If "%" is specified in the file name, a new session file is 
 * created only if 1) a property is changed from the default, and 2)
 * A cookie was received by the browser.
 * <p>
 * See also: {@link sunlabs.brazil.template.SetTemplate}
 * which is preferrable in most cases, providing a templated based
 * (instead of URL based) 
 * mechanism for maintaining persistent properties.
 *
 * @author		Stephen Uhler
 * @version		2.3, 08/12/19
 */

public class ConfigFileHandler implements Handler {
    static final String SET = "set";   // glob matching url setting props
    static final String PREFIX = "prefix";   // prefix for adding to props
    static final String NAME = "name";     // name of the config file
    static final String DEFAULT = "default";    // the default config file
    static final String ROOT = "root";    // root of config file
    static final String GLOB = "glob";  

    String propsPrefix;
    String name;	// name of the config file
    String urlPrefix;	// prefix for any request we handle
    String set;	// glob pattern to match url needed to modify props
    String nc;	// glob pattern to match url needed for no content
    String match; // glob pattern to match settable props not in deflt file
    String root;	// where to find the root of the config file
    Properties defaultProperties;	// where to find the defaults
    boolean copy = false;	// undocumented backwart compat flag

    /**
     * Make sure default properties exist before starting this handler, 
     * or that "match" is specified".
     */

    public boolean
    init(Server server, String prefix) {
	propsPrefix = prefix;
	urlPrefix = server.props.getProperty(prefix + PREFIX, "/");
	set = server.props.getProperty(prefix + SET, urlPrefix + prefix);
	nc = server.props.getProperty(prefix + "noContent", "X");
	match = server.props.getProperty(prefix + GLOB);
	name = server.props.getProperty(prefix + NAME, prefix + ".cfg");
	copy = (server.props.getProperty(prefix + "copy") != null);
	root = server.props.getProperty(ROOT, ".");
	String defaultName = server.props.getProperty(prefix + DEFAULT);
	File defaultFile = null;
	if (defaultName != null) {
	    if (defaultName.startsWith("/")) {
		defaultFile = new File(defaultName);
	    } else {
		defaultFile = new File(root, defaultName);
	    }
	}
	defaultProperties = new Properties();
	try {
	    FileInputStream in = new FileInputStream(defaultFile);
	    defaultProperties.load(in);
	    in.close();
	} catch (IOException e) {
	    if (match == null) {
		server.log(Server.LOG_WARNING, prefix, "No default file: " +
		defaultFile + " ConfigFileHandler NOT installed!");	
		return false;
	    }
	}
	server.log(Server.LOG_DIAGNOSTIC, prefix, "\n  set=" + set +
		"\n  name=" + name + "\n  default=" + defaultFile);
	return true;
    }

    /**
     * Extract the session state into the request object, optionally
     * modifying the properties.  If the properties are modified,
     * they are stored in a file for safe keeping.
     */

    public boolean
    respond(Request request) throws IOException {
	if (!request.url.startsWith(urlPrefix)) {
	    return false;
	}

	/*
	 * Get the existing properties.
	 */

	root = request.props.getProperty(ROOT, root);
	String id = request.props.getProperty("SessionID", "common");
	Properties p = (Properties) SessionManager.getSession(id,
		propsPrefix, Properties.class);

	/*
	 * If no properties exist, read in the session
	 * properties, otherwise use the default properties
	 */

	if (p.isEmpty()) {
	    File file = getPropsFile(name, id);
	    request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
		    "No properties, looking in: " + file);
	    try {
		FileInputStream in = new FileInputStream(file);
		p.load(in);
		in.close();
	    } catch (IOException e) {
		request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
		    "Can't find config file: " +
		    file + " using default properties");	
		Enumeration enumer = defaultProperties.propertyNames();
		while(enumer.hasMoreElements()) {
		    String key = (String) enumer.nextElement();
		    p.put(key, defaultProperties.getProperty(key));
		}
	    }
	}

	/*
	 * If "SET" then update the properties file
	 */

	if (Glob.match(set, request.url)) {
	    Dictionary update = request.getQueryData(null);
	    // System.out.println("Setting from: " + update);
	    Enumeration enumer = update.keys();
	    boolean changed = false;
	    synchronized (p) {
		while(enumer.hasMoreElements()) {
		    String key = (String) enumer.nextElement();
		    if (p.containsKey(key) || isMatch(key)) {
			p.put(key, update.get(key));
			changed = true; // XXX not quite
		    } else {
			request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
			    "Can't set key " + key +
			    ", name doesn't exist in default file" +
			    " or doesn;t match: " + match);
		    }
		}
		if (!changed) {
		    request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
			    "Set called, nothing changed!!");
		}

		/*
		 * If the client has cookies enabled, try to save the
		 * properties.  This depends upon a side effect of the
		 * "sessionHandler" to let us know if the browser
		 * actually sent us our cookie.
		 */

		if (changed && (name.indexOf("%") < 0 ||
			request.props.containsKey("UrlID") ||
			request.props.containsKey("gotCookie"))) {
		    File file = getPropsFile(name, id);
		    try {
			FileOutputStream out = new FileOutputStream(file);
			p.save(out, request.serverUrl() + request.url +
			    " (from ConfigFileHandler: " + propsPrefix + ")");
			out.close();
			request.log(Server.LOG_DIAGNOSTIC, propsPrefix,
			    "Saving configuration properties to " + file);
		    } catch (IOException e) {
			request.log(Server.LOG_WARNING, propsPrefix,
				"Can't save properties to: " + file);
		    }
		}
	    }

	    /*
	     * If this is a post, trash the input and change to a get.
	     * XXX This is probably incorrect.
	     */

	    if (request.method.equals("POST")) {
		request.method = "GET";
		request.postData = null;
		request.headers.put("Content-Length", "0");
	    }	
	}

	if (copy) {
	    Enumeration enumer = p.keys();
	    while(enumer.hasMoreElements()) {
		String key = (String) enumer.nextElement();
		request.props.put(key, p.getProperty(key));
	    }
	} else {
	    request.addSharedProps(p);
//	    request.log(Server.LOG_DIAGNOSTIC, propsPrefix, "Chaining " +
//		    p + " onto request.props");
	}
	return false;
    }

    /**
     * True if key "s" matches glob pattern in "match".  If
     * match is null, then no match.
     */

    private boolean
    isMatch(String s) {
	return (match != null && Glob.match(match, s));
    }

    /**
     * Find the config file name
     */

    private File
    getPropsFile(String name, String id) {
	int index;
	if ((index = name.indexOf("%")) >= 0) {
	    name = name.substring(0,index) + id + name.substring(index+1);
	}
	File file;
	if (name.startsWith("/")) {
	    file = new File(name);
	} else {
	    file = new File(root, name);
	}
	return file;
    }
}