/* ======================================================================
The Bodington System Software License, Version 1.0
  
Copyright (c) 2001 The University of Leeds.  All rights reserved.
  
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1.  Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.

3.  The end-user documentation included with the redistribution, if any,
must include the following acknowledgement:  "This product includes
software developed by the University of Leeds
(http://www.bodington.org/)."  Alternately, this acknowledgement may
appear in the software itself, if and wherever such third-party
acknowledgements normally appear.

4.  The names "Bodington", "Nathan Bodington", "Bodington System",
"Bodington Open Source Project", and "The University of Leeds" must not be
used to endorse or promote products derived from this software without
prior written permission. For written permission, please contact
d.gardner@leeds.ac.uk.

5.  The name "Bodington" may not appear in the name of products derived
from this software without prior written permission of the University of
Leeds.

THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO,  TITLE,  THE IMPLIED WARRANTIES 
OF QUALITY  AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO 
EVENT SHALL THE UNIVERSITY OF LEEDS OR ITS CONTRIBUTORS BE LIABLE FOR 
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
POSSIBILITY OF SUCH DAMAGE.
=========================================================

This software was originally created by the University of Leeds and may contain voluntary 
contributions from others.  For more information on the Bodington Open Source Project, please 
see http://bodington.org/

====================================================================== */

package org.bodington.assessment;

import org.apache.log4j.Logger;

import org.bodington.servlet.*;
import org.bodington.servlet.facilities.Facility;

import java.io.*;
import java.sql.*;
import java.util.*;
import java.rmi.RemoteException;
import javax.servlet.*;
import javax.servlet.http.*;

import org.bodington.database.PrimaryKey;
import org.bodington.server.*;
import org.bodington.server.realm.Permission;
import org.bodington.server.realm.User;
import org.bodington.server.resources.Resource;
import org.bodington.server.events.Event;
import org.bodington.server.events.OpinionEvent;
import org.bodington.util.CSVWriter;
import org.bodington.assessment.*;

/**
 * Class that encapsulates the web-based user interface to a questionnaire. The
 * session type associated with this class is
 * {@link org.bodington.assessment.QuestionnaireSession}. The type of resource
 * associated with this class is {@link org.bodington.assessment.Questionnaire}.
 * @see org.bodington.assessment.QuestionnaireSession
 * @see org.bodington.assessment.Questionnaire
 * @author Jon Maber
 * @author Alexis O'Connor
 */
public class QuestionnaireFacility extends Facility
	{
    /**
     * Interface for objects that render questionnaires. 
     * @author Alexis O'Connor
     */
    interface QuestionnaireRenderer
        {
        /**
         * Render the associated questionnaire paper in HTML.
         * @param request the request object.
         * @param out the object to write to.
         * @param session the questionnaire session.
         */
        void paperToHTML( Request request, PrintWriter out,
            QuestionnaireSession session ) throws IOException,
            BuildingServerException;

        /**
         * Render the specified question in HTML.
         * @param question the questionnaire question.
         * @param response a user's response to the questionnaire question.
         * @param out the object to write to.
         */
        void questionToHTML( QuestionnaireQuestion question,
            QuestionnaireResponse response, PrintWriter out ) throws IOException,
            BuildingServerException;
        }
    
    /**
     * Utility class that contains wizard-oriented functionality pertaining to
     * {@link Questionnaire}. This class is intended to simplify the details of
     * possible property settings on {@link Questionnaire} into discrete
     * combinations that can be understood by an end-user. This class is
     * tightly-bound to {@link QuestionnaireFacility} so that it is why this
     * class is an inner class of that class as opposed to {@link Questionnaire}
     * itself.
     * @author Alexis O'Connor
     * @see Questionnaire
     */
    static class QuestionnaireWizard
        {
        static final String CLASSIC = "CLASSIC";
        static final String ONE_ATTEMPT = "ONE_ATTEMPT";
        static final String SURVEY = "SURVEY";
        static final String UNKNOWN = "UNKNOWN";

        /**
         * Get the wizard value corresponding to the specified
         * {@link javax.servlet.ServletRequest} parameters. This method provides a way
         * for converting from a set of zero to many parameters understood by
         * the enclosing class, to a discrete value. The value returned will be
         * equal to one of the constants in this class.
         * @param map an object containing {@link javax.servlet.ServletRequest}
         *        parameters.
         * @return a value corresponding to one of the constants in this class.
         */
        static String wizardValue( Map map )
            {
            Set keys = map.keySet();

            if ( keys.isEmpty() )
                {
                return CLASSIC;
                }
            else if ( keys.size() == 1 && keys.contains( SINGLE_SUBMIT ) )
                {
                return ONE_ATTEMPT;
                }
            else if ( keys.contains( HIDE_SUBMITTED )
                && keys.contains( SINGLE_SUBMIT )
                && keys.contains( MULTIPLE_RESULTS ) )
                {
                return SURVEY;
                }

            return UNKNOWN;
            }

        /**
         * Get the {@link javax.servlet.ServletRequest} parameters corresponding to the
         * specified wizard value.
         * @param value a value corresponding to one of the constants in this
         *        class.
         * @return an object containing {@link javax.servlet.ServletRequest}
         *         parameters.
         */
        static Map wizardParameters( String value )
            {
            HashMap map = new HashMap();

            if ( CLASSIC.equals( value ) )
                {
                }
            else if ( ONE_ATTEMPT.equals( value ) )
                {
                map.put( SINGLE_SUBMIT, EMPTY_AS_ARRAY);
                }
            else if ( SURVEY.equals( value ) )
                {
                map.put( SINGLE_SUBMIT, EMPTY_AS_ARRAY );
                map.put( HIDE_SUBMITTED, EMPTY_AS_ARRAY );
                map.put( MULTIPLE_RESULTS, EMPTY_AS_ARRAY );
                }
            
            return map;
            }
        }
    
    private static Logger log = Logger.getLogger(QuestionnaireFacility.class);
	
	static final int EVENT_START        =0;
	static final int EVENT_RECORD  =1;

    static final String EMPTY = "Empty";
    static final String[] EMPTY_AS_ARRAY = new String[] { EMPTY };
    /*
     * Parameter names:
     */
    static final String SINGLE_SUBMIT = "single_submit";
    static final String HIDE_SUBMITTED = "hide_submitted";
    static final String MULTIPLE_RESULTS = "multiple_results";

    public boolean canCopy( Resource resource )
    {
        return true;
    }
    
	public Resource newResource()
		{
		return new Questionnaire();
		}
	
	
	public boolean initResource( Request request, Resource resource )
        throws Exception
        {
        if ( !(resource instanceof Questionnaire) )
            {
            throw new Exception( "Technical problem: An incorrect type of "
                    + "resource was created." );
            }

        Questionnaire questionnaire = (Questionnaire)resource;
        /*
         * NOTE: all of these parameters are booleans, so we are only interested
         * in their presence, not their associated value.
         */
        HashSet set = new HashSet();
        set.addAll( request.getParameterMap().keySet() );
        String wizardValue = request.getParameter( "wizardValue" );
        set.addAll( QuestionnaireWizard.wizardParameters( wizardValue ).keySet() );
        questionnaire.setMultipleAttempts( !set.contains( SINGLE_SUBMIT ) );
        questionnaire.setHideAnswersOnSubmit( set.contains( HIDE_SUBMITTED ) );
        questionnaire.setMultipleResults( set.contains( MULTIPLE_RESULTS ) );
        return true;
        }

	public boolean initResource( Resource original_resource, Resource new_resource )
		throws BuildingServerException
		{
		 if ( !(new_resource instanceof Questionnaire) )
		   throw new BuildingServerException( "Error: The wrong type of resource was created." );

		 Questionnaire original = (Questionnaire)original_resource;
		 Questionnaire new_q = (Questionnaire)new_resource;

		 new_q.setQuestionnaireFlags(original.getQuestionnaireFlags());

		 return true;
	}


    /**
     * Copy the content of one resource to another. <p>
     * <h4>Content copied</h4>
     * <ul>
     * <li>authored questions.
     * <li>authored statements.
     * </ul>
     * <h4>Content <em>not</em> copied</h4>
     * <ul>
     * <li>user answers.
     * </ul>
     * </p>
     * @param original_resource the resource whose contents are to be copied.
     * @param new_resource the resource to copy content to.
     * @param breq the building request object.
     * @throws BuildingServerException if there is any problem copying the
     *         resource content.
     */
	public void copyContent( Resource original_resource, Resource new_resource, Request breq )
		 throws BuildingServerException
	        {
//	  authored : questions, statements
//	  userdata : answers? /** @todo  */

	         BuildingSession session = BuildingSessionManagerImpl.getSession( original_resource );
		 if ( !(session instanceof QuestionnaireSession) )
		   throw new BuildingServerException( "Unable to access appropriate resource session." );

		 try
		 {
		   if ( breq.getParameter( "authored" ) != null )
		   {
		     copyAuthoredContent( original_resource, new_resource );
//		     if ( breq.getParameter( "userdata" ) != null )
//		       copyUserContent( original_resource, new_resource );
		   }
		 }
		 catch (Exception ex)
		 {
		   throw new BuildingServerException( ex.getMessage() );
		 }
	}

    public void sendVirtualFile(Request req, HttpServletResponse res) throws ServletException, IOException
    {
        try
        {
            if ( req.getPageName().equals( "results.csv" ) )
            {
                if (req.getResource().checkPermission(Permission.REVIEW))
                {
                    BuildingSession session = BuildingSessionManagerImpl.getSession( req.getResource() );
                    if ( (session instanceof QuestionnaireSession) )
                    {
                        QuestionnaireSession q_session = (QuestionnaireSession)session;
                        exportcsv( req, res, q_session );
                    }
                }
                else
                {
                    res.sendError(HttpServletResponse.SC_FORBIDDEN, "You need review access");
                }
            }
            else
            {
                super.sendVirtualFile(req, res);
            }
        }
        catch (BuildingServerException bse)
        {
            log.error(bse);
            res.sendError(500);
        }
    }

    /**
     * Copy the authored content of an existing Resource to another Resource. <p>
     * <i>(WebLearn modification (method added): 02/12/2003 Colin Tatham)</i>
     * @param original_resource The resource with content to copy.
     * @param new_resource The resource to copy content to.
     * @exception BuildingServerException Thrown if there is any problem copying the resource content.
     * @exception RemoteException Thrown if there is any problem copying the resource content.
     *
     */
	private void copyAuthoredContent( Resource original_resource, Resource new_resource )
		throws BuildingServerException, RemoteException
		{
		 QuestionnaireSession session;
		 QuestionnaireQuestion orig_question, new_question;

		 session = (QuestionnaireSession)BuildingSessionManagerImpl.getSession( original_resource );
		 List questions = ((QuestionnaireSession)session).getQuestionnaireQuestionsInOrder();

		 for (int i=0; i<questions.size(); i++ )
		 {
		   orig_question = (QuestionnaireQuestion)questions.get( i );
		   new_question = new QuestionnaireQuestion();
		   new_question.setQuestionnaireId( new_resource.getResourceId() );
		   new_question.setStandardQuestion( orig_question.isStandardQuestion() );
		   new_question.setCanComment( orig_question.isCanComment() );
		   new_question.setIntroduction( orig_question.getIntroduction() );
		   new_question.setOrdinal( orig_question.getOrdinal() );
		   new_question.setFlags( orig_question.getFlags() );
		   for ( int n=0; n<orig_question.getStatementCount(); n++ )
		     new_question.setStatement( n, orig_question.getStatement(n) );

		   new_question.save();
		 }
		}


	public boolean createCheck( Request request, PrintWriter out )
        {
        try
            {
            /*
             * NOTE: all of these parameters are booleans, so we are only 
             * interested in their presence, not their associated value.
             */
            HashMap map = new HashMap();
            map.putAll( request.getParameterMap() );
            String wizardValue = request.getParameter( "wizardValue" );
            map.putAll( QuestionnaireWizard.wizardParameters( wizardValue ) );
            String[] messages = checkParameters( map );
            if ( messages.length == 0 )
                {
                return true;
                }
            else
                {
                out.println( "<ul>" );
                for ( int i = 0; i < messages.length; i++ )
                    out.println( "<li>" + messages[i] + "</li>" );
                out.println( "</ul>" );
                return false;
                }
            }
        catch ( IOException e )
            {
            }
        catch ( BuildingServerException e )
            {
            }
        return false;
        }
	
	public void insert( Request req, PrintWriter out, String command, String insertname )
		throws ServletException, IOException
		{
		log.debug( Thread.currentThread().getName() + " QuestionnaireFacility insert()" );

		try
			{
			BuildingSession session;
			QuestionnaireSession q_session;

		    session = BuildingSessionManagerImpl.getSession( req.getResource() );
		    if ( !(session instanceof QuestionnaireSession) )
		    	{
		    	out.println( "<HR>Technical problem: unable to access appropriate tool session.<HR>" );
		    	return;
		    	}
		    q_session = (QuestionnaireSession)session;

			if ( command.equalsIgnoreCase( "qpaper" ) ||
		    	command.equalsIgnoreCase( "qrecord" ) ||
		    	command.equalsIgnoreCase( "qitemedit" ) ||
		    	command.equalsIgnoreCase( "qeditqconfirm" ) ||
		    	command.equalsIgnoreCase( "qdeleteq" ) ||
		    	command.equalsIgnoreCase( "qnewq" ) ||
		    	command.equalsIgnoreCase( "exportimsqti" ) ||
		    	command.equalsIgnoreCase( "importimsqti" )	||
                command.equalsIgnoreCase( "exportcsv" ))
				{

				if ( out==null )
					return;

				if ( command.equalsIgnoreCase( "qpaper" ) )
					qprintpaper( req, out, q_session, insertname );

				if ( command.equalsIgnoreCase( "qrecord" ) )
					qrecord( req, out, q_session, insertname );

				if ( command.equalsIgnoreCase( "qitemedit" ) )
					qitemedit( req, out, q_session, insertname );

				if ( command.equalsIgnoreCase( "exportimsqti" ) )
					exportimsqti( req, out, q_session );
				
                if ( command.equalsIgnoreCase( "importimsqti" ) )
                    importimsqti( req, out, q_session );
                
				return;
				}
			}		
		catch ( BuildingServerException bsex )
			{
			logException( out, "QuestionnaireFacility", "insert", 
			    "Technical error processing page.",
			    bsex );
			return;
			}
		
		
		super.insert( req, out, command, insertname );
		}

	/**
     * Print out the questionnaire paper. This ouputs the contents of the
     * questionnaire to the <code>out</code> parameter. The rendering can be
     * altered via the <code>style</code> parameter, which corresponds to the
     * activity that the user is currently doing. The <code>style</code>
     * parameter understands the following values:
     * <ul>
     * <li><code>run</code> - a user is recording his or her responses.
     * <li><code>view</code> - a user is viewing the questions.
     * <li><code>edit</code> - a user is editing quesions within the
     * questionnaire.
     * <li><code>analysis</code> - a user is analysing recorded responses to
     * the questionnaire.
     * </ul>
     * @param req the request object.
     * @param out the writer object.
     * @param session the questionnaire session object.
     * @param style the style of the presentation required (see above).
     * @throws IOException
     * @throws BuildingServerException
     */
	private void qprintpaper( Request req, PrintWriter out,
        QuestionnaireSession session, String style ) throws IOException,
        BuildingServerException
		{
        /*
         * NOTES:
         * - the relationship between this method and it's renderers is in a
         * state of evolutionary flux! It's possible that the ...
         * [*] this method may instantiate oft-used variables on behalf of the
         * renders and pass them in the constructor. 
         * [*] the renderer interface may turn into an abstract class (in order
         * to capture more commonality explicitly).
         */
        
        /*
         * Instantiate some common variables.
         */
        Questionnaire questionnaire = session.getQuestionnaire();
        QuestionnaireResult result = session.getQuestionnaireResult();

        /*
         * Switch "state" if required.
         * 
         * (Essentially what we are doing here is effectively switching the
         * access control on an individual, depending on whether they have
         * submitted a paper or not. The reason we do not change the setting
         * in the acl table itself is because that works on groups. One
         * alternative might be to make each individual a single member of an
         * adhoc group. For example, if a questionnaire is 'one attempt only' we
         * could remove their RECORD permission once they had submitted an
         * answer).
         */
        
        // 1 attempt / 1 result allowed / 1 result recorded:
        if ( "run".equalsIgnoreCase( style ) 
            && !questionnaire.isMultipleAttempts() 
            && !questionnaire.isMultipleResults()
            && result != null )
            {
            style = "view";
            }

         /*
          * Hand off to appropriate renderer.
          */
        QuestionnaireRenderer renderer = null;
        if ( "run".equalsIgnoreCase( style ) )
            {
            renderer = new RunQuestionnaireRenderer();
            }
        else if ( "view".equalsIgnoreCase( style ) )
            {
            renderer = new ViewQuestionnaireRenderer();
            }
        else if ( "edit".equalsIgnoreCase( style ) )
            {
            renderer = new EditQuestionnaireRenderer();
            }
        else if ( "analysis".equalsIgnoreCase( style ) )
            {
            renderer = new AnalysisQuestionnaireRenderer();
            }
        renderer.paperToHTML(req, out, session);
		}

	private void qitemedit(  Request req, PrintWriter out, QuestionnaireSession q_session, String insertname )
		throws IOException, BuildingServerException
		{
		String id, field, statement;
		PrimaryKey qid;
		Map questions;
		QuestionnaireQuestion q;
		
		if ( !BuildingContext.getContext().checkPermission( Permission.EDIT ) )
			{
			out.println( "<PRE>You don't have permission to edit questions in this location of the building.</PRE>\n" );
			return;
			}


		//must have an id in the parameter part of the URL
		if ( req.getTemplateParameterCount() < 1 )
			{
			out.println( "<I>Can't display edit field.</I>" );
			return;
			}
		id=(String)req.getTemplateParameter( 0 );
		qid= new PrimaryKey( Integer.parseInt( id ) );

		
		questions = q_session.getQuestionnaireQuestions();
		q=(QuestionnaireQuestion)questions.get( qid );
		
		if ( q==null )
			{
			out.println( "<I>Can't display edit field.</I>" );
			return;
			}

		if ( insertname.equalsIgnoreCase( "ordinal" ) )
			{
			out.println( "<INPUT NAME=ordinal VALUE=\"" +q.getOrdinal() + "\">" );
			return;
			}
		if ( insertname.equalsIgnoreCase( "statements" ) )
			{
			out.println( "<INPUT NAME=statements VALUE=\"" +q.getStatementCount() + "\">" );
			return;
			}
		if ( insertname.equalsIgnoreCase( "introduction" ) )
			{
			out.print( "<SPAN CLASS=bs-textarea><TEXTAREA NAME=introduction COLS=45 ROWS=8>" );
			out.print( q.getIntroduction() );
			out.println( "</TEXTAREA></SPAN>" );
			return;
			}

		for ( int i=0; i<10; i++ )
			{
			field = "statement_" + (char)('a' + i );
			statement = q.getStatement( i );
			if ( insertname.equalsIgnoreCase( field ) )
				{
				out.print( "<SPAN CLASS=bs-textarea><TEXTAREA NAME=" );
				out.print( field );
				out.print( " COLS=30 ROWS=4>" );
				out.print( statement==null?"":statement );
				out.println( "</TEXTAREA></SPAN>" );
				return;
				}
			}

		if ( insertname.equalsIgnoreCase( "is_standard" ) )
			{
			out.print( "<INPUT TYPE=CHECKBOX NAME=is_standard VALUE=true ONCLICK=standardise() " );
			out.println( q.isStandardQuestion()?"CHECKED>":">" );
			return;
			}

		if ( insertname.equalsIgnoreCase( "can_comment" ) )
			{
			out.print( "<INPUT TYPE=CHECKBOX NAME=can_comment VALUE=true " );
			out.println( q.isCanComment()?"CHECKED>":">" );
			return;
			}
		}

	
	/**
     * Add a new question. Upon success, this method returns a number which is
     * the string form of the ID of the question that has just been added (see
     * {@link QuestionnaireQuestion#getQuestionnaireQuestionId()}). Upon
     * failure, the string returned is in fact an error message.
     * <p>
     * <i>NOTE: This method is intended to be called from templates.</i>
     * @param request the request object.
     * @return the ID of the newly created question, or an error message.
     * @see QuestionnaireQuestion#getQuestionnaireQuestionId()
     * @see #isNumber(String)
     */
    public String addQuestion(Request request)
        {
        // TODO: this clearly needs better factoring out!
        try
            {
            QuestionnaireSession session 
                = (QuestionnaireSession)BuildingSessionManagerImpl.getSession( 
                    request.getResource() );
            String ordinalText = request.getParameter( "ordinal");
            QuestionnaireQuestion q = ( ordinalText != null) 
                ? session.createQuestion( Integer.parseInt( ordinalText) )
                : session.createQuestion(); 
            return q.getQuestionnaireQuestionId().toString();
            }
        catch ( BuildingServerException e )
            {
            return e.getMessage();
            }
        catch ( RemoteException e )
            {
            return e.getMessage();
            }
        }
    
    /**
     * Edit a question. Upon success, this method returns a number which is the
     * string form of the ID of the question that has just been edited (see
     * {@link QuestionnaireQuestion#getQuestionnaireQuestionId()}). Upon
     * failure, the string returned is in fact an error message.
     * <p>
     * <i>NOTE: This method is intended to be called from templates.</i>
     * @param request the request object.
     * @return the ID of the newly created question, or an error message.
     * @see QuestionnaireQuestion#getQuestionnaireQuestionId()
     * @see #isNumber(String)
     */
    public String editQuestion( Request request )
        {
        String id, param;
        PrimaryKey questionID;
        Map questions;
        QuestionnaireQuestion q;
        QuestionnaireSession session;

        try
            {
            session 
                = (QuestionnaireSession)BuildingSessionManagerImpl.getSession( 
                    request.getResource() );
            }
        catch ( BuildingServerException e )
            {
            return e.getMessage();
            }

        if ( !BuildingContext.getContext().checkPermission( Permission.EDIT ) )
            return "You do not have permission to edit questions in this "
                + "location of the building.";

        // must have an id in the parameter part of the URL
        if ( request.getTemplateParameterCount() < 1 )
            return "Can not display edit field";

        id = (String)request.getTemplateParameter( 0 );
        questionID = new PrimaryKey( Integer.parseInt( id ) );
        try
            {
            questions = session.getQuestionnaireQuestions();
            q = (QuestionnaireQuestion)questions.get( questionID );

            if ( q == null ) 
                return "Can not display edit field";

            param = request.getParameter( "ordinal" );
            try
                {
                q.setOrdinal( Integer.parseInt( param ) );
                }
            catch ( NumberFormatException ex )
                {
                return "Invalid number in ordinal field.";
                }

            param = request.getParameter( "introduction" );
            if ( param == null ) param = "";
            session.changeQuestionText( q.getQuestionnaireQuestionId(), 
                q.getIntroductionBigStringId(), param );
            for ( int i = 0; i < 10; i++ )
                {
                param = request.getParameter( "statement_" + (char)('a' + i) );
                if ( param == null ) param = "";
                session.changeQuestionText( q.getQuestionnaireQuestionId(), 
                    q.getStatementBigStringId( i ), param );
                }

            param = request.getParameter( "is_standard" );
            q.setStandardQuestion( param != null && param.length() > 0 );

            param = request.getParameter( "can_comment" );
            q.setCanComment( param != null && param.length() > 0 );
            session.changeQuestion( q.getQuestionnaireQuestionId(), q );
            }
        catch ( RemoteException e )
            {
            return "There was a problem with saving the edited question.";
            }
        catch ( BuildingServerException e )
            {
            return "There was a problem with saving the edited question.";
            }

        return q.getQuestionnaireQuestionId().toString();
        }
    
    /**
     * Delete a question. Upon success, this method always returns a string that
     * can be parsed as an integer. Typically, this is the ID of a question that
     * is adjacent to the one that has just been deleted (see
     * {@link QuestionnaireQuestion#getQuestionnaireQuestionId()}). If there is
     * a previous question, that is returned. If there is no previous question,
     * but there is a following question, that is the one returned. If no other
     * question is found, i.e. the deleted question was the only one left, the
     * value <code>0</code> is returned. Upon failure, the string returned is
     * in fact an error message.
     * <p>
     * <i>NOTE: This method is intended to be called from templates.</i>
     * @param request the request object.
     * @return the ID of a suitable adjacent question, or an error message.
     * @see QuestionnaireQuestion#getQuestionnaireQuestionId()
     * @see #isNumber(String)
     */
    public String deleteQuestion( Request request )
        {
        String id, param, notparam;
        PrimaryKey qid;
        QuestionnaireSession session;

        if ( !BuildingContext.getContext().checkPermission( Permission.EDIT ) )
            return "You do not have permission to delete (edit) questions in "
                + "this location of the building.";

        try
            {
            session 
                = (QuestionnaireSession)BuildingSessionManagerImpl.getSession( 
                    request.getResource() );
            }
        catch ( BuildingServerException e )
            {
            return e.getMessage();
            }

        // must have an id in the parameter part of the URL
        if ( request.getTemplateParameterCount() < 1 )
            return "Can not delete question.";

        id = (String)request.getTemplateParameter( 0 );
        qid = new PrimaryKey( Integer.parseInt( id ) );

        param = request.getParameter( "confirm" );
        if ( param == null ) param = "";
        notparam = request.getParameter( "notconfirm" );
        if ( notparam == null ) notparam = "";
        if ( param.length() < 1 || notparam.length() > 0 )
            return "Deleting was not confirmed - question not deleted.";
        
        // Try and find the ID of an adjacent question to the deletee:
        String questionID = String.valueOf( 0 );
        try
            {
            // NOTE: this is a UI convenience and *not* mission-critical!
            List questions = session.getQuestionnaireQuestionIds();
            int index = questions.indexOf( qid );
            if ( (index - 1) > 0 ) // does 'previous' exist?
                questionID = 
                    ((PrimaryKey)questions.get( index - 1 )).toString();
            else if ( (index + 1) < questions.size() ) // does 'next' exist?
                questionID = 
                    ((PrimaryKey)questions.get( index + 1 )).toString();
            }
        catch ( RemoteException e )
            {
            }
        catch ( BuildingServerException e )
            {
            }

        // Actually remove the intended deletee:
        try
            {
            session.removeQuestion( qid );
            }
        catch ( RemoteException e )
            {
            return e.getMessage();
            }
        catch ( BuildingServerException e )
            {
            return e.getMessage();
            }
        
        return questionID;
        }
    
    /**
     * Indicates whether the string is a number. Specifically, this method
     * indicates whether the string is parsable as a <code>double</code>.
     * @param s the string to be parsed.
     * @return <code>true</code> if the string is a parsable number, otherwise
     *         <code>false</code>.
     */
    public boolean isNumber( String s )
        {
        try
            {
            Double.parseDouble( s );
            return true;
            }
        catch ( NumberFormatException e )
            {
            return false;
            }
        }

	private void qrecord(  Request req, PrintWriter out, QuestionnaireSession q_session, String insertname )
		throws IOException, BuildingServerException
		{
		char letter;
		int i, j;
		String param;
		List questions_in_order;
		PrimaryKey[] list;
		QuestionnaireQuestion qq;
		int items[];
		String comments[];
		User user = (User)BuildingContext.getContext().getUser();
		questions_in_order = q_session.getQuestionnaireQuestionsInOrder();
			
		items = new int[questions_in_order.size()];
		comments = new String[questions_in_order.size()];
		list = new PrimaryKey[questions_in_order.size()];
		
		for ( i=0; i<questions_in_order.size(); i++ )
			{
			qq=(QuestionnaireQuestion)questions_in_order.get( i );
			list[i] = qq.getQuestionnaireQuestionId();
		
			//if no selection this is indicated with item = 100
			items[i]=100;
			comments[i]=null;
			
			// is the question present?
			param=req.getParameter( "Q" + list[i] );
			if ( param==null || !param.equalsIgnoreCase( "true" ) )
				continue;
				

			param=req.getParameter( "QR" + list[i] );
			if ( param!=null && param.length()>0 )
				{
				for ( j=0; j<10; j++ )
					{
					letter=(char)('A'+j);
					if ( param.charAt( 0 )==letter )
						{
						items[i]=j;
						break;
						}
					}
				}

			comments[i]=req.getParameter( "QC" + list[i] );
			if ( comments[i]!=null )
				comments[i]=comments[i].trim();
			
			}
        
        boolean newResult = false;
        // Do we want this stored in a new result instance?
        Questionnaire q = q_session.getQuestionnaire();
        newResult = q.isMultipleResults() && !q.isMultipleAttempts();
			
		PrimaryKey result_id 
            = q_session.record( newResult, list, items, comments );
		
		out.println( "<P>Your responses have been recorded .</P>" );
			
        Event event = new OpinionEvent( 
        				OpinionEvent.EVENT_RECORD,
        				req.getResource().getResourceId(), 
        				user.getUserId(), 
        				new Integer( result_id.intValue() ) );
        event.save();
		}



	private void exportimsqti( Request req, PrintWriter out,
        QuestionnaireSession q_session ) throws IOException
        {
        try
            {
            String fileName = q_session.exportXML( true, true );
            out.println( "<P>The XML file has been created and is available "
                + "for download with the name <em>" + fileName + "</em>"
                + " <A HREF=\"" + fileName + "\">here</A>.</P>" );
            out.println( "<P>(Left click to view in browser, right click to " 
                + "save to disk.) " );
            }
        catch ( BuildingServerException bsex )
            {
            out.println( "<PRE>" );
            out.println( bsex.getMessage() );
            out.println( "</PRE>" );
            }

        return;
        }

	private void importimsqti(  Request req, PrintWriter out, QuestionnaireSession q_session )
		throws IOException
		{
		String file = req.getParameter( "file" );
		if ( file == null || file.length() == 0 )
		    {
		    out.println( "<P>Import failed: no file was supplied.</P>" );
		    return;
		    }

		try
		    {
		    q_session.importXML( file );
    		out.println( "DONE" );
		    }
		catch ( BuildingServerException bsex )
		    {
    		out.println( "<PRE>" );
    		out.println( bsex.getMessage() );
    		out.println( "</PRE>" );
		    }
		
		
		return;
		}
    
	private void exportcsv(Request req, HttpServletResponse resp,
        QuestionnaireSession q_session) throws IOException
    {
        resp.setContentType("text/comma-separated-values");
        resp.setHeader("Content-Disposition","attachment; filename=\"results.csv\"");
        
        PrintWriter out = resp.getWriter();
        try
        {
            Questionnaire paper = q_session.getQuestionnaire();
            List list = q_session.getQuestionnaireQuestionsInOrder();
            QuestionnaireQuestion[] questions = new QuestionnaireQuestion[list
                .size()];
            questions = (QuestionnaireQuestion[]) list.toArray(questions);

            CSVWriter csv = new CSVWriter(out);

            if (paper == null || questions.length == 0)
            {
                csv.write("The questionnaire contains no questions.");
                csv.writeln();
                return;
            }

            // 1st line: the questions themselves.
            for (int i = 0; i < questions.length; i++)
            {
                csv.write(questions[i].getIntroduction());
                if (questions[i].isCanComment())
                    csv.write("");
            }
            csv.writeln();

            // Subsequent line(s): one user response per line.
            Enumeration results = QuestionnaireResult
                .findQuestionnaireResults(q_session.getQuestionnaire()
                    .getQuestionnaireId());
            while (results.hasMoreElements())
            {
                QuestionnaireResult result = (QuestionnaireResult) results
                    .nextElement();
                Map responses = result.getQuestionnaireResponses();
                for (int i = 0; i < questions.length; i++)
                {
                    QuestionnaireResponse response = (QuestionnaireResponse) responses
                        .get(questions[i].getQuestionnaireQuestionId());
                    // MCQ section?
                    if (questions[i].isMultipleChoice())
                        csv.write(response != null ? response.itemToString()
                                        : "");
                    // Comment section?
                    if (questions[i].isCanComment())
                        csv.write(response != null ? response.commentToString()
                                        : "");
                }
                csv.writeln();
            }

        }
        catch (IOException e)
        {
            log.error(e.getMessage(), e);
        }
        catch (BuildingServerException bse)
        {
            log.error(bse);
        }
    }
    
    /**
     * Check the specified parameters. These parameters are consistent with the
     * form understood by {@link javax.servlet.ServletRequest}. This method
     * expects to find parameters which pertain to a proposed set of property
     * settings on a questionnaire instance. This method looks for incompatible
     * combinations of properties and is intended to be called prior to actually
     * setting these values. Success is indicated by the return of an empty
     * array. Otherwise, the element at each index represents a separate error
     * message.
     * @param params a map of key-value pairs.
     * @return an array of error messages.
     * @see #requestParameters(Questionnaire)
     * @see javax.servlet.ServletRequest
     */
    private String[] checkParameters( Map params ) 
        throws IOException, BuildingServerException
        {
        // TODO: capture these semantics in questionnaire itself.
        Vector messages = new Vector();
        Set keys = params.keySet();
        boolean multipleAttemptsAllowed = !keys.contains( SINGLE_SUBMIT );
        boolean hideAnswersOnSubmit = keys.contains( HIDE_SUBMITTED );
        boolean multipleResults  = keys.contains( MULTIPLE_RESULTS );
        
        if (multipleAttemptsAllowed && hideAnswersOnSubmit)
            {
            messages.add("A questionnaire that allows for multiple attempts, " 
                + "but where the answers are hidden from the user after "
                + "submission, are incompatible options.");
            }
        
        if (multipleAttemptsAllowed && multipleResults)
            {
            messages.add("A questionnaire that allows for multiple attempts and " 
                + "multiple results is not supported at the current time.");
            }
        
        return (String[]) messages.toArray(new String[messages.size()]);
        }
    
    /**
     * Get the request parameters for the specified questionnaire. These
     * parameters are consistent with the form understood by
     * {@link javax.servlet.ServletRequest}. Each parameter has a 1-1
     * relationship to a property of the questionnaire. The mapping for request
     * parameters to questionnaire properties can be seen in the table below.
     * <p>
     * <table cellspacing="0" border="1"> <thead>
     * <tr>
     * <th>Parameter</th>
     * <th>Type</th>
     * <th>Questionnaire Property</th>
     * </tr>
     * </thead> <tbody>
     * <tr>
     * <td><code>one_attempt</code></td>
     * <td><code>boolean</code></td>
     * <td>{@link Questionnaire#isMultipleAttempts()} <code>== false</code></td>
     * </tr>
     * <tr>
     * <td><code>hide_submitted</code></td>
     * <td><code>boolean</code></td>
     * <td>{@link Questionnaire#isHideAnswersOnSubmit()} <code>== true</code></td>
     * </tr>
     * <tr>
     * <td><code>one_attempt</code></td>
     * <td><code>boolean</code></td>
     * <td>{@link Questionnaire#isMultipleResults()} <code>== true</code></td>
     * </tr>
     * </tbody> </table>
     * @param questionnaire the questionnaire.
     * @return the request parameters for the specified questionnaire.
     * @see Questionnaire
     * @see javax.servlet.ServletRequest
     */
    private Map requestParameters( Questionnaire questionnaire )
        {
        HashMap map = new HashMap();

        if ( !questionnaire.isMultipleAttempts() )
            map.put( SINGLE_SUBMIT, EMPTY_AS_ARRAY );

        if ( questionnaire.isHideAnswersOnSubmit() )
            map.put( HIDE_SUBMITTED, EMPTY_AS_ARRAY );

        if ( questionnaire.isMultipleResults() )
            map.put( MULTIPLE_RESULTS, EMPTY_AS_ARRAY );

        return map;
        }
    
    /**
     * Create an HTML control for the implicit questionnaire. This method will
     * return a radio button with the name <code>wizardValue</code>. If the
     * wizard-oriented value of the implicit questionnaire corresponds with the
     * <code>value</code> parameter then this control will be selected (i.e.
     * <code>checked</code>).
     * <p>
     * <em>NOTE: this method is intended to be called from templates.</em>
     * @param request the current request.
     * @param value a constant corresponding to a value in
     *        {@link QuestionnaireWizard}.
     * @param enabled indicates whether or not this control should be enabled.
     * @return an HTML control for the implicit questionnaire.
     * @see QuestionnaireWizard
     */
    public String wizardControl( Request request, String value,
        boolean enabled )
        {
        String wizardValue = null;
        try
            {
            QuestionnaireSession session 
                = (QuestionnaireSession) BuildingSessionManagerImpl.getSession( 
                    request.getResource() );
            Map map = requestParameters( session.getQuestionnaire() );
            wizardValue = QuestionnaireWizard.wizardValue( map );
            }
        catch ( BuildingServerException e )
            {
            return "";
            }
        catch ( RemoteException e )
            {
            return "";
            }
        
        StringBuffer sb = new StringBuffer( "<input type=\"radio\" name=" );
        sb.append( "\"wizardValue\" value=\"" ).append( value ).append( "\"");
        
        // Requested control matches actual value?
        if ( wizardValue.equals( value ) )
            sb.append( " checked=\"checked\"" );
        
        // Should the control be disabled?
        if ( !enabled )
            sb.append( " disabled=\"disabled\"" );
        
        // Close it!
        sb.append( " />");
        
        return sb.toString();
        }
	}


