#!/bin/bash
#
# Copyright (C) 2012-2015 SUSE Linux GmbH
#
# Authors:
# Kilian Petsch <kpetsch@suse.de>
# Frank Sundermeyer <fsundermeyer at opensuse dot org>
#
# Wrapper script which lets you use either xsltproc or saxon6
# with the same command
#
# NOTE:
#
# Setting the catalog for the resolver
#
# xsltproc and saxon have different ways to determine which catalogs to use
# for resolving Identifiers, URI, etc
# * xsltproc: reads $XML_CATALOG_FILES
# * saxon: uses a JAVA resolver class which uses the catalogs= setting in the
#          system wide CatalogManager.properties file
#          (e.g. /etc/java/resolver/CatalogManager.properties)
#
# DAPS sets and exports $XML_CATALOG_FILES, so we will use these settings for
# this wrapper as well (for both, xsltproc and saxon). Catalogs added with
# --catalog will be added to the existing $XML_CATALOG_FILES
#
# If this script is called from the command-line, it will also use
# $XML_CATALOG_FILES. If not set (probably the case on systems with no
# xsltproc), saxon will use the defaults from CatalogManager.properties
#
# If using xsltproc and if no additional catalogs have been specified with
# --catalog, there is nothing to do, because XML_CATALOG_FILES is either set
# system wide or exported by DAPS
#
# Saxon9 support (see ticket #94)
#

ME="$(basename "$0")"
XSLTPROC="/usr/bin/xsltproc"
SAXON="/usr/bin/saxon"
# TODO: we may need to change this path with configure
DBXSLT=/usr/share/xml/docbook/stylesheet/nwalsh/current

unset DEBUG ERR_CODE OUTPUT PARAMETERS PARMS SAXON6_ARGS STRPARMS STYLESHEET
unset XINCLUDE XMLFILE XML_TMP_FILE XSLTPROC_ARGS

# list of stringparams
declare -a STRPARMS
# list of params
declare -a PARMS
# final list of stringparams/params
declare -a PARAMETERS

# Help function to preserve readability of the while loop
function usage {
    cat <<EOF_helptext
Usage: daps-xslt -o <OUTPUTFILE> -s <STYLESHEET> -f <XMLDOCUMENT> [OPTIONS] <XSLTPROCESSOR>
Wrapper script for DAPS to use either xsltproc or saxon as an XSLT processor.

Mandatory parameters:
 --stylesheet=STYLESHEET,     Path to the stylesheet to be used. Mandatory.

 --file=XMLDOCUMENT,          XML document to which the stylesheet will be
 -f XMLDOCUMENT               applied. Mandatory.

 --output=OUTPUTFILE,         Specify a file for output. If not used, output
 - o OUTPUTFILE               goes to STDOUT. Optional.


Optional parameters:
 --catalogs="CATALOGS"        Define a space-separated list of XML catalogs
                              for resolving identifiersr; to be used in addition
                              to the system wide default catalog.

 --debug                      Activate a debugging mode (verbose).

 --help, -h                   Print this help.

 --param="KEY=VALUE"          Pass a parameter KEY with the numeric value VALUE
                              to the stylesheet. The key-value pair must be
                              written as KEY=VALUE. Use --stringparam to pass
                              string values. You may pass up to 32 key/value
                              pairs by using this parameter multiple times. If
                              the same key/value pair is passed more than once,
                              the last one "wins".
                              Optional.

 --saxon6_args="LIST"         saxon6 specific options (see saxon6 --help).
                              "LIST" is a list of options specified in the
                              same way as when calling saxon6 directly.
                              If double-quotes need to be passed on, LIST needs
                              to be put in single quotes.
                              This parameter is ignored when using xsltproc.

 --stringparam='"KEY=VALUE"'  Pass a parameter KEY with the string value VALUE
                              to the stylesheet. The key-value pair must be
                              written as '"KEY=VALUE"' (mind the quoting!).
                              Use --param to pass numeric values.
                              You may pass multiple key/value pairs up to a
                              maximum of 32. If the same key/value pair is
                              passed more than once, the last one "wins".
                              Optional.

 --xsltproc_args="LIST"       xsltproc specific options (see man xsltptroc).
                              "LIST" is a list of options specified in the same
                              way as when calling xsltproc directly.
                              If double-quotes need to be passed on, LIST needs
                              to be put in single quotes.
                              This parameter is ignored when using saxon.

 --xinclude                   Process the input document using the XInclude
                              specification.


XSLT processor:
  Define the XSLT processor, xsltproc or saxon6 (mandatory) and
  processor-specific options (optional, see the respective man pages
  for details).
EOF_helptext
    exit
}

function cleanup {
    [[ -f $XML_TMP_FILE ]] && rm -f "$XML_TMP_FILE"
}

function exit_on_error {
    echo -e "ERROR: $1" >&2
    cleanup
    exit 1;
}

function run-saxon {
    local CATALOGS SAXON_ARGS
    # TODO fs 2012-11-24
    # check whether this works with other distributions
    # -dependencies in spec-file may need to be enhanced because of
    #  the additional jars

    # replace [:space:] with ";" but retain spaces in filenames
    CATALOGS="${XML_CATALOG_FILES// \//;/}"

    if [[ -z "$DEBUG" ]]; then
        # Recover silently from recoverable errors
        SAXON_ARGS="-w0 $SAXON_ARGS"
    fi

    # if the stylesheet location is a link, saxon needs -u
    if [[ urn: = "${STYLESHEET:0:4}" || http: = "${STYLESHEET:0:5}" || file: = "${STYLESHEET:0:5}" ]]; then
        SAXON_ARGS="$SAXON_ARGS -u"
    fi

   # Add the DocBook extensions to the CLASSPATH

    CLASSPATH="${DBXSLT}/extensions/saxon65.jar:$CLASSPATH"

    # A local CatalogManager.properties file is stored in
    # ${DAPSROOT}/etc/
    [[ -n $DAPSROOT ]] && CLASSPATH="${DAPSROOT}/etc/:$CLASSPATH"

    # Additional JAR files
    ADDITIONAL_JARS="xml-commons-resolver12.jar"

    # Add catalogs, if specified
    [[ -n "$CATALOGS" ]] && ADDITIONAL_FLAGS="$ADDITIONAL_FLAGS -Dxml.catalog.files=$CATALOGS"

    # enable XIncludes (by adding Xerces) if requested
    if [[ -n "$XINCLUDE" ]]; then
        ADDITIONAL_JARS="$ADDITIONAL_JARS xerces-j2.jar xerces-j2-xml-apis.jar"
        ADDITIONAL_FLAGS="$ADDITIONAL_FLAGS \
-Djavax.xml.parsers.DocumentBuilderFactory=org.apache.xerces.jaxp.DocumentBuilderFactoryImpl \
-Djavax.xml.parsers.SAXParserFactory=org.apache.xerces.jaxp.SAXParserFactoryImpl \
-Dorg.apache.xerces.xni.parser.XMLParserConfiguration=org.apache.xerces.parsers.XIncludeParserConfiguration"
    fi

    export ADDITIONAL_FLAGS ADDITIONAL_JARS CLASSPATH

    if [[ -n "$DEBUG" ]]; then
        cat <<EOF
---------------------------------------------------------"
Running $ME with the following parameters:
  CLASSPATH="$CLASSPATH"
  XML_CATALOG_FILES="$XML_CATALOG_FILES"
  CATALOGS="$CATALOGS"
  ADDITIONAL_JARS="$ADDITIONAL_JARS"
  ADDITIONAL_FLAGS="$ADDITIONAL_FLAGS"
  $SAXON $SAXON_ARGS $@"
---------------------------------------------------------
EOF
    fi
    eval "$SAXON $SAXON_ARGS $*"
}

process_params () {
    # Function to create a list of params and stringparams for xsltproc
    # This function ensure that we can pass a certain parameter twice, with
    # that last one specified taking precedence. This is needed to overwrite
    # stringparams already set in the makefiles.
    # Natively, xsltproc does not support specifying a parameter twice, so
    # something like xsltproc --param foo bar --param foo baz will result
    # in an error.
    # This function takes care that all key/value with the same key are
    # deleted, except the last one specified.
    #
    # This functions takes two parameters
    # * PREFIX which must be either "param" or "stringparam"
    # * a list of KEY=VALUE pairs (param(stringparam values passed to this
    #   script
    #

    local ELEM I KEY PREFIX RESULT SEEN STR_PAR VALUE
    local -a KEYLIST
    local -a PARAMLIST


    PREFIX=$1
    shift
    PARAMLIST=( "$@" )

    if [[ param != $PREFIX && stringparam != $PREFIX ]]; then
        exit_on_error "Error:\nFirst parameter when calling process_params must be \"param\" or \"stringparam\"!"
    fi

    # initialize KEYLIST with an empty value
    KEYLIST[0]=""

    for ((I=${#PARAMLIST[@]}-1; I>=0; I--)); do
        SEEN=0
        STR_PAR=${PARAMLIST[$I]}
        # get rid of the double quotes of stringparams
        # ${STR_PAR:1:-1} does not work with bash 3.2
        #
        STR_PAR=${STR_PAR#\"}
        STR_PAR=${STR_PAR%\"}
        # split into KEY/VALUE at first "="
        KEY=${STR_PAR%%=*} # delete "=VALUE" part
        if [[ stringparam = "$PREFIX" ]];then
            VALUE="\"${STR_PAR#*=}\"" # delete "KEY=" part
        else
            VALUE=${STR_PAR#*=}
        fi
        #
        # we need to stay Bash 3.x compatible, therefore no
        # associative arrays, but rather using a list

        for ELEM in "${KEYLIST[@]}"; do
            if [[ $KEY = "$ELEM" ]]; then
                SEEN=1
                break
            fi
        done
        if [[ 0 = "$SEEN" ]]; then
            KEYLIST+=("$KEY")
            RESULT="$RESULT --$PREFIX $KEY $VALUE"
        fi
    done
    echo "$RESULT"
}

piped_input () {
    XML_TMP_FILE=$(mktemp -q daps_xslt_input.XXXXXXXX) || exit_on_error "Could not save piped input into a temporary file"
    cat > "$XML_TMP_FILE"
    XMLFILE="$XML_TMP_FILE"
    [[ -s $XMLFILE ]] || exit_on_error "Did not get XML data via pipe"
}

#----------------------------------------------------------------------------
# MAIN

# enable interruption/abortion via Ctrl-Z/Ctrl-C
trap "exit_on_error '\nCaught SIGTERM/SIGINT'" SIGTERM SIGINT
trap "cleanup" ERR EXIT

[[ -z "$1" ]] && usage

export POSIXLY_CORRECT=1
ARGS=$(getopt -o "f:hno:s:v" -l "catalogs:,debug,file:,help,nonet,novalid,output:,param:,saxon6_args:,stringparam:,stylesheet:,xinclude,xsltproc_args:" -n "$ME" -- "$@")

eval set -- "$ARGS"

while true; do
    case "$1" in
        --catalogs)
            XML_CATALOG_FILES="$2 $XML_CATALOG_FILES";
            export XML_CATALOG_FILES
            shift 2;;
        --debug)
            DEBUG=1
            export VERBOSE=1
            shift;;
        -f|--file)
            XMLFILE="$2"
            shift 2;;
        -h|--help)
            usage
            exit 0;;
        -o|--output)
            OUTPUT="-o $2"
            shift 2;;
        --param)
            # Test for equal sign
            if [[ $2 =~ = ]]; then
                PARMS+=("\"$2\"")
            else
                exit_on_error "--param requires \"KEY=VALUE\" as parameter"
            fi
            shift 2;;
        -s|--stylesheet)
            STYLESHEET="$2"
            shift 2
            ;;
        --saxon6_args)
            SAXON6_ARGS="$2"
            shift 2
            ;;
        --stringparam)
            # check for equal sign
            if [[ $2 =~ = ]]; then
                STRPARMS+=("\"$2\"")
            fi
            shift 2;;
        -x|--xinclude)
            XINCLUDE="--xinclude"
            shift;;
        --xsltproc_args)
            XSLTPROC_ARGS="$2"
            shift 2
            ;;
        --)
            shift
            break;;
        *) exit_on_error "Internal error!" ;;
    esac
done

unset POSIXLY_CORRECT

#-----
# Getting the xsltprocessor
#
if [[ $1 =~ xsltproc || $1 =~ saxon ]]; then
    PROCESSOR=$1
    shift;
else
    exit_on_error "Invalid XSLT processor: must be either xsltproc or saxon"
    exit 1;
fi

# ???
#PROCESSOR_ARGS="$*"

#-----
# Check if stylesheet has been specified
#
[[ -z "$STYLESHEET" ]] && exit_on_error "you must specify a stylesheet with --stylesheet <STYLESHEET>"

#----
# Get input from pipe (in case a pipe exists)
#
# Not needed ATM since we always use --file in DAPS
# commenting the code in case it is needed in the future
# BEWARE:
# This code covers the following cases:
# * cat foo | daps_xslt
# * daps_xslt --file
# * daps_xslt < foo
# Input coming from a pseudo-Terminal (ok, this is a rare cornercase) is
# not supported (cat foo > /dev/pts1 && daps_xslt < /dev/pts/1 )

if [[ -z "$XMLFILE" ]]; then
    if [[ -p /dev/stdin ]]; then
        # pipe
        piped_input
    elif [[ ! -t 0 && ! -p /dev/stdin ]]; then
        # redirected with < foo
        piped_input
    else
        exit_on_error "You must either specify an XML file with --file or pipe it into $ME"
    fi
fi

#-----
# run the XSLT processor
#
if [[ $PROCESSOR =~ xsltproc ]]; then
    # Process stringparam and param values for xsltproc
    if [[ -n "${STRPARMS[@]}" ]]; then
        PARAMETERS=("$(process_params "stringparam" "${STRPARMS[@]}")") || exit_on_error "wrong parameter for function process_params, must be either \"param\" or \"stringparam\""
    fi
    if [[ -n "${PARMS[@]}" ]]; then
        PARAMETERS+=("$(process_params "param" "${PARMS[@]}")") || exit_on_error "wrong parameter  for function process_params, must be either \"param\" or \"stringparam\""
    fi

    COMMAND="$XSLTPROC $XSLTPROC_ARGS $XINCLUDE ${PARAMETERS[*]} $OUTPUT $STYLESHEET $XMLFILE"

    if [[ -n "$DEBUG" ]]; then
        echo "---------------------------------------------------------"
        echo "Running the following command:"
        echo -e "$COMMAND"
        echo "---------------------------------------------------------"
    fi
    eval "$COMMAND"
    # capture xsltproc return value
    ERR_CODE=$?
else
    run-saxon "$SAXON6_ARGS $OUTPUT $XMLFILE $STYLESHEET" "${PARMS[@]}" "${STRPARMS[@]}"
    # capture saxon6 return value
    ERR_CODE=$?
fi


#-----
# Cleanup
# done via trap, see above

exit $ERR_CODE;
