root/usr/src/lib/libslp/javalib/com/sun/slp/SLPV1SSrvMsg.java
/*
 * 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;
    }
}