/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright (c) 1999 by Sun Microsystems, Inc.
 * All rights reserved.
 *
 */

//  SLPV1SSrvMsg.java: SLPv1 server side service rqst/reply.
//  Author:           James Kempf
//  Created On:       Thu Sep 10 15:33:58 1998
//  Last Modified By: James Kempf
//  Last Modified On: Fri Nov  6 14:03:00 1998
//  Update Count:     41
//


package com.sun.slp;

import java.util.*;
import java.io.*;


/**
 * The SLPV1SSrvMsg class models the SLP server side service request message.
 *
 * @author James Kempf
 */

class SLPV1SSrvMsg extends SSrvMsg {

    // For eating whitespace.

    final static char SPACE = ' ';

    // Comma for list parsing.

    final static char COMMA = ',';

    // Logical operators.

    final static char OR_OP = '|';
    final static char AND_OP = '&';

    // Logical operator corner case needs this.

    final static char HASH = '#';

    // Comparison/Assignment operators.

    final static char EQUAL_OP = '=';
    final static char NOT_OP = '!';
    final static char LESS_OP = '<';
    final static char GREATER_OP = '>';
    final static char GEQUAL_OP = 'g';
    final static char LEQUAL_OP = 'l';

    // Parens.

    final static char OPEN_PAREN = '(';
    final static char CLOSE_PAREN = ')';

    // LDAP present operator

    final static char PRESENT = '*';

    // Wildcard operator.

    final static String WILDCARD = "*";

    // Character code for parsing.

    String charCode = IANACharCode.UTF8;

    // For creating a null reply.

    protected SLPV1SSrvMsg() {}

    // Construct a SLPV1SSrvMsg from the input stream.

    SLPV1SSrvMsg(SrvLocHeader hdr, DataInputStream dis)
	throws ServiceLocationException, IOException {
	super(hdr, dis);

    }

    // Construct an empty SLPV1SSrvMsg, for monolingual off.

    static SrvLocMsg makeEmptyReply(SLPHeaderV1 hdr)
	throws ServiceLocationException {

	SLPV1SSrvMsg msg = new SLPV1SSrvMsg();
	msg.hdr = hdr;

	msg.makeReply(new Hashtable(), null);

	return msg;

    }

    // Initialize the message from the input stream.

    void initialize(DataInputStream dis)
	throws ServiceLocationException, IOException {

	SLPHeaderV1 hdr = (SLPHeaderV1)getHeader();
	StringBuffer buf = new StringBuffer();

	// First get the previous responder.

	hdr.parsePreviousRespondersIn(dis);

	// Now get the raw query.

	hdr.getString(buf, dis);

	String rq = buf.toString();

	// Parse the raw query to pull out the service type, scope,
	//  and query.

	StringTokenizer st = new StringTokenizer(rq, "/", true);

	try {

	    String type =
		Defaults.SERVICE_PREFIX + ":" +
		st.nextToken().trim().toLowerCase() + ":";

	    serviceType =
		hdr.checkServiceType(type);

	    st.nextToken();  // get rid of slash.

	    // Get the scope.

	    String scope = st.nextToken().trim().toLowerCase();

	    // Special case if scope is empty (meaning the next
	    //  token will be a slash).

	    if (scope.equals("/")) {
		scope = "";

	    } else {

		st.nextToken();  // get rid of slash.

		if (scope.length() > 0) {

		    // Validate the scope name.
	
		    hdr.validateScope(scope);
		}
	    }

	    // Set up scopes vector.

	    hdr.scopes = new Vector();

	    // Substitute default scope here.

	    if (scope.length() <= 0) {
		scope = Defaults.DEFAULT_SCOPE;

	    }

	    hdr.scopes.addElement(scope.toLowerCase().trim());

	    // Parsing the query is complicated by opaques having slashes.
	
	    String q = "";

	    while (st.hasMoreTokens()) {
		q = q + st.nextToken();

	    }

	    // Drop off the final backslash, error if none.

	    if (!q.endsWith("/")) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {rq});
	    }

	    query = q.substring(0, q.length()-1);

	    // Save header char code for parsing.

	    charCode = hdr.charCode;

	    // Convert the query into a V2 query.

	    convertQuery();

	    // If the query is for "service:directory-agent", then we
	    //  mark it as having been multicast, because that is the
	    //  only kind of multicast that we accept for SLPv1. Anybody
	    //  who unicasts this to us will time out.

	    if (serviceType.equals(Defaults.DA_SERVICE_TYPE.toString())) {
		hdr.mcast = true;

	    }

	    // Construct description.

	    hdr.constructDescription("SrvRqst",
				     "        service type=``" +
				     serviceType + "''\n" +
				     "        query=``" +
				     query + "''");

	}  catch (NoSuchElementException ex) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {rq});
	}
    }

    // Make a reply message.

    SrvLocMsg makeReply(Hashtable urltable,
			Hashtable URLSignatures)
	throws ServiceLocationException {

	SLPHeaderV1 hdr =
	    ((SLPHeaderV1)getHeader()).makeReplyHeader();

	ByteArrayOutputStream baos = new ByteArrayOutputStream();

	// Edit out abstract types and nonService: URLs.

	Enumeration en = urltable.keys();
	Vector urls = new Vector();

	while (en.hasMoreElements()) {
	    ServiceURL surl = (ServiceURL)en.nextElement();

	    // Reject if abstract type or nonservice: URL.

	    ServiceType type = surl.getServiceType();

	    if (!type.isAbstractType() && type.isServiceURL()) {
		urls.addElement(surl);

	    }
	}

	hdr.iNumReplies = urls.size();
	// keep this info so SAs can drop 0 replies

	int n = urls.size();

	// Write out the size of the list.

	hdr.putInt(n, baos);

	en = urls.elements();

	// Write out the size of the list.

	while (en.hasMoreElements()) {
	    ServiceURL surl = (ServiceURL)en.nextElement();

	    hdr.parseServiceURLOut(surl, true, baos);

	}

	// We ignore the signatures because we only do V1 compatibility
	//  for nonprotected scopes.

	hdr.payload = baos.toByteArray();

	hdr.constructDescription("SrvRply",
				 "        service URLs=``" + urls + "''\n");

	return hdr;

    }

    // Convert the query to a V2 query.

    void convertQuery()
	throws ServiceLocationException {

	// Check for empty query.

	query = query.trim();

	if (query.length() <= 0) {
	    return;

	}

	// Check for query join.

	if (!(query.startsWith("(") && query.endsWith(")"))) {

	    // Rewrite to a standard query.

	    query = rewriteQueryJoin(query);

	}

	// Now rewrite the query into v2 format.

	query = rewriteQuery(query);
    }


    // Rewrite a query join as a conjunction.

    private String rewriteQueryJoin(String query)
	throws ServiceLocationException {

	// Turn infix expression into prefix.

	StringBuffer sbuf = new StringBuffer();
	StringTokenizer tk = new StringTokenizer(query, ",", true);
	boolean lastTokComma = true;
	int numEx = 0;

	while (tk.hasMoreElements()) {
	    String exp = tk.nextToken().trim();

	    if (exp.equals(",")) {
		if (lastTokComma) {
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

		} else {	
		    lastTokComma = true;
		}
	
	    } else {
		lastTokComma = false;

		if (exp.length() <= 0) {
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

		}

		// Put in parens

		sbuf.append("(");
		sbuf.append(exp);
		sbuf.append(")");

		numEx++;
	    }
	}

	if (lastTokComma || numEx == 0) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	}

	if (numEx > 1) {
	    sbuf.insert(0, "(&");
	    sbuf.append(")");

	}

	return sbuf.toString();
    }

    // Rewrite a v1 query into v2 format. This includes character escaping.

    private String rewriteQuery(String whereList)
	throws ServiceLocationException {

	// Parse a logical expression.

	StreamTokenizer tk =
	    new StreamTokenizer(new StringReader(whereList));

	tk.resetSyntax();  		// make all chars ordinary...
	tk.whitespaceChars('\000','\037');
	tk.ordinaryChar(SPACE);		// but beware of embedded whites...
	tk.wordChars('!', '%');
	tk.ordinaryChar(AND_OP);
	tk.wordChars('\'', '\'');
	tk.ordinaryChar(OPEN_PAREN);
	tk.ordinaryChar(CLOSE_PAREN);
	tk.wordChars('*', '{');
	tk.ordinaryChar(OR_OP);
	tk.wordChars('}', '~');

	// Initialize parse tables in terminal.

	tk.ordinaryChar(EQUAL_OP);
	tk.ordinaryChar(NOT_OP);
	tk.ordinaryChar(LESS_OP);
	tk.ordinaryChar(GREATER_OP);

	StringBuffer buf = new StringBuffer();


	// Parse through the expression.

	try {
	    parseInternal(tk, buf, true);

	} catch (IOException ex) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	}

	return buf.toString();
    }

    // Do the actual parsing, using the passed-in stream tokenizer.

    private void
	parseInternal(StreamTokenizer tk, StringBuffer buf, boolean start)
	throws ServiceLocationException, IOException {

	int tok = 0;
	boolean ret = true;

	do {
	    tok = eatWhite(tk);

	    // We should be at the beginning a parenthesized
	    //  where list.
	
	    if (tok == OPEN_PAREN) {

		// Get the next token. Eat whitespace in the process.
	
		tok = eatWhite(tk);

		// If it's a logOp, then process as a logical expression.
		//  This handles the following nasty case:
		//
		//  	(&#44;&#45==the rest of it)

		int logOp = tok;

		if (logOp == AND_OP) {

		    // Need to check for escape as first thing.

		    tok = tk.nextToken();
		    String str = tk.sval; // not used if token not a string...
		    tk.pushBack();

		    if (tok == StreamTokenizer.TT_WORD) {

			if (str.charAt(0) != HASH) {
			    parseLogicalExpression(logOp, tk, buf);

			} else {
			    parse(tk, buf, true);
					// cause we can't push back twice

			}

		    } else {
			parseLogicalExpression(logOp, tk, buf);

		    }

		    break;

		} else if (logOp == OR_OP) {

		    parseLogicalExpression(logOp, tk, buf);

		    break;

		} else {

		    // It's a terminal expression. Push back the last token
		    //  and parse the terminal.

		    tk.pushBack();

		    parse(tk, buf, false);

		    break;

		}

	    } else {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
	    }

	} while (true);

	// Since terminals are allowed alone at the top level,
	//  we need to check here whether anything else is
	//  in the query.

	if (start) {

	    tok = eatWhite(tk);

	    if (tok != StreamTokenizer.TT_EOF) {
	
		// The line should have ended by now.

		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
	    }
	}

    }

    // Rewrite a logical expression.

    private void
	parseLogicalExpression(int logOp, StreamTokenizer tk, StringBuffer buf)
	throws ServiceLocationException, IOException {

	// Append paren and operator to buffer.

	buf.append((char)OPEN_PAREN);
	buf.append((char)logOp);

	int tok = 0;

	do {

	    tok = eatWhite(tk);

	    if (tok == OPEN_PAREN) {

		// So parseInternal() sees a parenthesized list.

		tk.pushBack();

		// Go back to parseInternal.

		parseInternal(tk, buf, false);
	
	    } else if (tok == CLOSE_PAREN) {

		// Append the character to the buffer and return.

		buf.append((char)tok);

		return;

	    } else {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
	    }

	} while (tok != StreamTokenizer.TT_EOF);

	// Error if we've not caught ourselves before this.

	throw
	    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
    }

    // Parse a terminal. Opening paren has been got.

    private void parse(StreamTokenizer tk,
		       StringBuffer buf,
		       boolean firstEscaped)
	throws ServiceLocationException, IOException {

	String tag = "";
	int tok = 0;

	tok = eatWhite(tk);

	// Gather the tag and value.

	if (tok != StreamTokenizer.TT_WORD) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
	}

	// Parse the tag.

	tag = parseTag(tk, firstEscaped);

	if (tag.length() <= 0) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
	}

	// Unescape tag.

	tag = ServiceLocationAttributeV1.unescapeAttributeString(tag,
								 charCode);

	// Now escape in v2 format,

	tag = ServiceLocationAttribute.escapeAttributeString(tag, true);

	// Parse the operator.

	char compOp = parseOperator(tk);

	// If this was a keyword operator, then add present
	// operator and closing paren and return.

	if (compOp == PRESENT) {
	    buf.append(OPEN_PAREN);
	    buf.append(tag);
	    buf.append(EQUAL_OP);
	    buf.append(PRESENT);
	    buf.append(CLOSE_PAREN);
	    return;

	}

	// Parse value by reading up to the next close paren.
	//  Returned value will be in v2 format.

	String valTok = parseValue(tk);

	// Construct the comparision depending on the operator.

	if (compOp == NOT_OP) {

	    // If the value is an integer, we can construct a query
	    //  that will exclude the number.

	    try {

		int n = Integer.parseInt(valTok);

		// Bump the integer up and down to catch numbers on both
		//  sides of the required number. Be careful not to
		//  overstep bounds.

		if (n < Integer.MAX_VALUE) {
		    buf.append(OPEN_PAREN);
		    buf.append(tag);
		    buf.append(GREATER_OP);
		    buf.append(EQUAL_OP);
		    buf.append(n + 1);
		    buf.append(CLOSE_PAREN);

		}

		if (n > Integer.MIN_VALUE) {
		    buf.append(OPEN_PAREN);
		    buf.append(tag);
		    buf.append(LESS_OP);
		    buf.append(EQUAL_OP);
		    buf.append(n - 1);
		    buf.append(CLOSE_PAREN);

		}

		if ((n < Integer.MAX_VALUE) && (n > Integer.MIN_VALUE)) {
		    buf.insert(0, OR_OP);
		    buf.insert(0, OPEN_PAREN);
		    buf.append(CLOSE_PAREN);

		}

	    } catch (NumberFormatException ex) {

		// It's not an integer. We can construct a query expression
		// that will not always work. The query rules out advertisments
		// where the attribute value doesn't match and there are
		// no other attributes or values, and advertisements
		// that don't contain the attribute, but it doesn't rule out
		// a multivalued attribute with other values or if there
		// are other attributes. The format of the query is:
		// "(&(<tag>=*)(!(<tag>=<value>))).

		buf.append(OPEN_PAREN);
		buf.append(AND_OP);
		buf.append(OPEN_PAREN);
		buf.append(tag);
		buf.append(EQUAL_OP);
		buf.append(PRESENT);
		buf.append(CLOSE_PAREN);
		buf.append(OPEN_PAREN);
		buf.append(NOT_OP);
		buf.append(OPEN_PAREN);
		buf.append(tag);
		buf.append(EQUAL_OP);
		buf.append(valTok);
		buf.append(CLOSE_PAREN);
		buf.append(CLOSE_PAREN);
		buf.append(CLOSE_PAREN);

	    }

	} else if ((compOp == LESS_OP) || (compOp == GREATER_OP)) {

	    int n = 0;

	    try {

		n = Integer.parseInt(valTok);

	    } catch (NumberFormatException ex) {

		// It's a parse error here.

		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	    }

	    // We don't attempt to handle something that would cause
	    // arithmetic overflow.

	    if ((n == Integer.MAX_VALUE) || (n == Integer.MIN_VALUE)) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	    }

	    // Construct a query that includes everything
	    //  to the correct side.

	    buf.append(OPEN_PAREN);
	    buf.append(tag);

	    if (compOp == LESS_OP) {
		buf.append(LESS_OP);
		buf.append(EQUAL_OP);
		buf.append(n - 1);

	    } else {
		buf.append(GREATER_OP);
		buf.append(EQUAL_OP);
		buf.append(n + 1);

	    }

	    buf.append(CLOSE_PAREN);

	} else {

	    // Simple, single operator. Just add it with the
	    //  value.

	    buf.append(OPEN_PAREN);
	    buf.append(tag);

	    // Need to distinguish less and greater equal.

	    if (compOp == LEQUAL_OP) {
		buf.append(LESS_OP);
		buf.append(EQUAL_OP);

	    } else if (compOp == GEQUAL_OP) {
		buf.append(GREATER_OP);
		buf.append(EQUAL_OP);

	    } else {
		buf.append(compOp);

	    }

	    buf.append(valTok);
	    buf.append(CLOSE_PAREN);

	}

    }

    // Gather tokens with embedded whitespace and return.

    private String parseTag(StreamTokenizer tk, boolean ampStart)
	throws ServiceLocationException, IOException {

	String value = "";

	// Take care of corner case here.

	if (ampStart) {
	    value = value +"&";
	    ampStart = false;
	}

	do {

	    if (tk.ttype == StreamTokenizer.TT_WORD) {
		value += tk.sval;

	    } else if ((char)tk.ttype == SPACE) {
		value = value + " ";

	    } else if ((char)tk.ttype == AND_OP) {
		value = value + "&";
	
	    } else {
		break;

	    }
	    tk.nextToken();

	} while (true);

	return value.trim();  // removes trailing whitespace...
    }

    private char parseOperator(StreamTokenizer tk)
	throws ServiceLocationException, IOException {

	int tok = tk.ttype;

	// If the token is a close paren, then this was a keyword
	// (e.g. "(foo)". Return the present operator.

	if ((char)tok == CLOSE_PAREN) {
	    return PRESENT;

	}

	if (tok != EQUAL_OP && tok != NOT_OP &&
	    tok != LESS_OP && tok != GREATER_OP) {

	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	}

	char compOp = (char)tok;

	// Get the next token.

	tok = tk.nextToken();

	// Look for dual character operators.

	if ((char)tok == EQUAL_OP) {

	    // Here, we can have either "!=", "<=", ">=", or "==".
	    //  Anything else is wrong.

	    if (compOp != LESS_OP && compOp != GREATER_OP &&
		compOp != EQUAL_OP && compOp != NOT_OP) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
	    }

	    // Assign the right dual operator.

	    if (compOp == LESS_OP) {
		compOp = LEQUAL_OP;

	    } else if (compOp == GREATER_OP) {
		compOp = GEQUAL_OP;

	    }

	} else if (compOp != LESS_OP && compOp != GREATER_OP) {

	    // Error if the comparison operator was something other
	    //  than ``<'' or ``>'' and there is no equal. This
	    //  rules out ``!'' or ``='' alone.

	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	} else {

	    // Push back the last token if it wasn't a two character operator.

	    tk.pushBack();

	}

	return compOp;
    }


    private String parseValue(StreamTokenizer tk)
	throws ServiceLocationException, IOException {

	int tok = 0;
	StringBuffer valTok = new StringBuffer();

	// Eat leading whitespace.

	tok = eatWhite(tk);

	// If the first value is a paren, then we've got an
	//  opaque.

	if ((char)tok == OPEN_PAREN) {

	    valTok.append("(");

	    // Collect all tokens up to the closing paren.

	    do {

		tok = tk.nextToken();

		// It's a closing paren. break out of the loop.

		if ((char)tok == CLOSE_PAREN) {
		    valTok.append(")");
		    break;

		} else if ((char)tok == EQUAL_OP) {
		    valTok.append("=");

		} else if (tok == StreamTokenizer.TT_WORD) {
		    valTok.append(tk.sval);

		} else {
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});
		}

	    } while (true);


	    // Eat whitespace until closing paren.

	    tok = eatWhite(tk);

	    if ((char)tok != CLOSE_PAREN) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	    }

	} else {

	    // Error if just a closed paren.

	    if (tok == CLOSE_PAREN) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_query_error",
				new Object[] {query});

	    }

	    do {

		// Append the token if a WORD

		if (tok == StreamTokenizer.TT_WORD) {
		    valTok.append(tk.sval);

		} else if ((tok != StreamTokenizer.TT_EOF) &&
			   (tok != StreamTokenizer.TT_EOL) &&
			   (tok != CLOSE_PAREN)) {

		    // Otherwise, it's a token char, so append.

		    valTok.append((char)tok);

		}

		tok = tk.nextToken();

	    } while (tok != CLOSE_PAREN);
	}

	// If a wildcard, remove wildcard stars here for later re-insertion.

	String strval = valTok.toString().trim();
	boolean wildstart = false;
	boolean wildend = false;

	if (strval.startsWith(WILDCARD)) {
	    wildstart = true;
	    strval = strval.substring(1, strval.length());

	}

	if (strval.endsWith(WILDCARD)) {
	    wildend = true;
	    strval = strval.substring(0, strval.length()-1);

	}

	// Evaluate the value.

	Object val =
	    ServiceLocationAttributeV1.evaluate(strval, charCode);

	// Now convert to v2 format, and return.

	if (val instanceof String) {
	    strval =
		ServiceLocationAttribute.escapeAttributeString(val.toString(),
							       false);

	    // Add wildcards back in.

	    if (wildstart) {
		strval = WILDCARD + strval;

	    }

	    if (wildend) {
		strval = strval + WILDCARD;

	    }

	} else {
	    strval = val.toString();

	}

	return strval;

    }

    // Eat whitespace.

    private int eatWhite(StreamTokenizer tk)
	throws IOException {

	int tok = tk.nextToken();

	while (tok == SPACE) {
	    tok = tk.nextToken();

	}

	return tok;
    }
}