/*
 * ident	"@(#)ServiceLocationAttribute.java	1.2	99/05/10 SMI"
 *
 * Copyright (c) 1999 by Sun Microsystems, Inc.
 * All rights reserved.
 *
 */

//  SCCS Status:      @(#)ServiceLocationAttribute.java	1.2	05/10/99
//  ServiceLocationAttribute.java : Class for attributes in SLP.
//  Author:           James Kempf, Erik Guttman
//

package com.sun.slp;

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

/**
 * The ServiceLocationAttribute class models SLP attributes.
 *
 * @version 1.2 00/10/14
 * @author James Kempf, Erik Guttman
 */

public class ServiceLocationAttribute extends Object
    implements Serializable {

    // Characters to escape.

    final static String RESERVED = "(),\\!<=>~";
    final static char ESCAPE = '\\';

    final static char CTL_LOWER = (char)0x00;
    final static char CTL_UPPER = (char)0x1F;
    final static char DEL = (char)0x7F;

    // Whitespace chars.

    static final String WHITESPACE = " \n\t\r";
    static final char SPACE = ' ';

    // For character escaping.

    static final char COMMA = ',';
    static final char PERCENT = '%';

    // Bad tag characters.

    final private static String BAD_TAG_CHARS = "*\n\t\r";

    // For identifying booleans.

    final static String TRUE = "true";
    final static String FALSE = "false";

    //
    // Package accessable fields.
    //

    Vector values = null;
    String id = null;

    // For V1 compatibility subclass.

    ServiceLocationAttribute() {}

    /**
     * Construct a service location attribute.
     *
     * @param id		The attribute name
     * @param values_in	Vector of one or more attribute values. Vector
     *			contents must be uniform in type and one of
     *			Integer, String, Boolean, or byte[]. If the attribute
     *			is a keyword attribute, then values_in should be null.
     * @exception IllegalArgumentException Thrown if the
     *			vector contents is not of the right type or
     *			an argument is null or syntactically incorrect.
     */

    public ServiceLocationAttribute(String id_in, Vector values_in)
	throws IllegalArgumentException {

	Assert.nonNullParameter(id_in, "id");

	id = id_in;
	if (values_in != null &&
	    values_in.size() > 0) { // null, empty indicate keyword attribute.

	    values = (Vector)values_in.clone();

	    verifyValueTypes(values, false);

	}
    }

    /**
     * Construct a service location attribute from a parenthesized expression.
     * The syntax is:
     *
     *	 exp = "(" id "=" value-list ")" | keyword
     *    value-list = value | value "," value-list
     *
     *
     * @param exp The expression
     * @param dontTypeCheck True if multivalued booleans and vectors
     *			   of varying types are allowed.
     * @exception ServiceLocationException If there are any syntax errors.
     */

    ServiceLocationAttribute(String exp, boolean allowMultiValuedBooleans)
	throws ServiceLocationException {

	if (exp == null || exp.length() <= 0) {
	    new ServiceLocationException(ServiceLocationException.PARSE_ERROR,
					 "null_string_parameter",
					 new Object[] {exp});

	}

	// If start and end paren, then parse out assignment.

	if (exp.startsWith("(") && exp.endsWith(")")) {

	    StringTokenizer tk =
		new StringTokenizer(exp.substring(1, exp.length() - 1),
				    "=",
				    true);

	    try {

		// Get the tag.

		id =
		    unescapeAttributeString(tk.nextToken(), true);

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

		tk.nextToken();  // get rid of "="

		// Gather the rest.

		String rest = tk.nextToken("");

		// Parse the comma separated list.

		values = SrvLocHeader.parseCommaSeparatedListIn(rest, true);

		// Convert to objects.

		int i, n = values.size();
		Class vecClass = null;

		for (i = 0; i < n; i++) {
		    String value = (String)values.elementAt(i);

		    // Need to determine which type to use.

		    Object o = evaluate(value);

		    values.setElementAt(o, i);

		}

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

	    verifyValueTypes(values, allowMultiValuedBooleans);

	} else {

	    // Check to make sure there's no parens.

	    if (exp.indexOf('(') != -1 || exp.indexOf(')') != -1) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"assignment_syntax_err",
				new Object[] {exp});
	    }

	    // Unescape the keyword.

	    id = unescapeAttributeString(exp, true);

	}
    }

    static Object evaluate(String value)
	throws ServiceLocationException {

	Object o = null;

	// If it can be converted into an integer, then convert it.

	try {

	    o = Integer.valueOf(value);

	} catch (NumberFormatException ex) {

	    // Wasn't an integer. Try boolean.

	    if (value.equalsIgnoreCase(TRUE) ||
		value.equalsIgnoreCase(FALSE)) {
		o = Boolean.valueOf(value);

	    } else {
	
		// Process the string to remove escapes.

		String val = (String)value;

		// If it begins with the opaque prefix, treat it as an
		//  opaque.

		if (val.startsWith(Opaque.OPAQUE_HEADER)) {
		    o = Opaque.unescapeByteArray(val);

		} else {
		    o = unescapeAttributeString(val, false);

		}
	    }
	}

	return o;

    }

    //
    // Property accessors.
    //

    /**
     * @return A vector of attribute values, or null if the attribute is
     *         a keyword attribute. If the attribute is single-valued, then
     *         the vector contains only one object.
     *
     */

    public Vector getValues() {

	if (values == null) {
	    return null;	// keyword case.
	}

	Vector ret = (Vector)values.clone();

	// Need to process Opaques.

	int i, n = ret.size();

	for (i = 0; i < n; i++) {
	    Object o = ret.elementAt(i);

	    if (o instanceof Opaque) {
		o = ((Opaque)o).bytes;

	    }

	    ret.setElementAt(o, i);
	}

	return ret;
    }

    /**
     * @return The attribute name.
     */

    public String getId() {

	return id;

    }

    /**
     * Return an escaped version of the id parameter , suitable for inclusion
     * in a query.
     *
     * @param str The string to escape as an id.
     * @return The string with any reserved characters escaped.
     * @exception IllegalArgumentException Thrown if the
     *			string contains bad tag characters.
     */

    static public String escapeId(String str)
	throws IllegalArgumentException {
	String ret = null;

	try {
	    ret = escapeAttributeString(str, true);

	} catch (ServiceLocationException ex) {
	    throw new IllegalArgumentException(ex.getMessage());

	}

	return ret;
    }

    /**
     * Return an escaped version of the value parameter, suitable for inclusion
     * in a query. Opaques are stringified.
     *
     * @param val The value to escape.
     * @return The stringified value.
     * @exception IllegalArgumentException Thrown if the object is not
     * 	         one of byte[], Integer, Boolean, or String.
     */

    static public String escapeValue(Object val)
	throws IllegalArgumentException {

	// Check type first.

	typeCheckValue(val);

	// Make Opaque out of byte[].

	if (val instanceof byte[]) {
	    val = new Opaque((byte[])val);

	}

	return  escapeValueInternal(val);

    }

    // Check type to make sure it's OK.

    static private void typeCheckValue(Object obj) {
	SLPConfig conf = SLPConfig.getSLPConfig();

	Assert.nonNullParameter(obj, "attribute value vector element");

	if (obj.equals("")) {
	    throw
		new IllegalArgumentException(
				conf.formatMessage("empty_string_value",
						   new Object[0]));
	}

	if (!(obj instanceof Integer) && !(obj instanceof Boolean) &&
	    !(obj instanceof String) && !(obj instanceof byte[])) {
	    throw
		new IllegalArgumentException(
				conf.formatMessage("value_type_error",
						   new Object[0]));
	}

    }

    // We know the value's type is OK, so just escape it.

    private static String escapeValueInternal(Object val) {

	String s;

	// Escape any characters needing it.

	if (val instanceof String) {

	    try {

		s = escapeAttributeString((String)val, false);

	    } catch (ServiceLocationException ex) {
		throw
		    new IllegalArgumentException(ex.getMessage());

	    }

	} else {
	    s = val.toString();

	}

	return s;
    }

    //
    // Methods for dealing with the type of attribute values.
    //

    // Verify the types of incoming attributes.

    protected void
	verifyValueTypes(Vector values_in, boolean dontTypeCheck) {

	SLPConfig conf = SLPConfig.getSLPConfig();

	// Make sure the types of objects passed in are acceptable
	//  and that all objects in the vector have the same type.

	int i, n = values_in.size();
	Class cls = null;

	for (i = 0; i < n; i++) {
	    Object obj = values_in.elementAt(i);

	    typeCheckValue(obj);

	    if (i == 0) {
		cls = obj.getClass();

	    } else if (!cls.equals(obj.getClass()) && !dontTypeCheck) {
		throw
		    new IllegalArgumentException(
				conf.formatMessage("type_mismatch_error",
						   new Object[0]));
	    }

	    // If it's a boolean and there's more than one, signal error
	    // unless multivalued booleans are allowed.

	    if (!dontTypeCheck && i != 0 && obj instanceof Boolean) {
		throw
		    new IllegalArgumentException(
				conf.formatMessage("multivalued_boolean",
						   new Object[0]));

	    }

	    // If it's a byte array, create a Opaque object.

	    if (obj instanceof byte[]) {
		values_in.setElementAt(new Opaque((byte[])obj), i);

	    } else if (obj instanceof String) {
		String val = (String)obj;

		// If it's a string and looks like "1" or "true", then
		//  append a space onto the end.

		try {

		    Object obj2 = evaluate(val);

		    if (!(obj2 instanceof String)) {
			values_in.setElementAt((String)val + " ", i);

		    }

		} catch (ServiceLocationException ex) {

		    // Ignore for now.

		}
	    }
	}

    }

    //
    // Methods for externalizing attributes.
    //

    /**
     * Externalize the attribute into a string that can be written
     * to a byte stream. Includes escaping any characters that
     * need to be escaped.
     *
     * @return String with attribute's external representation.
     * @exception ServiceLocationException Thrown if the
     *			string contains unencodable characters.
     */

    String externalize()
	throws ServiceLocationException {

	if (values == null) {	// keyword attribute...
	    return escapeAttributeString(id, true);
	}

	Vector v = new Vector();

	for (Enumeration e = values.elements(); e.hasMoreElements(); ) {
	    Object o = e.nextElement();
	    String s = null;

	    s = escapeValueInternal(o);

	    v.addElement(s);
	}

	StringBuffer buf =
	    new StringBuffer("(" +
			     escapeAttributeString(id, true) +
			     "=");

	buf.append(SrvLocHeader.vectorToCommaSeparatedList(v));

	buf.append(")");

	return buf.toString();
    }

    //
    // Escaping and unescaping strings.
    //

    /**
     * Escape any escapable characters to a 2 character escape
     * in the attribute string.
     *
     * @param string The String.
     * @param badTag Check for bad tag characters if true.
     * @return The escaped string.
     * @exception ServiceLocationException Thrown if the string
     *			contains a character that can't be encoded.
     */

    static String escapeAttributeString(String string,
					boolean badTag)
	throws ServiceLocationException {

	StringBuffer buf = new StringBuffer();
	int i, n = string.length();

	for (i = 0; i < n; i++) {
	    char c = string.charAt(i);

	    // Check for bad tag characters first.

	    if (badTag && BAD_TAG_CHARS.indexOf(c) != -1) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"bad_id_char",
				new Object[] {Integer.toHexString(c)});
	    }

	    // Escape if the character is reserved.

	    if (canEscape(c)) {
		buf.append(ESCAPE);
	
		String str = escapeChar(c);

		// Pad with zero if less than 2 characters.

		if (str.length() <= 1) {
		    str = "0" + str;

		}

		buf.append(str);

	    } else {

		buf.append(c);

	    }
	}

	return buf.toString();

    }


    /**
     * Convert any 2 character escapes to the corresponding characters.
     *
     * @param string The string to be processed.
     * @param badTag Check for bad tag characters if true.
     * @return The processed string.
     * @exception ServiceLocationException Thrown if an escape
     *			is improperly formatted.
     */

    static String unescapeAttributeString(String string,
					  boolean badTag)
	throws ServiceLocationException {

	// Process escapes.

	int i, n = string.length();
	StringBuffer buf = new StringBuffer(n);

	for (i = 0; i < n; i++) {
	    char c = string.charAt(i);

	    // Check for escaped characters.

	    if (c == ESCAPE) {

		// Get the next two characters.

		if (i > n - 2) {
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"nonterminating_escape",
				new Object[] {string});
		}

		i++;
		c = unescapeChar(string.substring(i, i+2));
		i++;

		// Check whether it's reserved.

		if (!canEscape(c)) {
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"char_not_reserved_attr",
				new Object[] {new Character(c), string});
		}

	    } else {

		// Check whether the character is reserved.

		if (canEscape(c)) {	
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"reserved_not_escaped",
				new Object[] {new Character(c)});
		}

	    }

	    // If we need to check for a bad tag character, do so now.

	    if (badTag && BAD_TAG_CHARS.indexOf(c) != -1) {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"bad_id_char",
				new Object[] {Integer.toHexString(c)});

	    }

	    buf.append(c);

	}

	return buf.toString();
    }

    // Return true if the character c can be escaped.

    private static boolean canEscape(char c) {

	return ((RESERVED.indexOf(c) != -1) || 	
		((c >= CTL_LOWER && c <= CTL_UPPER) || c == DEL));	

    }

    /**
     * Return a string of integers giving the character's encoding in
     * the character set passed in as encoding.
     *
     * @param c The character to escape.
     * @return The character as a string of integers for the encoding.
     */

    static String escapeChar(char c) {

	byte[] b = null;

	try {

	    b = ("" + c).getBytes(Defaults.UTF8);

	} catch (UnsupportedEncodingException ex) {

	    Assert.assert(false, "no_utf8", new Object[0]);

	}

	int code = 0;

	// Assemble the character code.

	if (b.length > 3) {
	    Assert.assert(false,
			  "illegal_utf8",
			  new Object[] {new Character(c)});

	}

	code = (int)(b[0] & 0xFF);

	if (b.length > 1) {
	    code = (int)(code | ((b[1] & 0xFF) << 8));
	}

	if (b.length > 2) {
	    code = (int)(code | ((b[2] & 0xFF) << 16));
	}

	String str = Integer.toHexString(code);

	return str;
    }

    /**
     * Unescape the character encoded as the string.
     *
     * @param ch The character as a string of hex digits.
     * @return The character.
     * @exception ServiceLocationException If the characters can't be
     *		 converted into a hex string.
     */

    static char unescapeChar(String ch)
	throws ServiceLocationException {

	int code = 0;

	try {
	    code = Integer.parseInt(ch, 16);

	} catch (NumberFormatException ex) {

	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"not_a_character",
				new Object[] {ch});
	}

	// Convert to bytes.

	String str = null;
	byte b0 = 0, b1 = 0, b2 = 0, b3 = 0;
	byte b[] = null;

	b0 = (byte) (code & 0xFF);			
	b1 = (byte) ((code >> 8) & 0xFF);
	b2 = (byte) ((code >> 16) & 0xFF);
	b3 = (byte) ((code >> 24) & 0xFF);

	// We allow illegal UTF8 encoding so we can decode byte arrays.

	if (b3 != 0) {
	    b = new byte[3];
	    b[3] = b3;
	    b[2] = b2;
	    b[1] = b1;
	    b[0] = b0;
	} else if (b2 != 0) {
	    b = new byte[3];
	    b[2] = b2;
	    b[1] = b1;
	    b[0] = b0;
	} else if (b1 != 0) {
	    b = new byte[2];
	    b[1] = b1;
	    b[0] = b0;
	} else {
	    b = new byte[1];
	    b[0] = b0;
	}

	// Make a string out of it.

	try {
	    str = new String(b, Defaults.UTF8);

	} catch (UnsupportedEncodingException ex) {

	    Assert.assert(false, "no_utf8", new Object[0]);

	}

	int len = str.length();

	if (str.length() > 1) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"more_than_one",
				new Object[] {ch});

	}

	return (len == 1 ? str.charAt(0):(char)0);
    }

    /**
     * Merge the values in newAttr into the attribute in the hashtable
     * if a duplicate attribute, signal error if a type mismatch.
     * Both the return vector and hashtable are updated, but the
     * newAttr parameter is left unchanged.
     *
     * @param attr The ServiceLocationAttribute to check.
     * @param attrHash A Hashtable containing the attribute tags as
     *			keys and the attributes as values.
     * @param returns A Vector in which to put the attribute when done.
     * @param dontTypeCheck If this flag is true, the value vector
     *			   may have two booleans, may
     *			   contain differently typed objects, or the
     *			   function may merge a keyword and nonkeyword
     *			   attribute.
     * @exception ServiceLocationException Thrown if a type mismatch
     *			occurs.
     */

    static void
	mergeDuplicateAttributes(ServiceLocationAttribute newAttr,
				 Hashtable attrTable,
				 Vector returns,
				 boolean dontTypeCheck)
	throws ServiceLocationException {

	// Look up the attribute

	String tag = newAttr.getId().toLowerCase();
	ServiceLocationAttribute attr =
	    (ServiceLocationAttribute)attrTable.get(tag);

	// Don't try this trick with ServerAttributes!

	Assert.assert((!(attr instanceof ServerAttribute) &&
		       !(newAttr instanceof ServerAttribute)),
		      "merge_servattr",
		      new Object[0]);

	// If the attribute isn't in the hashtable, then add to
	//  vector and hashtable.

	if (attr == null) {
	    attrTable.put(tag, newAttr);
	    returns.addElement(newAttr);
	    return;

	}


	Vector attrNewVals = newAttr.values;
	Vector attrVals = attr.values;

	// If both keywords, nothing further to do.

	if (attrVals == null && attrNewVals == null) {
	    return;

	}

	// If we are not typechecking and one is keyword while the other
	//  is not, then simply merge in the nonkeyword. Otherwise,
	//  throw a type check exception.

	if ((attrVals == null && attrNewVals != null) ||
	    (attrNewVals == null && attrVals != null)) {

	    if (dontTypeCheck) {
		Vector vals = (attrNewVals != null ? attrNewVals:attrVals);
		attr.values = vals;
		newAttr.values = vals;

	    } else {
		throw
		    new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"attribute_type_mismatch",
				new Object[] {newAttr.getId()});

	    }

	} else {

	    // Merge the two vectors. We type check against the attrVals
	    //  vector, if we are type checking.

	    int i, n = attrNewVals.size();
	    Object o = attrVals.elementAt(0);
	    Class c = o.getClass();

	    for (i = 0; i < n; i++) {
		Object no = attrNewVals.elementAt(i);

		// Check for type mismatch, throw exception if
		//  we are type checking.

		if ((c != no.getClass()) && !dontTypeCheck) {
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"attribute_type_mismatch",
				new Object[] {newAttr.getId()});

		}

		// If we are typechecking, and we get two opposite
		//  booleans, we need to throw an exception.

		if (no instanceof Boolean && !no.equals(o) && !dontTypeCheck) {
		    throw
			new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"boolean_incompat",
				new Object[] {newAttr.getId()});

		}

		// Add the value if it isn't already there.

		if (!attrVals.contains(no)) {
		    attrVals.addElement(no);

		}
	    }

	    // Set the new attribute's values so they are the same as the old.

	    newAttr.values = attrVals;

	}
    }

    //
    // Object overrides.
    //

    /**
     * Return true if the object equals this attribute.
     */

    public boolean equals(Object o) {

	if (!(o instanceof ServiceLocationAttribute)) {
	    return false;

	}

	if (o == this) {
	    return true;

	}

	ServiceLocationAttribute sla = (ServiceLocationAttribute)o;

	// check equality of contents, deferring check of all values

	Vector vSLA = sla.values;

	if (!sla.getId().equalsIgnoreCase(id)) {
	    return false;

	}

	if (values == null && vSLA == null) {
	    return true;

	}

	if ((values == null && vSLA != null) ||
	    (values != null && vSLA == null)) {
	    return false;

	}

	if (values.size() != vSLA.size()) {
	    return false;

	}

	// Check contents.

	Object oSLA = vSLA.elementAt(0);
	o = values.elementAt(0);

	if (o.getClass() != oSLA.getClass()) {
	    return false;

	}

	int i, n = vSLA.size();

	for (i = 0; i < n; i++) {
	    oSLA = vSLA.elementAt(i);

	    if (!values.contains(oSLA)) {
		return false;

	    }
	}

	return true;
    }

    /**
     * Return a human readable string for the attribute.
     */

    public String toString() {

	StringBuffer s = new StringBuffer("(");

	s.append(id);

	if (values != null) {
	    s.append("=");

	    int i, n = values.size();

	    for (i = 0; i < n; i++) {
		Object o = values.elementAt(i);

		// Identify type.

		if (i == 0) {
		    s.append(o.getClass().getName());
		    s.append(":");

		} else {
		    s.append(",");

		}
	
		// Stringify object.
	
		s.append(o.toString());

	    }
	}

	s.append(")");

	return s.toString();
    }

    // Overrides Object.hashCode().

    public int hashCode() {
	return id.toLowerCase().hashCode();

    }

}
