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

//  SCCS Status:      @(#)SLPHeaderV1.java	1.2	05/10/99
//  SLPHeaderV1.java: SLPv1 Header.
//  Author:           James Kempf
//  Created On:       Thu Sep 10 15:12:14 1998
//  Last Modified By: James Kempf
//  Last Modified On: Wed Jan 20 15:38:07 1999
//  Update Count:     59
//

package com.sun.slp;

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

/**
 * The SLPHeaderV1 class models the SLPv1 server side header.
 *
 * @version 1.2 00/10/14
 * @author James Kempf
 */

class SLPHeaderV1 extends SrvLocHeader implements Cloneable {

    // Version number.

    static int VERSION = 1;

    // Function code for message reply.

    int replyFunctionCode = SrvLocHeader.SrvAck;

    // Message description.

    String des = "";

    // Various header flags.

    protected static final int NOFLAG   = 0x00;
    protected static final int OVERFLOW = 0x80;
    protected static final int MONOLING = 0x40;
    protected static final int URLSIG   = 0x20;
    protected static final int ATTRSIG  = 0x10;
    protected static final int FRESH    = 0x08;

    protected static int LANG_CODE_BYTES = 2;

    protected static int HEADER_BYTES =
	VERSION_FUNCTION_BYTES + LANG_CODE_BYTES + 8;

    // Characters to escape.

    final private static String UNESCAPABLE_CHARS = ",=!></*()";
    final private static String ESCAPABLE_CHARS =
	UNESCAPABLE_CHARS + "&#;";

    String charCode = IANACharCode.UTF8;	// character encoding.
    boolean monolingual = false;		// monolingual flag.

    // Used to construct a header in SrvLocHeader.newInstance().

    SLPHeaderV1() {
	super();

	version = VERSION;

    }

    // Assign reply code based on function code type, then use superclass
    //  method to parse header.

    void parseHeader(int functionCode, DataInputStream dis)
	throws ServiceLocationException, IOException {

	this.functionCode = functionCode;

	// We ignore the error case here.

	switch (functionCode) {

	case SrvLocHeader.SrvReq:
	    replyFunctionCode = SrvLocHeader.SrvRply;
	    break;

	case SrvLocHeader.AttrRqst:
	    replyFunctionCode = SrvLocHeader.AttrRply;
	    break;

	case SrvLocHeader.SrvTypeRqst:
	    replyFunctionCode = SrvLocHeader.SrvTypeRply;
	    break;

	}

	length     = getInt(dis);
	byte flags   = (byte) ((char)dis.read() & 0xFF);
	nbytes++;

	overflow = ((flags & OVERFLOW) != 0x00);
	fresh = false; // fresh gets set on output in SLPv1
	monolingual = ((flags & MONOLING) != 0x00);
	boolean urlAuth = ((flags & URLSIG) != 0x00);
	boolean attrAuth = ((flags & ATTRSIG) != 0x00);

	// Security not handled for SLPv1.

	if (urlAuth || attrAuth) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.AUTHENTICATION_FAILED,
				"v1_no_security",
				new Object[0]);
	}

	int dialect    = (int) ((char)dis.read() & 0xFF);
	nbytes++;

	// Dialect must be zero.

	if (dialect != 0) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_nonzero_dialect",
				new Object[0]);

	}

	byte a_bTemp[] = new byte[LANG_CODE_BYTES];
	a_bTemp[0] = (byte) dis.read();
	a_bTemp[1] = (byte) dis.read();
	nbytes += 2;

	try {
	    locale = new Locale(new String(a_bTemp, IANACharCode.ASCII), "");

	} catch (UnsupportedEncodingException ex) {

	}

	int intCharCode = getInt(dis);
	charCode = IANACharCode.decodeCharacterEncoding(intCharCode);

	xid     = (short)getInt(dis);

	errCode = ServiceLocationException.OK;
    }

    // Parse an incoming V1 message and return the SrvLocMsg object.

    SrvLocMsg parseMsg(DataInputStream dis)
	throws ServiceLocationException,
	       IOException,
	       IllegalArgumentException {

	SrvLocMsg msg = null;

	// If this is a *multicast* request, we reject it except for DAAdvert.
	//  Multicast requests are only taken by SA servers.

	if (mcast && (functionCode != SrvLocHeader.DAAdvert)) {
	    return null;

	}

	// Switch and convert according to function code.

	switch (functionCode) {

	case SrvLocHeader.SrvReq:
	    msg = new SLPV1SSrvMsg(this, dis);
	    break;

	case SrvLocHeader.SrvReg:
	    msg = new SLPV1SSrvReg(this, dis);
	    break;

	case SrvLocHeader.SrvDereg:
	    msg = new SLPV1SSrvDereg(this, dis);
	    break;

	case SrvLocHeader.AttrRqst:
	    msg = new SLPV1SAttrMsg(this, dis);
	    break;

	case SrvLocHeader.SrvTypeRqst:
	    msg = new SLPV1SSrvTypeMsg(this, dis);
	    break;

	case SrvLocHeader.DAAdvert:
	    msg = new SLPV1CDAAdvert(this, dis);
	    break;

	default:
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"function_code_error",
				new Object[] {
		    new Integer(functionCode)});

	}

	// Check for size overflow.

	if (nbytes > length) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"length_overflow",
				new Object[] {
		    new Integer(nbytes), new Integer(length)});

	}

	return msg;

    }

    // Externalize the message by converting it to bytes and writing
    //  it to the output stream.

    public void
	externalize(ByteArrayOutputStream baos, boolean mcast, boolean isTCP)
	throws ServiceLocationException {

	// Need to put in the error code or previous responders.

	ByteArrayOutputStream fin = new ByteArrayOutputStream();

	if (functionCode == SrvLocHeader.SrvAck ||
	    functionCode == SrvLocHeader.SrvTypeRply ||
	    functionCode == SrvLocHeader.SrvRply ||
	    functionCode == SrvLocHeader.AttrRply ||
	    functionCode == SrvLocHeader.DAAdvert) {
	    putInt(errCode, fin);

	} else {

	    // Parse out previous responders. Note there will only be some
	    //  if the error code is not put out.

	    if (previousResponders != null) {
		parseCommaSeparatedListOut(previousResponders, fin);

	    }
	}

	// Parse payload out if error code is OK and payload is nonnull.

	if (payload != null && errCode == ServiceLocationException.OK) {
	    fin.write(payload, 0, payload.length);
	}

	// Don't touch payload here, somebody may put in a previousResponder
	//  and resend the message.

	byte[] npayload = fin.toByteArray();

	// Set overflow flag if buffer is too large and this isn't going out
	//  via TCP.

	if (((npayload.length + 12) > SLPConfig.getSLPConfig().getMTU()) &&
	    !isTCP) {
	    overflow = true;

	}

	baos.write((byte) (0xFF & version));
	nbytes++;
	baos.write((byte) (0xFF & functionCode));
	nbytes++;

	length = npayload.length +12; // the 12 is the length of this header!

	putInt(length, baos);  // what about overflow???

	byte flags = 0X00;

	if (overflow) {
	    flags = (byte)(flags | OVERFLOW);
	} else {
	    flags = (byte)(flags & ~OVERFLOW);
	}

	if (monolingual) {
	    flags = (byte)(flags | MONOLING);
	} else {
	    flags = (byte)(flags & ~MONOLING);
	}

	if (fresh) {
	    flags = (byte)((flags | FRESH) & 0XFF);
	} else {
	    flags = (byte)((flags & ~FRESH) & 0XFF);
	}

	baos.write((byte) (0xFF & flags));
	nbytes++;
	baos.write((byte) (0xFF & 0)); // dialect...
	nbytes++;

	String language = locale.getLanguage();

	baos.write((byte) (0xFF & language.charAt(0)));
	baos.write((byte) (0xFF & language.charAt(1)));
	nbytes += 2;

	int intCharCode = 0;

	try {
	    intCharCode = IANACharCode.encodeCharacterEncoding(charCode);

	} catch (ServiceLocationException ex) {
	    Assert.assert(false,
			  "v1_unsupported_encoding",
			  new Object[] {charCode});

	}

	putInt(intCharCode, baos);
	putInt(xid, baos);

	// Write the body.

	baos.write(npayload, 0, npayload.length);
	nbytes += npayload.length;
    }

    // Create an error reply using the reply code. Calculate the
    //  error code using the exception.

    SrvLocMsg makeErrorReply(Exception ex) {

	// If this is a DAAdvert, then no error reply is returned
	//  because V1 doesn't support unicast SrvRqst for DAAdvert.

	if (functionCode == SrvLocHeader.DAAdvert) {
	    return null;

	}

	// Clone the header to make sure that everything else is the same.
	//  We don't want to use the same header because it may be tested
	//  elsewhere.

	SLPHeaderV1 hdr = null;

	try {
	    hdr = (SLPHeaderV1)this.clone();

	} catch (CloneNotSupportedException exx) {

	    // We know we support it.

	}

	hdr.fresh = false;
	hdr.overflow = false;
	hdr.mcast = false;
	hdr.functionCode = replyFunctionCode;

	// We should *not* be getting a null exception down this path!

	Assert.assert(ex != null,
		      "null_parameter",
		      new Object[] {ex});

	if (ex instanceof ServiceLocationException) {

	    hdr.errCode = ((ServiceLocationException)ex).getErrorCode();

	    // Handle monolingual bit here. If the exception is
	    //  LANGUAGE_NOT_SUPPORTED and the message type is
	    //  either SrvRqst or AttrRqst, then we simply return an
	    //  empty message unless the monolingual flag is on.

	    if (hdr.errCode ==
		ServiceLocationException.LANGUAGE_NOT_SUPPORTED) {

		try {

		    if (!hdr.monolingual) {

			if (hdr.functionCode == SrvLocHeader.SrvReq) {

			    return SLPV1SSrvMsg.makeEmptyReply(hdr);

			} else if (hdr.functionCode == SrvLocHeader.AttrRqst) {

			    return SLPV1SAttrMsg.makeEmptyReply(hdr);

			}
		    }

		} catch (ServiceLocationException exx) {

		    hdr.monolingual = true;
		    hdr.makeErrorReply(exx);

		}

		// Otherwise, we just ignore it.
	    }

	    // Anything over AUTHENTICATION_FAILED is an internal error in V1.

	    if (hdr.errCode > ServiceLocationException.AUTHENTICATION_FAILED) {
		hdr.errCode = ServiceLocationException.PARSE_ERROR;

	    }

	} else if (ex instanceof IllegalArgumentException ||
		   ex instanceof IOException) {
	    hdr.errCode = ServiceLocationException.PARSE_ERROR;

	} else {
	    hdr.errCode = ServiceLocationException.PARSE_ERROR;

	}

	// Construct header description.

	hdr.constructDescription("SrvLocMsg", "");

	return hdr;
    }

    // Return a reply header with flags properly set.

    SLPHeaderV1 makeReplyHeader() {

	SLPHeaderV1 hdr = null;

	try {
	    hdr = (SLPHeaderV1)this.clone();

	} catch (CloneNotSupportedException ex) {

	    // We know that we support it.
	}

	hdr.functionCode = replyFunctionCode;
	hdr.length = 0;
	hdr.previousResponders = null;
	hdr.scopes = null;
	hdr.overflow = false;
	hdr.fresh = false;
	hdr.mcast = false;
	hdr.nbytes = 0;

	return hdr;
    }

    // Construct a description of the header. Messages add individual
    //  descriptions to this.

    protected void constructDescription(String msgType,
					String msgDescription) {

	des =
	    msgType + ":version=``" + version + "''\n" +
	    "       functionCode=``" + functionCode + "''\n" +
	    "       length=``" + length + "''\n" +
	    "       overflow=``" + overflow + "''\n" +
	    "       mcast = ``" + mcast + "''\n" +
	    "       fresh=``" + fresh + "''\n" +
	    "       monolingual=``" + monolingual + "''\n" +
	    "       charCode=``" + charCode + "''\n" +
	    "       locale = ``" + locale + "''\n" +
	    "       xid=``0x" + Integer.toHexString(xid) + "''\n" +
	    "       errCode=``" + errCode + "''\n" +
	    "       previousResponders=``" + previousResponders + "''\n" +
	    "       scopes=``" + scopes + "''\n" +
	    msgDescription;

    }

    // Return display string.

    public String toString() {

	return des;

    }

    //
    // Validation Utilities.
    //

    /**
     * Validate the scope name to be sure it doesn't contain forbidden
     * chars and isn't one of the reserved scope names.
     */

    static void validateScope(String scope)
	throws ServiceLocationException
    {
	if (scope.indexOf('/') != -1 || scope.indexOf(',') != -1 ||
	    scope.indexOf(':') != -1) {
	    throw new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_scope_char_res",
				new Object[] {scope});
	}

	// Check against reserved scope names.

	if (scope.equalsIgnoreCase("local") ||
	    scope.equalsIgnoreCase("remote")) {
	    throw new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_scope_name_res",
				new Object[] {scope});
	}

    }

    /**
     * Remove IANA from the service type name.
     *
     * @param serviceType The service type and naming authority.
     * @return The service type name with IANA removed.
     */

    static String removeIANA(String serviceType) {

	// Substitute null string for IANA.

	int idx = 0;

	serviceType = serviceType.toLowerCase();

	if ((idx = serviceType.indexOf("." + ServiceType.IANA)) != -1) {
	    serviceType = serviceType.substring(0, idx);

	}

	return serviceType;
    }

    // Check whether this is a vaild SLPv1 service type. Also remove
    //  IANA.

    static String checkServiceType(String stype)
	throws ServiceLocationException {

	// Check for trailing colon and remove it.

	if (!stype.endsWith(":")) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_service_type_format",
				new Object[] {stype});

	}

	String type = stype.substring(0, stype.length()-1);

	// Remove IANA.

	type = removeIANA(type);

	// Check syntax.

	ServiceType st = new ServiceType(type);

	// Reject if abstract type. SLPv1 doesn't handle
	//  abstract types.

	if (st.isAbstractType()) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_abstract_type",
				new Object[0]);

	}

	// Reject if not a service: type. SLPv1 doesn't handle
	//  nonservice: types.

	if (!st.isServiceURL()) {
	    throw
		new ServiceLocationException(
				ServiceLocationException.PARSE_ERROR,
				"v1_not_surl",
				new Object[0]);

	}

	return type;
    }

    //
    // Parsing Utilities.
    //

    // Parse string, bump byte count.

    byte[] getString(StringBuffer buf, DataInputStream dis)
	throws ServiceLocationException, IOException {

	int i, n = 0;

	// Get length.

	n = getInteger(dis);

	byte[] bytes = new byte[n];

	// Read bytes.

	dis.readFully(bytes, 0, n);

	// If the encoding type is Unicode, then figure out first
	//  whether it's big or little endian from byte header. Future
	//  calls won't have to go through this grief unless the byte header
	//  is missing.

	if (this.charCode == IANACharCode.UNICODE) {

	    this.charCode = IANACharCode.getUnicodeEndianess(bytes);

	}

	String charCode = this.charCode;

	// If we are still just Unicode by this point, then we need to
	//  add the big endian bytes to the beginning of the array.
	//  Otherwise, Java won't parse it. Note that we don't change
	//  the flag in the header, since we will need to convert the
	//  next time around as well.

	if (charCode == IANACharCode.UNICODE) {
	    charCode = IANACharCode.UNICODE_BIG;

	    bytes = IANACharCode.addBigEndianFlag(bytes);

	}

	// Convert the bytes into a string.

	buf.setLength(0);

	buf.append(getBytesString(bytes, charCode));

	return bytes;
    }

    // Parse out string, bump byte count. Use header encoding.

    byte[] putString(String string, ByteArrayOutputStream baos) {

	// If the charCode is UNICODE, arbirtarily change to big or little,
	//  while Java will parse.

	if (charCode == IANACharCode.UNICODE) {
	    charCode = IANACharCode.UNICODE_BIG;

	}

	byte[] bytes = putStringField(string, baos, charCode);

	nbytes += bytes.length;

	return bytes;

    }

    // Parse in a service URL including lifetime if necessary.

    protected ServiceURL
	parseServiceURLIn(DataInputStream dis,
			  boolean lifeTimeToo,
			  short errCode)
	throws ServiceLocationException, IOException {

	int lifetime = 0;
	StringBuffer buf = new StringBuffer();

	if (lifeTimeToo) {
	    lifetime = getInt(dis);
	}

	getString(buf, dis);

	ServiceURL url = null;

	try {

	    url = new ServiceURLV1(buf.toString(), lifetime);

	} catch (IllegalArgumentException ex) {

	    throw
		new ServiceLocationException(errCode,
					     "malformed_url",
					     new Object[] {ex});
	}

	return url;
    }

    // Parse out a service URL including lifetime if required.

    void
	parseServiceURLOut(ServiceURL surl,
			   boolean lifetimeToo,
			   ByteArrayOutputStream baos)
	throws ServiceLocationException {

	String ssurl = surl.toString();

	if (lifetimeToo) {
	    putInt(surl.getLifetime(), baos);
	}

	putString(ssurl, baos);

    }

    // Parse in a list of attributes, returing a vector of
    //  ServiceLocationAttribute objects.

    protected Vector parseAttributeVectorIn(DataInputStream dis)
	throws ServiceLocationException, IOException {

	StringBuffer buf = new StringBuffer();

	getString(buf, dis);

	SLPConfig config = SLPConfig.getSLPConfig();

	// Parse the list into ServiceLocationAttribute objects.

	Vector attrForms = parseCommaSeparatedListIn(buf.toString(), false);

	int i, n = attrForms.size();

	for (i = 0; i < n; i++) {
	    String attrForm =
		(String)attrForms.elementAt(i);

	    attrForms.setElementAt(new ServiceLocationAttributeV1(attrForm,
								  charCode,
								  false),
				   i);
	}

	return attrForms;
    }
	
    // Parse out a V1 attribute vector.

    void
	parseAttributeVectorOut(Vector attrs,
				ByteArrayOutputStream baos)
	throws ServiceLocationException {

	Enumeration en = attrs.elements();
	Vector strings = new Vector();

	// Convert the attributes to strings, escaping characters to
	//  escape.

	while (en.hasMoreElements()) {
	    ServiceLocationAttribute attr =
		(ServiceLocationAttribute)en.nextElement();

	    // Make an SLPv1 attribute out of it, so we can
	    //  externalize it with the v1 encoding scheme.

	    ServiceLocationAttributeV1 attrv1 =
		new ServiceLocationAttributeV1(attr);
	    attrv1.charCode = charCode;
	    String out = attrv1.externalize();

	    strings.addElement(out);

	}

	// Parse it out.

	parseCommaSeparatedListOut(strings, baos);

    }

    // Parse in previous responders.

    void parsePreviousRespondersIn(DataInputStream dis)
	throws ServiceLocationException, IOException {

	StringBuffer buf = new StringBuffer();

	getString(buf, dis);

	previousResponders =
	    parseCommaSeparatedListIn(buf.toString(), true);

    }

    // Put out a vector of strings.

    void putStringVector(Vector v, ByteArrayOutputStream baos) {

	int i, n = v.size();

	// Put out the total number of strings.

	putInt(n, baos);

	// Put out the strings.

	for (i = 0; i < n; i++) {

	    putString((String)v.elementAt(i), baos);
	}
    }

    // Return an SLPv1 DAAdvert.

    SDAAdvert
	getDAAdvert(short xid,
		    long timestamp,
		    ServiceURL url,
		    Vector scopes,
		    Vector attrs)
	throws ServiceLocationException {

	// If scopes vector is null, then return all scopes for this
	//  DA.

	if (scopes.size() <= 0) {
	    scopes = SLPConfig.getSLPConfig().getSAConfiguredScopes();

	}

	return new SLPV1SDAAdvert(this, xid, timestamp, url, scopes, attrs);

    }

    // Reimplement clone() to get the header size right.

    public Object clone()
	throws CloneNotSupportedException {
	SLPHeaderV1 hdr = (SLPHeaderV1)super.clone();

	hdr.nbytes = HEADER_BYTES + 2;  // for error code...

	return hdr;
    }
}
