The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/* $Header: /home/cvsroot/NetZ3950/yazwrap/send.c,v 1.12 2006/05/08 10:54:14 mike Exp $ */

/*
 * yazwrap/send.c -- wrapper functions for Yaz's client API.
 *
 * This file provides functions which (we hope) will be easier to
 * invoke via XS than the raw Yaz API.  We do this by providing fewer
 * functions at a higher level; and, where appropriate, using more
 * primitive C data types.
 */

#include <unistd.h>
#include <yaz/proto.h>
#include <yaz/pquery.h>		/* prefix query compiler */
#include <yaz/ccl.h>		/* CCL query compiler */
#include <yaz/yaz-ccl.h>	/* CCL-to-RPN query converter */
#include <yaz/otherinfo.h>
#include <yaz/charneg.h>
#include "ywpriv.h"


Z_ReferenceId *make_ref_id(Z_ReferenceId *buf, databuf refId);
static Odr_oid *record_syntax(ODR odr, int preferredRecordSyntax);
static databuf encode_apdu(ODR odr, Z_APDU *apdu, char **errmsgp);
static int prepare_odr(ODR *odrp, char **errmsgp);
static databuf nodata(char *msg);


/*
 * Errors are indicated by returning a databuf with a null data member,
 * with *errmsgp pointed at an error message whose memory is managed by
 * this module.
 */
databuf makeInitRequest(databuf referenceId,
			int preferredMessageSize,
			int maximumRecordSize,
			mnchar *user,
			mnchar *password,
			mnchar *groupid,
			mnchar *implementationId,
			mnchar *implementationName,
			mnchar *implementationVersion,
			mnchar *charset,
			mnchar *language,
			char **errmsgp)
{
    static ODR odr = 0;
    Z_APDU *apdu;
    Z_InitRequest *req;
    Z_ReferenceId zr;
    Z_IdAuthentication auth;
    Z_IdPass id;

    if (!prepare_odr(&odr, errmsgp))
	return nodata((char*) 0);
    apdu = zget_APDU(odr, Z_APDU_initRequest);
    req = apdu->u.initRequest;

    req->referenceId = make_ref_id(&zr, referenceId);
    /*
     * ### We should consider allowing the caller to influence which
     * of the following options are set.  The ones marked with the
     * Mystic Rune Of The Triple Hash are actually not supported in
     * Net::Z3950.pm as I write.
     */
    ODR_MASK_SET(req->options, Z_Options_search);
    ODR_MASK_SET(req->options, Z_Options_present);
    ODR_MASK_SET(req->options, Z_Options_namedResultSets);
    ODR_MASK_SET(req->options, Z_Options_triggerResourceCtrl); /* ### */
    ODR_MASK_SET(req->options, Z_Options_scan);
    ODR_MASK_SET(req->options, Z_Options_sort);	/* ### */
    ODR_MASK_SET(req->options, Z_Options_extendedServices); /* ### */
    ODR_MASK_SET(req->options, Z_Options_delSet); /* ### */

    ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_1);
    ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_2);
    ODR_MASK_SET(req->protocolVersion, Z_ProtocolVersion_3);

    *req->preferredMessageSize = preferredMessageSize;
    *req->maximumRecordSize = maximumRecordSize;

    /*
     * We interpret the `user', `password' and `group' arguments as
     * follows: if `user' is not specified, then authentication is
     * omitted (which is more or less the same as "anonymous"
     * authentication); if `user' is specified but not `password',
     * then it's treated as an "open" authentication token; if both
     * `user' and `password' are specified, then they are used in
     * "idPass" authentication, together with `group' if specified.
     */
    if (user != 0) {
	req->idAuthentication = &auth;
	if (password == 0) {
	    auth.which = Z_IdAuthentication_open;
	    auth.u.open = user;
	} else {
	    auth.which = Z_IdAuthentication_idPass;
	    auth.u.idPass = &id;
	    id.userId = user;
	    id.groupId = groupid;
	    id.password = password;
	}
    }

    if (charset || language) {
        Z_OtherInformation **p;
        Z_OtherInformationUnit *p0;

        yaz_oi_APDU(apdu, &p);
         
        if ((p0=yaz_oi_update(p, odr, NULL, 0, 0))) {
            ODR_MASK_SET(req->options, Z_Options_negotiationModel);
             
            p0->which = Z_OtherInfo_externallyDefinedInfo;
            p0->information.externallyDefinedInfo =
                yaz_set_proposal_charneg(
                    odr,
                    (const char**)&charset,
                    charset ? 1 : 0,
                    (const char**)&language, language ? 1 : 0, 1);
        }
    }
     

    if (implementationId != 0)
	req->implementationId = implementationId;
    if (implementationName != 0)
	req->implementationName = implementationName;
    if (implementationVersion != 0)
	req->implementationVersion = implementationVersion;

    return encode_apdu(odr, apdu, errmsgp);
}


/*
 * I feel really uncomfortable about that fact that if this function
 * fails, the caller has no way to know why -- it could be an illegal
 * record syntax, an unsupported query type, a bad search command or
 * failure to encode the APDU.  Oh well.
 */
databuf makeSearchRequest(databuf referenceId,
			  int smallSetUpperBound,
			  int largeSetLowerBound,
			  int mediumSetPresentNumber,
			  char *resultSetName,
			  char *databaseName,
			  char *smallSetElementSetName,
			  char *mediumSetElementSetName,
			  int preferredRecordSyntax,
			  int queryType,
			  char *query,
			  char **errmsgp)
{
    static ODR odr = 0;
    Z_APDU *apdu;
    Z_SearchRequest *req;
    Z_ReferenceId zr;
    Z_ElementSetNames smallES, mediumES;
    oident attrset;
    int oidbuf[20];		/* more than enough */
    Z_Query zquery;
    Odr_oct ccl_query;
    struct ccl_rpn_node *rpn;
    int error, pos;
    static CCL_bibset bibset;
    Z_External *ext;

    if (!prepare_odr(&odr, errmsgp))
	return nodata((char*) 0);
    apdu = zget_APDU(odr, Z_APDU_searchRequest);
    req = apdu->u.searchRequest;

    req->referenceId = make_ref_id(&zr, referenceId);
    *req->smallSetUpperBound = smallSetUpperBound;
    *req->largeSetLowerBound = largeSetLowerBound;
    *req->mediumSetPresentNumber = mediumSetPresentNumber;
    *req->replaceIndicator = 1;
    if (strcmp (resultSetName, "0") != 0)
	req->resultSetName = resultSetName;
    req->num_databaseNames = 1;
    req->databaseNames = &databaseName;

    /* Translate a single element-set names into a Z_ElementSetNames */
    req->smallSetElementSetNames = &smallES;
    smallES.which = Z_ElementSetNames_generic;
    smallES.u.generic = smallSetElementSetName;

    req->mediumSetElementSetNames = &mediumES;
    mediumES.which = Z_ElementSetNames_generic;
    mediumES.u.generic = mediumSetElementSetName;

    /* Convert from our enumeration to the corresponding OID */
    if ((req->preferredRecordSyntax =
	 record_syntax(odr, preferredRecordSyntax)) == 0)
	return nodata(*errmsgp = "can't convert record syntax");

    /* Convert from our querytype/query pair to a Z_Query */
    req->query = &zquery;

    switch (queryType) {
    case QUERYTYPE_PREFIX:
	/* ### Is type-1 always right?  What about type-101 when under v2? */
        zquery.which = Z_Query_type_1;
        if ((zquery.u.type_1 = p_query_rpn (odr, PROTO_Z3950, query)) == 0)
	    return nodata(*errmsgp = "can't compile PQN query");
        break;

    case QUERYTYPE_CCL:
        zquery.which = Z_Query_type_2;
        zquery.u.type_2 = &ccl_query;
        ccl_query.buf = (unsigned char*) query;
        ccl_query.len = strlen(query);
        break;

    case QUERYTYPE_CCL2RPN:
        zquery.which = Z_Query_type_1;
	if (bibset == 0) {
	    FILE *fp;
	    bibset = ccl_qual_mk();
	    if ((fp = fopen("ccl.qual", "r")) != 0) {
		ccl_qual_file(bibset, fp);
		fclose(fp);
	    } else if (errno != ENOENT) {
		return nodata(*errmsgp = "can't read CCL qualifier file");
	    }
	}
        if ((rpn = ccl_find_str(bibset, query, &error, &pos)) == 0)
	    return nodata(*errmsgp = (char*) ccl_err_msg(error));
        if ((zquery.u.type_1 = ccl_rpn_query(odr, rpn)) == 0)
	    return nodata(*errmsgp = "can't encode Type-1 query");
        attrset.proto = PROTO_Z3950;
        attrset.oclass = CLASS_ATTSET;
        attrset.value = VAL_BIB1; /* ### should be configurable! */
        zquery.u.type_1->attributeSetId = oid_ent_to_oid(&attrset, oidbuf);
        ccl_rpn_delete (rpn);
        break;

    case QUERYTYPE_CQL:
        zquery.which = Z_Query_type_104;
        ext = (Z_External*) odr_malloc(odr, sizeof(*ext));
        ext->direct_reference = odr_getoidbystr(odr, "1.2.840.10003.16.2");
        ext->indirect_reference = 0;
        ext->descriptor = 0;
        ext->which = Z_External_CQL;
        ext->u.cql = odr_strdup(odr, query);
        zquery.u.type_104 = ext;
        break;

    default:
	return nodata(*errmsgp = "unknown queryType");
    }

    return encode_apdu(odr, apdu, errmsgp);
}


/* Inspired by the scan implementation from client.c
 * in the source package of the YAZ C toolkit available
 * at http://www.indexdata.dk/yaz/
 */
databuf makeScanRequest(databuf referenceId,
                        char *databaseName,
                        int stepSize,
                        int numberOfTermsRequested,
                        int preferredPositionInResponse,
                        int queryType,
                        char *query,
			char **errmsgp)                    
{
    static ODR odr = 0;
    Z_APDU *apdu;
    Z_ScanRequest *req;
    Z_ReferenceId zr;
    static CCL_bibset bibset;
    int oid[OID_SIZE];

    if (!prepare_odr(&odr, errmsgp))
        return nodata((char*) 0);

    apdu = zget_APDU(odr, Z_APDU_scanRequest);
    req = apdu->u.scanRequest;

    req->referenceId = make_ref_id(&zr, referenceId);
    req->num_databaseNames = 1;
    req->databaseNames = &databaseName;
    req->stepSize = &stepSize;
    req->numberOfTermsRequested = &numberOfTermsRequested;
    req->preferredPositionInResponse = &preferredPositionInResponse;

    /* ### should this share code with makeSearchRequest()? */
    if (queryType == QUERYTYPE_CCL2RPN) {
        oident bib1;
        int error, pos;
        struct ccl_rpn_node *rpn;

        rpn = ccl_find_str (bibset,  query, &error, &pos);
        if (bibset == 0) {
            FILE *fp;
            bibset = ccl_qual_mk();
            if ((fp = fopen("ccl.qual", "r")) != 0) {
                ccl_qual_file(bibset, fp);
                fclose(fp);
            } else if (errno != ENOENT) {
                return nodata (*errmsgp = "can't read CCL qualifier file");
            }
        }
        rpn = ccl_find_str (bibset,  query, &error, &pos);
        if (error) {
            return nodata (*errmsgp = (char *) ccl_err_msg(error));
        }
        bib1.proto = PROTO_Z3950;
        bib1.oclass = CLASS_ATTSET;
        bib1.value = VAL_BIB1;
        req->attributeSet = oid_ent_to_oid (&bib1, oid);

        if (!(req->termListAndStartPoint = ccl_scan_query (odr, rpn))) {
           return  nodata (*errmsgp = "can't convert CCL to Scan term");
        }
        ccl_rpn_delete (rpn);

    } else {  /* QUERYTYPE_PREFIX */
        YAZ_PQF_Parser pqf_parser = yaz_pqf_create ();

        if (!(req->termListAndStartPoint =
            yaz_pqf_scan(pqf_parser, odr, &req->attributeSet, query)))
        {
            size_t off;
            (void) yaz_pqf_error (pqf_parser,(const char **) errmsgp, &off);
            yaz_pqf_destroy (pqf_parser);
            return nodata(*errmsgp);
        }
        yaz_pqf_destroy (pqf_parser);
    }

    return encode_apdu(odr, apdu, errmsgp);
}


databuf makePresentRequest(databuf referenceId,
			   char *resultSetId,
			   int resultSetStartPoint,
			   int numberOfRecordsRequested,
			   char *elementSetName,
			   int preferredRecordSyntax,
			   char **errmsgp)
{
    static ODR odr = 0;
    Z_APDU *apdu;
    Z_PresentRequest *req;
    Z_ReferenceId zr;
    Z_RecordComposition rcomp;
    Z_ElementSetNames esname;

    if (!prepare_odr(&odr, errmsgp))
	return nodata((char*) 0);
    apdu = zget_APDU(odr, Z_APDU_presentRequest);
    req = apdu->u.presentRequest;

    req->referenceId = make_ref_id(&zr, referenceId);
    if (strcmp (resultSetId, "0") != 0)
	req->resultSetId = resultSetId;
    *req->resultSetStartPoint = resultSetStartPoint;
    *req->numberOfRecordsRequested = numberOfRecordsRequested;
    req->num_ranges = 0;	/* ### would be nice to support this */
    req->recordComposition = &rcomp;
    rcomp.which = Z_RecordComp_simple;	/* ### espec suppport would be nice */
    rcomp.u.simple = &esname;
    esname.which = Z_ElementSetNames_generic;
    esname.u.generic = elementSetName;
    if ((req->preferredRecordSyntax =
	 record_syntax(odr, preferredRecordSyntax)) == 0)
	return nodata(*errmsgp = "can't convert record syntax");

    return encode_apdu(odr, apdu, errmsgp);
}


databuf makeDeleteRSRequest(databuf referenceId,
			    char *resultSetId,
			    char **errmsgp)
{
    static ODR odr = 0;
    Z_APDU *apdu;
    Z_DeleteResultSetRequest *req;
    Z_ReferenceId zr;
    Z_ResultSetId *rsList[1];
    int x;

    if (!prepare_odr(&odr, errmsgp))
	return nodata((char*) 0);
    apdu = zget_APDU(odr, Z_APDU_deleteResultSetRequest);
    req = apdu->u.deleteResultSetRequest;

    req->referenceId = make_ref_id(&zr, referenceId);
    req->deleteFunction = &x;
    x = Z_DeleteResultSetRequest_list;
    req->num_resultSetList = 1;
    req->resultSetList = &rsList[0];
    rsList[0] = resultSetId;

    return encode_apdu(odr, apdu, errmsgp);
}


/*
 * If refId is non-null, copy it into the provided buffer, and return
 * a pointer to it; otherwise, return a null pointer.  Either way, the
 * result is suitable to by plugged into an APDU structure.
 */
Z_ReferenceId *make_ref_id(Z_ReferenceId *buf, databuf refId)
{
    if (refId.data == 0)
	return 0;

    buf->buf = (unsigned char*) refId.data;
    buf->len = (int) refId.len;
    return buf;
}


static Odr_oid *record_syntax(ODR odr, int preferredRecordSyntax)
{
    oident prefsyn;
    int oidbuf[20];		/* more than enough */
    int *oid;

    prefsyn.proto = PROTO_Z3950;
    prefsyn.oclass = CLASS_RECSYN;
    prefsyn.value = (oid_value) preferredRecordSyntax;
    if ((oid = oid_ent_to_oid(&prefsyn, oidbuf)) == 0)
	return 0;

    return odr_oiddup(odr, oid);
}


/*
 * Memory management strategy: every APDU we're asked to allocate
 * obliterates the previous one by overwriting our static ODR buffer,
 * so the caller _must_ ensure that it copies or otherwise consumes
 * the return value before the next call is made.  (This strategy
 * would normally stink, but it's actually not error-prone in this
 * context, since we know that the Perl XS code is about to copy the
 * data onto its stack.)
 */
static databuf encode_apdu(ODR odr, Z_APDU *apdu, char **errmsgp)
{
    databuf res;
    int len;
    res.len = 0;		/* Not needed, but prevents compiler warning */
    res.data = 0;

    if (!z_APDU(odr, &apdu, 0, (char*) 0)) {
	*errmsgp = odr_errmsg(odr_geterror(odr));
	return res;
    }

    res.data = odr_getbuf(odr, &len, (int*) 0);
    res.len = len;
    return res;
}


static int prepare_odr(ODR *odrp, char **errmsgp)
{
    if (*odrp != 0) {
	odr_reset(*odrp);
    } else if ((*odrp = odr_createmem(ODR_ENCODE)) == 0) {
	*errmsgp = "can't create ODR stream";
	return 0;
    }

    return 1;
}


/*
 * Return a databuf with a null pointer (used as an error indicator)
 * (In passing, we also report to stderr what the problem was.)
 */
static databuf nodata(char *msg)
{
    databuf buf;

#ifndef NDEBUG
    if (msg != 0) {
	fprintf(stderr, "DEBUG nodata(): %s\n", msg);
    }
#endif
    buf.len = 0;		/* Not needed, but prevents compiler warning */
    buf.data = 0;
    return buf;
}


/*
 * Simple wrapper for cs_write() when that comes along.  Also calls
 * cs_look() to detect the completion of a connection when that comes
 * along.
 */
int yaz_write(COMSTACK cs, databuf buf)
{
    if (cs_look(cs) == CS_CONNECT) {
	if (cs_rcvconnect(cs) < 0) {
	    return -1;
	}
    }

    return write(cs_fileno(cs), buf.data, buf.len);
}