/* * InterviewController.java * * Created on March 4, 2010, 10:00 AM */ package net.sf.egonet.controller; import java.awt.Desktop; import java.net.URI; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import javax.swing.event.EventListenerList; import net.sf.egonet.model.Answer; import net.sf.egonet.model.Interview; import net.sf.egonet.model.Question; import net.sf.egonet.model.QuestionOption; import net.sf.egonet.model.Study; import net.sf.egonet.persistence.Answers; import net.sf.egonet.persistence.Interviewing; import net.sf.egonet.persistence.Interviews; import net.sf.egonet.persistence.Options; import net.sf.egonet.persistence.Questions; import net.sf.egonet.persistence.Studies; import net.sf.egonet.web.Main; import org.mortbay.jetty.Server; /** * InterviewController is a Singleton class that allows third party clients to * control the operation of the Egonet software. In this model, clients see * the Egonet software as a library with a set of methods it can call in order * to control Egonet's execution. This class is the provider of these methods. * * More specifically, while Egonet provides several different functions, * including study authoring, data analysis and data import and export, this * class focuses only on Egonet's interviewing capability. It allows client * software to control the way Egonet provides its interviewing functionality * to interviewers. Using InterviewController, client software directs Egonet * to start its server and to keep the server session alive while an arbitrary * number of studies are sequentially selected for execution. During the * execution of a study, an arbitrary number of interviews are sequentially * selected for execution. Once the client software has finished with all * studies and their interviews, it instructs Egonet to stop its server. This * concept is illustrated in the diagram below, with each indented portion of * the diagram indicating an inner loop that can be instructed by the client * software to execute an arbitrary number of times. * * startServer() * beginStudy() * beginInterview() * endInterview() * endStudy() * stopServer() * * InterviewController also provides a method for client software to retrieve * responses to any of the questions defined in the active study. In addition, * InterviewController will, if the client software requests it, notify the * client when it detects user activity and when an interview has been ended. * * All of these control functions are performed by the client software without * any user intervention. The interviewer is presented with the interview * starting page displayed in his/her browser for the interview and study * selected by the client software. When the interviewer is finished with the * interview, control is automatically transfered back to the client software. * When the interviewer has finished with the client software session, the * client software instructs Egonet to shut down. * * When InterviewController is not used, Egonet operates in its traditional * manner. * * @author Matt Futterman */ public class InterviewController implements EgonetMonitor { private static InterviewController interviewController; private static DBSessionFactoryManager dbSessionFactoryManager; private static EventListenerList listenerList; private static Server server; private static long dbStudyID; private static long dbInterviewID; private static String studyName; private static String clientCaseID; private static Question caseIDQuestion; private static Question completionStatusFlagQuestion; private static Question everTouchedStatusFlagQuestion; private static List<Question> questionList; private static Map<String,String> preloadMap; private boolean ensureIntrinsicFlagsExist; private boolean completionStatusFlagEnabled; private boolean everTouchedStatusFlagEnabled; private boolean preloadsHaveBeenLoaded; private enum AdminQuestions { caseID( 0 ), completed( 1 ), everTouched( 2 ); private final int position; private AdminQuestions( int position ) { this.position = position; } private int getPosition() { return position; } } /** * This constructor creates the Singleton instance of this class. */ private InterviewController() { dbSessionFactoryManager = DBSessionFactoryManager.getInstance(); listenerList = new EventListenerList(); Main.setUsingDBSessionFactoryManager( true ); Main.setHomePage( StartInterviewPage.class ); Main.registerEgonetMonitor( this ); server = Main.createAndConfigureServer(); } /** * This method returns the singleton instance of InterviewController. The * instance will be created the first time this method is called. */ public static InterviewController getInstance() { if (interviewController == null) { interviewController = new InterviewController(); } return interviewController; } /** * Returns whether or not the use of an instrinsic completion status flag * is enabled. This behavior is specified on a per-study basis. */ public boolean isCompletionStatusFlagEnabled( boolean enable ) { return completionStatusFlagEnabled; } /** * Returns whether or not the use of an instrinsic everTouched status flag * is enabled. This behavior is specified on a per-study basis. */ public boolean isEverTouchedStatusFlagEnabled( boolean enable ) { return everTouchedStatusFlagEnabled; } /** * Adds the given InterviewControllerListener to the InterviewController's * list of listeners. */ public void addInterviewControllerListener( InterviewControllerListener l ) { listenerList.add( InterviewControllerListener.class, l ); } /** * Removes the given InterviewControllerListener from the InterviewController's * list of listeners. */ public void removeInterviewControllerListener( InterviewControllerListener l ) { listenerList.remove( InterviewControllerListener.class, l ); } /** * Returns whether or not the Egonet server is running. */ public boolean isServerRunning() { return server.isRunning(); } /** * Returns whether or not an Egonet study is active. */ public boolean isStudyActive() { return dbStudyID != 0; } private void setStudyInactive() { dbStudyID = 0; studyName = null; questionList = null; } /** * Returns whether or not an Egonet interview is active. */ public boolean isInterviewActive() { return dbInterviewID != 0; } private void setInterviewInactive() { dbInterviewID = 0; } /** * Starts the Egonet server if it is not already running. * * Throws an IllegalStateException if the Egonet server is already running. * * @throws Exception */ public void startServer() throws Exception { if (server.isRunning()) { throw new IllegalStateException( "The Egonet server is already running." ); } server.start(); } /** * Stops the Egonet server if there is no active interview and no active * study. * * Throws an IllegalStateException if an interview is active or if a study * is active. * * @throws Exception */ public void stopServer() throws Exception { if (isInterviewActive()) { throw new IllegalStateException( "Cannot stop Egonet server. An interview is active." ); } if (isStudyActive()) { throw new IllegalStateException( "Cannot stop Egonet server. A study is active. Call endStudy()." ); } server.stop(); } /** * Begins an Egonet study named 'studyName' if there is no active interview * and no active study and the server is running. Uses 'dbConfigFilePath' * to retrieve the configuration file for the database associated with the * study. * * Throws an IllegalStateException if an interview or study is active or * if the server is not running. * * Throws an Exception if no Egonet study exists for 'studyName'. * * @param studyName name of an Egonet study * @param dbConfigFilePath configuration file for database associated with study * @throws Exception */ public void beginStudy( String studyName, String dbConfigFilePath ) throws Exception { beginStudy( studyName, dbConfigFilePath, false ); } /** * Begins an Egonet study named 'studyName' if there is no active interview * and no active study and the server is running. Uses 'dbConfigFilePath' * to retrieve the configuration file for the database associated with the * study. * * Throws an IllegalStateException if an interview or study is active or * if the server is not running. * * Throws an Exception if no Egonet study exists for 'studyName'. * * @param studyName name of an Egonet study * @param dbConfigFilePath configuration file for database associated with study * @param ensureIntrinsicFlagsExist whether or not the intrinsic flags must exist * @throws Exception */ public void beginStudy( String studyName, String dbConfigFilePath, boolean ensureIntrinsicFlagsExist ) throws Exception { if (isInterviewActive()) { throw new IllegalStateException( "An interview is active. Call endInterview()." ); } if (isStudyActive()) { throw new IllegalStateException( "A study is already active. Call endStudy()." ); } if (!server.isRunning()) { throw new IllegalStateException( "The Egonet server is not running. Call startServer()." ); } this.ensureIntrinsicFlagsExist = ensureIntrinsicFlagsExist; dbSessionFactoryManager.useSessionFactory( studyName, dbConfigFilePath ); Study study = null; List<Study> studyList = Studies.getStudies(); Iterator<Study> studies = studyList.iterator(); while( studies.hasNext() ) { Study thisStudy = studies.next(); if (thisStudy.getName().equals( studyName )) { study = thisStudy; dbStudyID = thisStudy.getId(); break; } } if (study == null) { stopServer(); throw new Exception( "Unable to find study '" + studyName + "'." ); } questionList = Questions.getQuestionsForStudy( dbStudyID, null ); this.studyName = studyName; } /** * Ends (makes inactive) the currently active Egonet study. * * Throws an IllegalStateException if an interview is active or if no study * is active. * * @throws Exception */ public void endStudy() throws Exception { if (isInterviewActive()) { throw new IllegalStateException( "An interview is active. Call endInterview()." ); } if (isStudyActive()) { setStudyInactive(); } else { throw new IllegalStateException( "No study is currently active. Call beginStudy()." ); } } /** * Begins an Egonet interview that is, or will be, associated with the given * client's case ID. The client's case ID is stored as the interview's * response for the question denoted as being of type 'EGO_ID'. An * interview is determined to be already in existance if the study database * contains an interview for which that interview's EGO_ID question * response matches 'clientCaseID'. Otherwise, a new interview is created * with its EGO_ID question response containing the value of 'clientCaseID'. * * Implementation note: This method sets the state for a subsequent call to * method getInterviewID(), which is called from the StartInterviewPage * class at the point where the interview's HTML entry point is launched in * the user's browser. Specifically, if this method finds an existing * interview for 'clientCaseID', class variable 'dbInterviewID' will * have a non-zero value, otherwise it will be zero, indicating that a new * interview must be created. See method getInterviewID() in this class. * * Throws an IllegalStateException if an interview is already active, if no * study is active, or if the server is not running. * * @param clientCaseID client case ID (to be) associated with Egonet interview * * @throws Exception */ public void beginInterview( String clientCaseID ) throws Exception { beginInterview( clientCaseID, null ); } /** * Begins an Egonet interview that is, or will be, associated with the given * client's case ID. The client's case ID is stored as the interview's * response for the question denoted as being of type 'EGO_ID'. An * interview is determined to be already in existance if the study database * contains an interview for which that interview's EGO_ID question * response matches 'clientCaseID'. Otherwise, a new interview is created * with its EGO_ID question response containing the value of 'clientCaseID'. * * Implementation note: This method sets the state for a subsequent call to * method getInterviewID(), which is called from the StartInterviewPage * class at the point where the interview's HTML entry point is launched in * the user's browser. Specifically, if this method finds an existing * interview for 'clientCaseID', class variable 'dbInterviewID' will * have a non-zero value, otherwise it will be zero, indicating that a new * interview must be created. See method getInterviewID() in this class. * * Throws an IllegalStateException if an interview is already active, if no * study is active, or if the server is not running. * * @param clientCaseID client case ID (to be) associated with Egonet interview * @param preloadMap a Map with keys that are Egoweb question names and values * that are responses for the associated questions * * @throws Exception */ public void beginInterview( String clientCaseID, Map<String,String> preloadMap ) throws Exception { if (isInterviewActive()) { throw new IllegalStateException( "An interview is already active. Call endInterview()." ); } if (!isStudyActive()) { throw new IllegalStateException( "No study is currently active. Call beginStudy()." ); } if (!server.isRunning()) { throw new IllegalStateException( "The Egonet server is not running. Call startServer()." ); } this.clientCaseID = clientCaseID; this.preloadMap = preloadMap; preloadsHaveBeenLoaded = false; loadAdminQuestions(); long questionID = caseIDQuestion.getId(); List<Answer> answerList = Answers.getAnswersForQuestion( new Long( questionID ) ); Iterator<Answer> answers = answerList.iterator(); while ( answers.hasNext() ) { Answer thisAnswer = answers.next(); String value = thisAnswer.getValue(); if (value != null) { if (value.equals( clientCaseID )) { // The interview already exists. dbInterviewID = thisAnswer.getInterviewId(); break; } } } Desktop.getDesktop().browse( new URI( "http://127.0.0.1:8080" ) ); } private void loadPreloadQuestions( Map<String,String> preloadMap ) throws Exception { StringBuffer errorBuf = new StringBuffer(); Set<Map.Entry<String,String>> entrySet = preloadMap.entrySet(); for ( Map.Entry<String,String> entry : entrySet ) { String varName = entry.getKey(); String value = entry.getValue(); Question question = Questions.getQuestionUsingTitleAndTypeAndStudy( varName, Question.QuestionType.EGO_ID, dbStudyID ); if (question == null) { String s = "Question '" + varName + "' not found in Egoweb database."; errorBuf.append( "\n" ); errorBuf.append( s ); } else { try { Answers.setAnswerForInterviewAndQuestion( dbInterviewID, question, value, "", Answer.SkipReason.NONE ); } catch ( Exception e ) { String s = "Could not store response for question '" + varName + "' in Egoweb database."; errorBuf.append( "\n" ); errorBuf.append( s ); } } } if (errorBuf.length() != 0) { throw new RuntimeException( errorBuf.toString() ); } } private void loadAdminQuestions() throws Exception { List<Question> localQuestionList = Questions.getQuestionsForStudy( dbStudyID, Question.QuestionType.EGO_ID ); try { caseIDQuestion = localQuestionList.get( AdminQuestions.caseID.getPosition() ); } catch( Exception e ) { String s = "Egoweb 'case ID' question not found."; throw new RuntimeException( s, e ); } StringBuffer errorBuf = new StringBuffer(); try { completionStatusFlagQuestion = localQuestionList.get( AdminQuestions.completed.getPosition() ); completionStatusFlagEnabled = true; } catch( Exception e ) { if (ensureIntrinsicFlagsExist) { String s = "Egoweb 'completion' status flag question not found."; errorBuf.append( "\n" ); errorBuf.append( s ); } } try { everTouchedStatusFlagQuestion = localQuestionList.get( AdminQuestions.everTouched.getPosition() ); everTouchedStatusFlagEnabled = true; } catch( Exception e ) { if (ensureIntrinsicFlagsExist) { String s = "Egoweb 'everTouched' status flag question not found."; errorBuf.append( "\n" ); errorBuf.append( s ); } } if (errorBuf.length() != 0) { throw new RuntimeException( errorBuf.toString() ); } } /** * Ends (makes inactive) the currently active Egonet interview. * Note that in most cases this method will be called internally when * Egonet detects that the user has completed an interview. However it * may also be called by the client when (for instance) a period of * inactivity has elapsed or there is some other reason that requires that * the interview be terminated programmatically. * * Throws an IllegalStateException if no study or interview is active. * * @throws Exception */ public void endInterview() throws Exception { if (!isStudyActive()) { throw new IllegalStateException( "No study is currently active. Call beginStudy()." ); } if (!isInterviewActive()) { throw new IllegalStateException( "No interview is currently active. Call beginInterview()." ); } endInterview_work(); } private void endInterview_work() { String caseID = clientCaseID; clientCaseID = null; fireInterviewHasEnded( studyName, caseID ); setInterviewInactive(); } /** * This method is called internally by Egonet (specifically, from class * StartInterviewPage) at the time where an interview's first page must be * displayed in the user's browser. The value of class variable * 'dbInterviewID' is used either to retrieve an existing interview (see the * comments for method beginInterview() in this class), or to * contain the database interview ID for a newly-created interview. This * method returns the database ID for the interview in either case. * * @return the database ID for the active interview */ long getInterviewID() { Interview interview = null; try { interview = Interviews.getInterview( dbInterviewID ); } catch ( RuntimeException e ) { } if (interview == null) { ArrayList<Answer> answers = new ArrayList<Answer>(); interview = Interviewing.findOrCreateMatchingInterviewForStudy( dbStudyID, answers ); dbInterviewID = interview.getId(); Answers.setAnswerForInterviewAndQuestion( dbInterviewID, caseIDQuestion, clientCaseID, "", Answer.SkipReason.NONE ); } return dbInterviewID; } /** * This method returns a response for 'questionName' (i.e. an Egonet * question "title") for the currently active interview. If there is no * response for 'questionName' an empty String is returned. * * Throws an IllegalStateException if there is no active study. * Throws an Exception if 'questionName' is not a recognized question title * in the active Egonet study. * * @param questionName Egonet question title * @return response for 'questionName' or empty String if no response exists * @throws Exception */ public String getQuestionResponse( String questionName ) throws Exception { if (questionList == null) { throw new IllegalStateException( "Question list has not been populated. Call beginStudy()." ); } Question question = null; Iterator<Question> questions = questionList.iterator(); while ( questions.hasNext() ) { Question thisQuestion = questions.next(); if (thisQuestion.getTitle().equals( questionName )) { question = thisQuestion; break; } } if (question == null) { throw new Exception( "Question named '" + questionName + "' not " + "found in study '" + studyName + "'." ); } String response = ""; Answer answer = Answers.getAnswerForInterviewAndQuestion( dbInterviewID, question ); if (answer != null) // CASE-3225 { response = answer.getValue(); if (question.getAnswerType() == Answer.AnswerType.SELECTION || question.getAnswerType() == Answer.AnswerType.MULTIPLE_SELECTION) // CASE-3215 { String selectionResponse = null; // CASE-3243 long answerID = answer.getId().longValue(); List<QuestionOption> optionList = Options.getOptionsForQuestion( question.getId() ); for ( QuestionOption option : optionList ) { long optionID = option.getId().longValue(); String optionIDString = new Long( optionID ).toString(); // CASE-3243 // CASE-3243 if (optionID == answerID) if (optionIDString.equals( response )) // CASE-3243 { selectionResponse = option.getValue(); break; } } if (selectionResponse == null) { String s = "No selection value found for answer to question '" + question.getTitle() + "'."; selectionResponse = ""; // CASE-3238 } response = selectionResponse; } } return response; } private void fireInterviewActivityOccurred( String studyName, String caseID ) { InterviewControllerEvent e = new InterviewControllerEvent( this, studyName, caseID ); // Guaranteed to return a non-null array. Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying those that are // interested in this event. for (int i = listeners.length-2; i >= 0; i -= 2) { if (listeners[i] == InterviewControllerListener.class) { if (listeners[i+1] instanceof InterviewControllerListener) { InterviewControllerListener l; l = (InterviewControllerListener)listeners[i+1]; l.interviewActivityOccurred( e ); } } } } private void fireInterviewHasEnded( String studyName, String caseID ) { InterviewControllerEvent e = new InterviewControllerEvent( this, studyName, caseID ); // Guaranteed to return a non-null array. Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying those that are // interested in this event. for (int i = listeners.length-2; i >= 0; i -= 2) { if (listeners[i] == InterviewControllerListener.class) { if (listeners[i+1] instanceof InterviewControllerListener) { InterviewControllerListener l; l = (InterviewControllerListener)listeners[i+1]; l.interviewHasEnded( e ); } } } } /** * Indicates that a user has performed an Egonet action. */ public void userActivityOccurred() { if (everTouchedStatusFlagEnabled) { try { setIntrinsicFlag( everTouchedStatusFlagQuestion ); // CASE-3219 if (!preloadsHaveBeenLoaded && preloadMap != null) // CASE-3228 { try { loadPreloadQuestions( preloadMap ); // CASE-3220 } catch( Exception e ) { throw new RuntimeException( e ); } preloadsHaveBeenLoaded = true; } } catch( RuntimeException e ) { e.toString(); // An exception occurs the very first time an interview is touched } } if (isInterviewActive()) { fireInterviewActivityOccurred( studyName, clientCaseID ); } } /** * Indicates that an interviewer has finished an interview. */ public void interviewHasEnded() { if (completionStatusFlagEnabled) { setIntrinsicFlag( completionStatusFlagQuestion ); // CASE-3219 } endInterview_work(); } private void setIntrinsicFlag( Question question ) // CASE-3219 { Answers.setAnswerForInterviewAndQuestion( dbInterviewID, question, "1", "", Answer.SkipReason.NONE ); } // --------------------------------- main ----------------------------------- public static void main( String[] args ) throws Exception { String testStudyName = "EgoWebSample"; // String dbConfigFilePath = "C:/Studies/cases6.0/server1/ChrisTest1/_vn_/Egonet/_client_/hibernate.cfg.xml"; String dbConfigFilePath = "C:/Studies/cases6.0/server1/EgoWebSample/_vn_/Egonet/_server_/hibernate.cfg.xml"; InterviewController controller = InterviewController.getInstance(); controller.startServer(); controller.beginStudy( testStudyName, dbConfigFilePath, false ); controller.beginInterview( "007", getTestPreloadsMap() ); // String response = controller.getQuestionResponse( "ego_ques_1" ); // response.toString(); // controller.endInterview(); // controller.stopServer(); } private static Map<String,String> getTestPreloadsMap() { Map<String,String> preloads = new HashMap<String,String>(); preloads.put( "site", "1" ); preloads.put( "touched", "1" ); return preloads; } }