package net.sf.egonet.model; import java.util.ArrayList; import java.util.regex.Matcher; import net.sf.egonet.model.Answer.AnswerType; import net.sf.egonet.persistence.TextInsertionUtil; import net.sf.egonet.web.panel.NumericLimitsPanel.NumericLimitType; public class Question extends OrderedEntity { // July 2010 - D. Kennedy requested the option for multiple questions // to appear on one page, but did not want it actually incorporated // this variable will control whether the option is available public static final boolean ALLOW_MULTIPLE_QUESTIONS_PER_PAGE = false; public static enum QuestionType { EGO_ID, EGO, ALTER, ALTER_PAIR, NETWORK }; private Long studyId; private String title; // Gender protected String citation; // This question originally written by Dr. X from the Institute of Advanced Research. protected String citationOld; protected String prompt; // Are you male or female? protected String promptOld; protected String preface; // The following questions relate to... protected String prefaceOld; private AnswerType answerType; private QuestionType type; private Boolean askingStyleList; private Long answerReasonExpressionId; // Answer the question if this expression is null or true private Long networkRelationshipExprId; private Long networkNShapeQId; private Long networkNColorQId; private Long networkNSizeQId; private Long networkEColorQId; private Long networkESizeQId; // an 'additional logic' expression in string form // to be evaluated if answerReasonExpressionId is null private String useIfExpression; // variables that deal with Selection / Multiple Selection questions // using the 'other-specify' option private Boolean otherSpecify; // if true, selection of 'Other' brings up text box // each question has SIX optional buttons associated with it // four of these are only active if the question is asked in a // list format, two always apply private Boolean noneButton; // list format only private Boolean allButton; // list format only private Boolean pageLevelDontKnowButton; // list format only private Boolean pageLevelRefuseButton; // list format only private Boolean dontKnowButton; // single question format only private Boolean refuseButton; // single question format only private String allOptionString; // if All button is clicked, use this // variables that will be specific to numeric answers, // and (optionally) limiting them to a range private NumericLimitType minLimitType; private Integer minLiteral; private String minPrevQues; private NumericLimitType maxLimitType; private Integer maxLiteral; private String maxPrevQues; // similar variables to be used with MULTIPLE_SELECTION questions private Integer minCheckableBoxes; private Integer maxCheckableBoxes; // these variables are (optionally) used with // list-of-alters format, when we want a limited number // of one item selected private Boolean withListRange; private String listRangeString; private Integer minListRange; private Integer maxListRange; // Date & Time_Span questions will let authors select which // time units are available for answering. // they will be stored as boolean bit values private Integer timeUnits; // symmetric indicates if a question should be asked a->b and b->a or just a->b private Boolean symmetric; // groupID will be used to group questions together on one page private Boolean keepOnSamePage; public Question() { prompt = ""; answerType = AnswerType.TEXTUAL; type = QuestionType.EGO; askingStyleList = new Boolean(false); otherSpecify = new Boolean(false); noneButton = new Boolean(false); allButton = new Boolean(false); pageLevelDontKnowButton = new Boolean(true); pageLevelRefuseButton = new Boolean(true); dontKnowButton = new Boolean(true); refuseButton = new Boolean(true); allOptionString = ""; useIfExpression = ""; minLimitType = NumericLimitType.NLT_NONE; minLiteral = 0; minPrevQues = ""; maxLimitType = NumericLimitType.NLT_NONE; maxLiteral = 10000; maxPrevQues = ""; minCheckableBoxes = 0; maxCheckableBoxes = 100; withListRange = new Boolean(false); listRangeString = new String(""); minListRange = new Integer(0); maxListRange = new Integer(100); timeUnits = 0xff; symmetric = true; keepOnSamePage = false; } public String toString() { return (title == null ? "Untitled question" : title)+" ("+type+")"; } // Copy all attributes except identifiers: id and key // Change name just slightly to not conflict with existing names public Question copy() { Question c = new Question(); c.setAnswerReasonExpressionId(getAnswerReasonExpressionId()); c.setAnswerType(getAnswerType()); c.setAskingStyleList(getAskingStyleList()); c.setCitation(getCitation()); c.setMaxCheckableBoxes(getMaxCheckableBoxes()); c.setMaxLimitTypeDB(getMaxLimitTypeDB()); c.setMaxLiteral(getMaxLiteral()); c.setMaxPrevQues(getMaxPrevQues()); c.setMinCheckableBoxes(getMinCheckableBoxes()); c.setMinLimitTypeDB(getMinLimitTypeDB()); c.setMinLiteral(getMinLiteral()); c.setMinPrevQues(getMinPrevQues()); c.setOrdering(getOrdering()); c.setOtherSpecify(getOtherSpecify()); c.setPreface(getPreface()); c.setPrompt(getPrompt()); c.setStudyId(getStudyId()); c.setTitle(getTitle().replaceAll("\\s+c\\d+", "")+" c"+(Math.abs(c.getRandomKey())%1000)); c.setTypeDB(getTypeDB()); c.setUseIfExpression(getUseIfExpression()); c.setNoneButton(getNoneButton()); c.setAllButton(getAllButton()); c.setPageLevelDontKnowButton(getPageLevelDontKnowButton()); c.setPageLevelRefuseButton(getPageLevelRefuseButton()); c.setDontKnowButton(getDontKnowButton()); c.setRefuseButton(getRefuseButton()); c.setAllOptionString(getAllOptionString()); c.setWithListRange(getWithListRange()); c.setListRangeString(getListRangeString()); c.setMinListRange(getMinListRange()); c.setMaxListRange(getMaxListRange()); c.setTimeUnits(getTimeUnits()); c.setSymmetric(getSymmetric()); c.setKeepOnSamePage(getKeepOnSamePage()); return c; } @Override public int compareTo(OrderedEntity entity) { if(entity instanceof Question) { Question question = (Question) entity; if(! getType().equals(question.getType())) { if(getType().ordinal() < question.getType().ordinal()) { return -1; } if(getType().ordinal() > question.getType().ordinal()) { return 1; } } } return super.compareTo(entity); } public String individualizePrompt(ArrayList<Alter> alters) { String questionText = getPrompt(); String dd = "\\$\\$"; String dd1 = dd+"1"; String dd2 = dd+"2"; String blank = "_____"; String alter1Name = alters.size() > 0 ? alters.get(0).getName() : blank; String alter2Name = alters.size() > 1 ? alters.get(1).getName() : blank; // Prevent StringIndexOutOfBoundsException when alter name contains ? or \ alter1Name = Matcher.quoteReplacement(alter1Name); alter2Name = Matcher.quoteReplacement(alter2Name); if(getType().equals(QuestionType.ALTER)) { questionText = questionText.replaceAll(dd1, alter1Name); questionText = questionText.replaceAll(dd, alter1Name); } if(getType().equals(QuestionType.ALTER_PAIR)) { questionText = questionText.replaceAll(dd1, alter1Name); questionText = questionText.replaceAll(dd2, alter2Name); questionText = questionText.replaceFirst(dd, alter1Name); questionText = questionText.replaceFirst(dd, alter2Name); } return questionText; } public AnswerType getAnswerType() { return this.answerType; } public String getTitle() { return this.title; } public QuestionType getType() { return this.type; } public Long getStudyId() { return this.studyId; } public String getAnswerTypeDB() { return getAnswerType().name(); } public String getTypeDB() { return typeDB(getType()); } public static String typeDB(QuestionType type) { return type == null ? null : type.name(); } /** * @return * if needsSelectionResponse returns a list of one Option, * if needsMultiSelectionResponse returns a list of Option * otherwise null */ public void setAnswerType(AnswerType val) { this.answerType = val; } public void setTitle(String val) { this.title = val; } public void setType(QuestionType val) { this.type = val; } public void setStudyId(Long val) { this.studyId = val; } public void setAnswerTypeDB(String val) { this.setAnswerType(AnswerType.valueOf(val)); } public void setTypeDB(String val) { this.setType(QuestionType.valueOf(val)); } /** Whether the question identifies the interviewee */ public boolean identifiesEgo() { return getType() == QuestionType.EGO_ID; } /** Whether the question concerns the interviewee */ public boolean isAboutEgo() { return getType() == QuestionType.EGO; } /** Whether the question concerns the interviewee's alter */ public boolean isAboutAlter() { return getType() == QuestionType.ALTER; } /** Whether the question describes the relationship between two of the interviewee's alters */ public boolean isAboutRelationship() { return getType() == QuestionType.ALTER_PAIR; } /** Whether the question describes the network of the interviewee's alters */ public boolean isAboutNetwork() { return getType() == QuestionType.NETWORK; } public boolean needsTextualResponse() { return getAnswerType() == AnswerType.TEXTUAL; } public boolean needsNumericalResponse() { return getAnswerType() == AnswerType.NUMERICAL; } public boolean needsSelectionResponse() { return getAnswerType() == AnswerType.SELECTION; } public boolean needsMultiSelectionResponse() { return getAnswerType() == AnswerType.MULTIPLE_SELECTION; } public void setAnswerReasonExpressionId(Long answerReasonExpressionId) { this.answerReasonExpressionId = answerReasonExpressionId; } public Long getAnswerReasonExpressionId() { return answerReasonExpressionId; } public void setNetworkRelationshipExprId(Long expId) { this.networkRelationshipExprId = expId; } public Long getNetworkRelationshipExprId() { return networkRelationshipExprId; } public void setNetworkNShapeQId(Long qid) { this.networkNShapeQId = qid; } public Long getNetworkNShapeQId() { return networkNShapeQId; } public void setNetworkNColorQId(Long qid) { this.networkNColorQId = qid; } public Long getNetworkNColorQId() { return networkNColorQId; } public void setNetworkNSizeQId(Long qid) { this.networkNSizeQId = qid; } public Long getNetworkNSizeQId() { return networkNSizeQId; } public void setNetworkEColorQId(Long qid) { this.networkEColorQId = qid; } public Long getNetworkEColorQId() { return networkEColorQId; } public void setNetworkESizeQId(Long qid) { this.networkESizeQId = qid; } public Long getNetworkESizeQId() { return networkESizeQId; } // --------------------------------- protected String getPromptOld() { return null; } public String getPrompt() { return migrateToText(this,"prompt"); } public void setPrompt(String val) { this.prompt = val; } protected void setPromptOld(String val) { this.promptOld = val; } public void setPreface(String preface) { this.preface = preface; } public String getPreface() { return migrateToText(this,"preface"); } public boolean hasPreface() { String preface = getPreface(); return ! (preface == null || preface.isEmpty() || preface.matches("\\s*")); } protected void setPrefaceOld(String preface) { this.prefaceOld = preface; } protected String getPrefaceOld() { return null; } public String getCitation() { return migrateToText(this,"citation"); } public void setCitation(String val) { this.citation = val; } protected String getCitationOld() { return null; } protected void setCitationOld(String val) { this.citationOld = val; } public void setAskingStyleList(Boolean askingStyleList) { if(askingStyleList != null) { this.askingStyleList = askingStyleList; } } public Boolean getAskingStyleList() { if(getType().equals(QuestionType.EGO)) { return false; } return askingStyleList == null ? false : askingStyleList; } /** * get/set the 'otherSpecify' flag which will deal with * a text entry box appearing when 'Other' is selected * @param otherSpecify */ public void setOtherSpecify (Boolean otherSpecify) { this.otherSpecify = (otherSpecify==null)? false:otherSpecify; } public Boolean getOtherSpecify() { if ( otherSpecify==null ) otherSpecify = false; return(otherSpecify); } public void setNoneButton ( Boolean noneButton) { this.noneButton = (noneButton==null) ? new Boolean(false) : noneButton; } public Boolean getNoneButton() { if ( noneButton==null) noneButton = new Boolean(false); return(noneButton); } public void setAllButton ( Boolean allButton) { this.allButton = (allButton==null) ? new Boolean(false) : allButton; } public Boolean getAllButton() { if ( allButton==null) allButton =new Boolean(false); return(allButton); } public void setPageLevelDontKnowButton ( Boolean pageLevelDontKnowButton ) { if ( pageLevelDontKnowButton==null) System.out.println ( "question " + title + "has null pageLevelDontKnowButton"); this.pageLevelDontKnowButton = (pageLevelDontKnowButton==null) ? new Boolean(true) : pageLevelDontKnowButton; } public Boolean getPageLevelDontKnowButton() { if ( pageLevelDontKnowButton==null) pageLevelDontKnowButton = new Boolean(true); return(pageLevelDontKnowButton ); } public void setPageLevelRefuseButton( Boolean pageLevelRefuseButton ) { this.pageLevelRefuseButton = (pageLevelRefuseButton==null) ? new Boolean(true) : pageLevelRefuseButton; } public Boolean getPageLevelRefuseButton() { if ( pageLevelRefuseButton==null) pageLevelRefuseButton = new Boolean(true); return(pageLevelRefuseButton); } public void setDontKnowButton ( Boolean dontKnowButton ) { this.dontKnowButton = (dontKnowButton==null) ? new Boolean(true): dontKnowButton; } public Boolean getDontKnowButton() { if ( dontKnowButton==null) dontKnowButton = new Boolean(true); return(dontKnowButton); } public void setRefuseButton ( Boolean refuseButton) { this.refuseButton = (refuseButton==null) ? new Boolean(true) : refuseButton; } public Boolean getRefuseButton() { if ( refuseButton==null) refuseButton = new Boolean(true); return(refuseButton); } public void setAllOptionString ( String allOptionString) { this.allOptionString = (allOptionString==null) ? "" : allOptionString; } public String getAllOptionString() { if ( allOptionString==null) allOptionString = ""; return(allOptionString); } public void setUseIfExpression ( String useIfExpression ) { this.useIfExpression = (useIfExpression==null) ? "" : useIfExpression; } public String getUseIfExpression() { if ( useIfExpression==null ) useIfExpression = ""; return (useIfExpression); } /* ****************************************************************** */ /* next set of functions deal with setting limits on NUMERICAL */ /* questions */ /* ****************************************************************** */ /** * getters & setters for the numeric bounds */ public void setMinLimitType(NumericLimitType minLimitType) { this.minLimitType = (minLimitType==null)?NumericLimitType.NLT_NONE:minLimitType; } public NumericLimitType getMinLimitType() { return(minLimitType);} public String getMinLimitTypeDB() { NumericLimitType nlType = getMinLimitType(); return ( nlType==null ? NumericLimitType.NLT_NONE.name() : nlType.name() ); } public void setMinLimitTypeDB ( String val ) { NumericLimitType nlType; if ( val==null || val.length()==0 ) nlType = NumericLimitType.NLT_NONE; else nlType = NumericLimitType.valueOf(val); setMinLimitType(nlType); } public void setMinLiteral ( Integer minLiteral ) { this.minLiteral = (minLiteral==null)?0:minLiteral; } public Integer getMinLiteral () { return(minLiteral);} public void setMinPrevQues ( String minPrevQues ) { this.minPrevQues = (minPrevQues==null)?"":minPrevQues; } public String getMinPrevQues() {return(minPrevQues);}; public void setMaxLimitType(NumericLimitType maxLimitType) { this.maxLimitType = (maxLimitType==null)?NumericLimitType.NLT_NONE:maxLimitType; } public NumericLimitType getMaxLimitType() { return(maxLimitType);} public String getMaxLimitTypeDB() { NumericLimitType nlType = getMaxLimitType(); return ( nlType==null ? NumericLimitType.NLT_NONE.name() : nlType.name() ); } public void setMaxLimitTypeDB ( String val ) { NumericLimitType nlType; if ( val==null || val.length()==0 ) nlType = NumericLimitType.NLT_NONE; else nlType = NumericLimitType.valueOf(val); setMaxLimitType(nlType); } public void setMaxLiteral ( Integer maxLiteral ) { this.maxLiteral = (maxLiteral==null)?10000:maxLiteral; } public Integer getMaxLiteral () { return(maxLiteral);} public void setMaxPrevQues ( String maxPrevQues ) { this.maxPrevQues = (maxPrevQues==null)?"":maxPrevQues; } public String getMaxPrevQues() {return(maxPrevQues);}; /** * getters & setters for min/max checkable checkboxes * for MULTIPLE_SELECTION questions */ public void setMinCheckableBoxes (Integer minCheckableBoxes) { this.minCheckableBoxes = (minCheckableBoxes==null)?new Integer(0):minCheckableBoxes; } public Integer getMinCheckableBoxes() { return( minCheckableBoxes ); } public void setMaxCheckableBoxes (Integer maxCheckableBoxes) { this.maxCheckableBoxes = (maxCheckableBoxes==null)?new Integer(100):maxCheckableBoxes; } public Integer getMaxCheckableBoxes() { return( maxCheckableBoxes ); } public void setTimeUnits (Integer timeUnits) { this.timeUnits = (timeUnits==null)?new Integer(0xff):timeUnits; } public Integer getTimeUnits() { return( timeUnits ); } /* ****************************************************************** */ /* these variables deal with ad-hoc situations in the list-of-alters */ /* question format. Sometimes we want the survey taker to limit the */ /* number of times a specific answer is selected - like indicating */ /* the four most recent sex partners out of a list of 20 */ public void setWithListRange( Boolean withListRange) { this.withListRange = (withListRange==null) ? new Boolean(false) : withListRange; } public Boolean getWithListRange() { return(withListRange); } public void setListRangeString( String listRangeString) { this.listRangeString = (listRangeString==null) ? new String("") : listRangeString; } public String getListRangeString() { return (listRangeString); } public void setMinListRange( Integer minListRange) { this.minListRange = (minListRange==null) ? new Integer(0) : minListRange; } public Integer getMinListRange() { return (minListRange); } public void setMaxListRange( Integer maxListRange) { this.maxListRange = (maxListRange==null) ? new Integer(0) : maxListRange; } public Integer getMaxListRange() { return (maxListRange); } /** * @return prompt that tells the survey taker how many of a crucial * answer to select in a list-of-alters question. */ public String getListRangePrompt() { String strPrompt = ""; if ( type!=QuestionType.ALTER && type!=QuestionType.ALTER_PAIR ) return(strPrompt); if ( !askingStyleList || !withListRange ) return(strPrompt); strPrompt = "Select " + listRangeString.toUpperCase() + " " + minListRange; if ( minListRange.equals(maxListRange) ) strPrompt += " time" + ((minListRange==1) ? "." : "s."); else strPrompt += " to " + maxListRange + " times."; return(strPrompt); } public void setSymmetric( Boolean symmetric ) { this.symmetric = (symmetric==null) ? new Boolean(false) : symmetric; } public Boolean getSymmetric() { if ( symmetric==null ) symmetric = new Boolean(false); return(symmetric); } public void setKeepOnSamePage( Boolean keepOnSamePage ) { this.keepOnSamePage = (keepOnSamePage==null) ? new Boolean(false) : keepOnSamePage; } public Boolean getKeepOnSamePage() { if ( keepOnSamePage==null ) keepOnSamePage = new Boolean(false); return(keepOnSamePage); } /* ****************************************************************** */ /* functions from this point down deal with substituting the answer */ /* from a previous question into a new questions prompt */ /* ****************************************************************** */ /** * non-static form of answerToQuestion below for the more common need * to retrieve the answer to THIS question */ public String answerToQuestion ( Long interviewId, ArrayList<Alter> listOfAlters ) { return ( TextInsertionUtil.answerToQuestion(title, interviewId, type, studyId, listOfAlters)); } /** * numeric questions sometimes have their response bounded by answers to previous * questions, the names held in minPrevQues and maxPrevQues. This function will * retrieve those answers * @param alterId1 id of alter one ( may be null ) * @param alterId2 id of alter two ( may be null ) * @param getMaximum if true get maximum limit, if false get minimum limit * @return answer to previous question */ public String answerToRangeLimitingQuestion ( Long interviewId, ArrayList<Alter> listOfAlters, boolean getMaximum ) { String strPreviousAnswer; if (getMaximum) strPreviousAnswer = TextInsertionUtil.answerToQuestion ( maxPrevQues, interviewId, type, studyId, listOfAlters); else strPreviousAnswer = TextInsertionUtil.answerToQuestion ( minPrevQues, interviewId, type, studyId, listOfAlters); // shouldn't happen, but check anyway // *might* want to assign low/high default values if not found, // for now just assign 0 if ( strPreviousAnswer==null ) strPreviousAnswer = "0"; return(strPreviousAnswer); } /** * a convenience function that turns around and called the TextInsertionUtil static function * variableInsertion. Most commonly we will want to do it on the * prompt of this question but in this case the prompt is an individualized * prompt that has had appropriate strings already substituted into place. * This deals with variable insertion using tags of the format <VAR Q1 /> * @param strPrompt text of question * @param alterId1 id of one person addressed. may be null * @param alterId2 id of second person addressed. may be null * @return a string with previous answers inserted into place */ public String variableInsertion ( String strPrompt, Long interviewId, ArrayList<Alter> listOfAlters ) { return ( TextInsertionUtil.variableInsertion(strPrompt, interviewId, type, studyId, listOfAlters )); } /** * dateDataInsertion is very similar to variable insertion but dates need a little bit of * extra work * @param strPrompt text of question * @param alterId1 id of one person addressed. may be null * @param alterId2 id of second person addressed. may be null * @return a string with previous answers inserted into place */ public String dateDataInsertion ( String strPrompt, Long interviewId, ArrayList<Alter> listOfAlters ) { return ( TextInsertionUtil.dateDataInsertion(strPrompt, interviewId, type, studyId, listOfAlters )); } /** * a convenience function that turns around and called the TextInsertionUtil static function * calculationInsertion. Most commonly we will want to do it on the * prompt of this question but in this case the prompt is an individualized * prompt that has had appropriate strings already substituted into place. * This deals with variable insertion using tags of the format <CALC Q1+Q2 /> * @param strPrompt text of question * @param interviewId uniquely identifies this interview * @param listOfAlters - used in retrieving question answers * @return a string with previous answers inserted into place */ public String calculationInsertion ( String strPrompt, Long interviewId, ArrayList<Alter> listOfAlters ) { return ( TextInsertionUtil.calculationInsertion(strPrompt, interviewId, type, studyId, listOfAlters )); } /** * a convenience function that turns around and called the TextInsertionUtil static function * calculationInsertion. Most commonly we will want to do it on the * prompt of this question but in this case the prompt is an individualized * prompt that has had appropriate strings already substituted into place * This deals with variable insertion using tags of the format <COUNT Q1 "answer"/> * @param strPrompt text of question * @param interviewId uniquely identifies this interview * @param listOfAlters - a list of ALL alters in the study * @return a string with previous answers inserted into place */ public String answerCountInsertion ( String strPrompt, Long interviewId ) { return ( TextInsertionUtil.answerCountInsertion(strPrompt, interviewId, studyId )); } /** * a convenience function that turns around and called the TextInsertionUtil static function * questionContainsAnswerInsertion. Most commonly we will want to do it on the * prompt of this question but in this case the prompt is an individualized * prompt that has had appropriate strings already substituted into place. * This deals with variable insertion using tags of the format <CONTAINS question answer /> * @param strPrompt text of question * @param interviewId uniquely identifies this interview * @param listOfAlters - used in retrieving question answers * @return a string with previous answers inserted into place */ public String questionContainsAnswerInsertion ( String strPrompt, Long interviewId, ArrayList<Alter> listOfAlters ) { return ( TextInsertionUtil.questionContainsAnswerInsertion(strPrompt, interviewId, type, studyId, listOfAlters )); } /** * a convenience function that turns around and called the TextInsertionUtil static function * conditionalTextInsertion. Most commonly we will want to do it on the * prompt of this question but in this case the prompt is an individualized * prompt that has had appropriate strings already substituted into place * This deals with variable insertion using tags of the format <IF Q1>3 "more than three"/> * @param strPrompt text of question * @param alterId1 id of one person addressed. may be null * @param alterId2 id of second person addressed. may be null * @return a string with previous answers inserted into place */ public String conditionalTextInsertion ( String strPrompt, Long interviewId, ArrayList<Alter> listOfAlters ) { return ( TextInsertionUtil.conditionalTextInsertion(strPrompt, interviewId, type, studyId, listOfAlters )); } public String escapeTextInsertionTags ( String strInput ) { return ( TextInsertionUtil.escapeTextInsertionTags ( strInput )); } }