/*** * Copyright (c) 2008, Endless Loop Software, Inc. * * This file is part of EgoNet. * * EgoNet is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * EgoNet is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.egonet.model; import java.util.*; import java.util.Map.Entry; import javax.swing.DefaultListModel; import javax.swing.JFrame; import javax.swing.JInternalFrame; import javax.swing.JOptionPane; import org.egonet.exceptions.DuplicateQuestionException; import org.egonet.model.Shared.AlterNameModel; import org.egonet.model.Shared.AlterSamplingModel; import org.egonet.model.answer.*; import org.egonet.model.question.*; /******************************************************************************* * Stores basic configuration data for the study including question order lists */ public class Study extends Observable implements Comparable<Study> { private String _uniqueId = ""; private String _uiType = Shared.TRADITIONAL_QUESTIONS; private boolean _studyDirty = false; private boolean _compatible = true; private boolean _inUse = false; private String _studyName = "New Study"; private boolean skipQuestions = false; private int _minAlters = 40; private int _maxAlters = 40; private Map<Class<? extends Question>,List<Long>> _questionOrder = new HashMap<Class<? extends Question>,List<Long>>(); private QuestionList _questions = new QuestionList(); /* Added for UNC */ private AlterSamplingModel alterSamplingModel = AlterSamplingModel.ALL; private AlterNameModel alterNameModel = AlterNameModel.FIRST_LAST; private Integer alterSamplingParameter = null; private boolean unlimitedMode = false; /** * Instantiates Default Study */ public Study() { this._questions.clear(); for(Class<? extends Question> type : Shared.questionClasses) _questionOrder.put(type, new ArrayList<Long>()); } /*************************************************************************** * Returns UniqueId of study read from file * * @return long Unique Id of study */ public String getStudyId() { return (_uniqueId); } /*************************************************************************** * Notifies observers that a field in the study has changed */ public void notifyObservers() { setChanged(); super.notifyObservers(this); } /*************************************************************************** * Returns name of study * * @return name name of study */ public String getStudyName() { return (_studyName); } /** * @return Returns the questions. */ public QuestionList getQuestions() { return _questions; } /** * @return Returns the questions. */ public Question getQuestion(Long id) { return _questions.getQuestion(id); } /** * @return Returns the firstQuestion. */ public Question getFirstQuestion() { return _questions.values().iterator().next(); } /*************************************************************************** * Returns array of questions for a specified category * * @param category * category of questions to return * @return questionOrder array of question Ids * @throws NoSuchElementException * for category out of range */ public List<Long> getQuestionOrder(Class<? extends Question> category) throws NoSuchElementException { return _questionOrder.get(category); } /*************************************************************************** * Working forward from beginning, find first unanswered question * * @return index of first unanswered question */ public Question getFirstStatableQuestion() { Question statable = null; /** * Try to find one all alters answer */ Iterator<Long> questions = getQuestionOrder(AlterPairQuestion.class).iterator(); while (questions.hasNext()) { Question q = (Question) _questions.getQuestion(questions.next()); if (q.isStatable() && !q.link.isActive()) { statable = q; break; } } /** * Settle for any statable */ if (statable == null) { questions = getQuestionOrder(AlterQuestion.class).iterator(); while (questions.hasNext()) { Question q = (Question) _questions.getQuestion(questions.next()); if (q.isStatable()) { statable = q; } } } return (statable); } /*************************************************************************** * Returns UniqueId of study read from file * * @param long * Unique Id of study */ public void setStudyId(String id) { _uniqueId = id; } /*************************************************************************** * Sets name of study * * @param name * name of study */ public void setStudyName(String name) { if (!_studyName.equals(name)) { _studyName = name; setModified(true); } } /*************************************************************************** * Sets study dirty flag; generally done when the study is written to a * file */ public void setModified(boolean dirty) { _studyDirty = dirty; notifyObservers(); } /*************************************************************************** * gets dirty state of study * * @return dirty */ public boolean isModified() { return (_studyDirty); } /** * @return Returns the compatible. */ public boolean isCompatible() { return _compatible; } /** * @param compatible The compatible to set. */ public void setCompatible(boolean compatible) { this._compatible = compatible; notifyObservers(); } /** * @return Returns the inUse. */ public boolean isInUse() { return _inUse; } /** * @param inUse The inUse to set. */ public void setInUse(boolean inUse) { this._inUse = inUse; } /** * @return Returns the uiType */ public String getUIType() { return _uiType; } public boolean confirmIncompatibleChange(JInternalFrame frame) { return confirmIncompatibleChange((JFrame)null); } /** * Warn user this change will make study no longer compatible with previous interviews * @param q * @throws DuplicateQuestionException */ public boolean confirmIncompatibleChange(JFrame frame) { boolean ok = true; if (isInUse() && isCompatible()) { int confirm = JOptionPane.showConfirmDialog( frame, "This study has already been used for at least one interview.\n" + "If you make this change you will have to save this as a new study and will \n" + "no longer be able to access prior interviews with this study.\n" + "Do you still wish to make this change?", "Incompatible Study Modification", JOptionPane.OK_CANCEL_OPTION); if (confirm != JOptionPane.OK_OPTION) { ok = false; } } return ok; } /*************************************************************************** * Adds a question to the full question list * * @param q * question to add */ public void addQuestion(Question q) throws DuplicateQuestionException { if (_questions.contains(q.UniqueId)) throw new DuplicateQuestionException("Question with uniqueId "+q.UniqueId+" already added to study: " + _questions.toString()); _questions.addQuestion(q); setModified(true); /* If not in appropriate array list, add to that list too */ if (!_questionOrder.get(q.getClass()).contains(q.UniqueId)) { _questionOrder.get(q.getClass()).add(q.UniqueId); } } /*************************************************************************** * Changes position of a question in an order list * * @param q * question to move * @param follow * question q should follow */ public void moveQuestionAfter(Question q, Question follow) { int followloc; if (_questionOrder.get(q.getClass()).contains(follow.UniqueId) || (follow.equals(getFirstQuestion()))) { _questionOrder.get(q.getClass()).remove(q.UniqueId); if (follow.equals(getFirstQuestion())) { _questionOrder.get(q.getClass()).add(0, q.UniqueId); } else { followloc = _questionOrder.get(q.getClass()).indexOf(follow.UniqueId); _questionOrder.get(q.getClass()).add(followloc + 1, q.UniqueId); } if (q.link.isActive()) { if (!doesQuestionPreceed(q.link.getAnswer().getQuestionId(), q.UniqueId)) { q.link.setAnswer(null); } } setModified(true); } } public void changeQuestionType(Question oldQ, Class<? extends Question> newType) throws DuplicateQuestionException { if(oldQ.getClass().equals(newType)) return; removeQuestion(oldQ); Question newQ = Question.newInstance(newType); // TODO: can be *way* improved newQ.title = newQ.title; newQ.text = oldQ.text; newQ.citation = oldQ.citation; newQ.answerType = oldQ.answerType; addQuestion(newQ); setModified(true); } /*************************************************************************** * Go through question list making sure any interquestion dependencies are * met * * @param q * question to move * @param type * new question type */ public void setCentralQuestion(Question q) { Iterator<Long> i = _questionOrder.get(AlterPairQuestion.class).iterator(); while (i.hasNext()) { Long key = (Long) i.next(); Question listQ = this._questions.getQuestion(key); if (listQ != null) { if (listQ.equals(q)) { if (!listQ.centralMarker) { listQ.centralMarker = true; setModified(true); } } else { /* Only one centralMarker allowed */ if (listQ.centralMarker) { listQ.centralMarker = false; setModified(true); } } } } } /*************************************************************************** * Go through question list making sure any interquestion dependencies are * met * * @param q * question to move * @param type * new question type */ public void validateQuestions() { boolean foundCentral = false; Iterator<Long> it = _questionOrder.get(AlterPairQuestion.class).iterator(); while (it.hasNext()) { Long key = it.next(); Question q = this._questions.getQuestion(key); if (q != null) { if (!foundCentral && q.centralMarker) { foundCentral = true; } else { /* Only one centralMarker allowed */ q.centralMarker = false; } } } if (!foundCentral) { /* Tag first Alter pair categorical question */ Iterator<Long> it2 = _questionOrder.get(AlterPairQuestion.class).iterator(); while (it2.hasNext() && !foundCentral) { Long key = it2.next(); Question q = this._questions.getQuestion(key); if ((q != null) && (q.answerType.equals(CategoricalAnswer.class))) { q.centralMarker = true; foundCentral = true; } } } } /*************************************************************************** * Searches question list for all questions of a given type, places them in * list * * @param questionType * type filter for question list * @param dlm * list model to use in inserting questions */ public void fillList(Class<? extends Question> questionType, DefaultListModel<Question> dlm) { for(Map.Entry<Class<? extends Question>,List<Long>> entry : _questionOrder.entrySet()) { if(!entry.getKey().equals(questionType)) continue; for(Long id : entry.getValue()) { if(_questions.contains(id)) dlm.addElement(_questions.getQuestion(id)); } } } /*************************************************************************** * Searches question list for all questions, places them in * list * * @param questionType * type filter for question list * @param dlm * list model to use in inserting questions */ public void fillList(DefaultListModel<Question> dlm) { Set<Entry<Class<? extends Question>, List<Long>>> entries = _questionOrder.entrySet(); for(Map.Entry<Class<? extends Question>,List<Long>> entry : entries) { if(entry.getKey().equals(AlterPromptQuestion.class)) continue; List<Long> questions = entry.getValue(); for(Long id : questions) { if(_questions.contains(id)) dlm.addElement(_questions.getQuestion(id)); } } } /*************************************************************************** * Searches question list for all questions of a given tpe, places them in * list until a given question is reached * * @param questionType * type filter for question list * @param dlm * list model to use in inserting questions * @param endId * question list end, stop when you see this question */ public void fillList(DefaultListModel<Question> dlm, Long endId) { for(Map.Entry<Class<? extends Question>,List<Long>> entry : _questionOrder.entrySet()) { if(entry.getKey().equals(AlterPromptQuestion.class)) continue; for(Long id : entry.getValue()) { if(id.equals(endId)) return; else if(_questions.contains(id)) dlm.addElement(_questions.getQuestion(id)); } } } /*************************************************************************** * Searches question list for all questions of a given tpe, places them in * list until a given question is reached * * @param questionType * type filter for question list * @param dlm * list model to use in inserting questions * @param endId * question list end, stop when you see this question */ public void fillList(Class<? extends Question> questionType, DefaultListModel<Question> dlm, Long endId) { for(Map.Entry<Class<? extends Question>,List<Long>> entry : _questionOrder.entrySet()) { if(!entry.getKey().equals(questionType)) continue; for(Long id : entry.getValue()) { if(id.equals(endId)) return; else if(_questions.contains(id)) dlm.addElement(_questions.getQuestion(id)); } } } /*************************************************************************** * Returns true iff q1 preceeds q2 in study * * @param q1 * Id of question which may preceed q2 * @param q2 * Id of question which may be preceeded by q1 */ public boolean doesQuestionPreceed(Long q1, Long q2) { for(Class<? extends Question> qT : Shared.questionClasses) { if(qT.equals(StudyQuestion.class)) continue; List<Long> questionList = _questionOrder.get(qT); for(Long key : questionList) { if (key.equals(q1)) { return true; } else if (key.equals(q2)) { return false; } } } return false; } /*************************************************************************** * Essentially makes sure order list matches question list */ public void verifyStudy() { for (Class<? extends Question> type : Shared.questionClasses) { Iterator<Long> it = getQuestionIterator(type); while (it.hasNext()) { Long qid = (Long) it.next(); if (_questions.getQuestion(qid) == null) { it.remove(); } } } } /*************************************************************************** * Returns a bi-directional list iterator of questions for a category * * @param category * category of question * @return iterator list iterator or questions */ public ListIterator<Long> getQuestionIterator(Class<? extends Question> category) { return (_questionOrder.get(category).listIterator()); } /*************************************************************************** * Remove all base or custom Questions from question list and order lists * * @param base * Remove questions from base file or custom file */ public void removeQuestions() { Question q; Iterator i = this._questions.values().iterator(); while (i.hasNext()) { q = (Question) i.next(); i.remove(); removeQuestion(q); } } /*************************************************************************** * Remove one Question from question map and order lists * * @param q * question to remove */ public void removeQuestion(Question q) { removeLinksToQuestion(q); for (List<Long> orderList : _questionOrder.values()) orderList.remove(q.UniqueId); _questions.remove(q.UniqueId); setModified(true); } /*************************************************************************** * Searches question list for any questions linked to a question about to * be removed, and removes those question links. * * @param questionType * type filter for question list * @param dlm * list model to use in inserting questions */ public void removeLinksToQuestion(Question lq) { Iterator<Question> i = this._questions.values().iterator(); while (i.hasNext()) { Question q = (Question) i.next(); if (q.link.isActive() && (q.link.getAnswer().getQuestionId().equals(lq.UniqueId))) { q.link.setAnswer(null); } } } public AlterSamplingModel getAlterSamplingModel() { return alterSamplingModel; } public void setAlterSamplingModel(AlterSamplingModel alterSamplingModel) { this.alterSamplingModel = alterSamplingModel; } public Integer getAlterSamplingParameter() { return alterSamplingParameter; } public void setAlterSamplingParameter(Integer alterSamplingParameter) { this.alterSamplingParameter = alterSamplingParameter; } public void setMinimumNumberOfAlters(int n) { if(n != _minAlters) setModified(true); _minAlters=n; } public int getMinimumNumberOfAlters() { return _minAlters; } public void setMaximumNumberOfAlters(int n) { if(n != _maxAlters) setModified(true); _maxAlters=n; } public int getMaximumNumberOfAlters() { return _maxAlters; } public int getExpectedNumberOfAlters(int collectedAlters) { if(alterSamplingModel.equals(AlterSamplingModel.RANDOM_SUBSET)) return alterSamplingParameter; else if(alterSamplingModel.equals(AlterSamplingModel.NTH_ALTER)) return collectedAlters/alterSamplingParameter; else return collectedAlters; } public int compareTo(Study o) { return getStudyId().compareTo(o.getStudyId()); } public AlterNameModel getAlterNameModel() { return alterNameModel; } public void setAlterNameModel(AlterNameModel alterNameModel) { this.alterNameModel = alterNameModel; } public String toString() { return getStudyName() + " (" + getStudyId() + ")"; } public void setAllowSkipQuestions(boolean selected) { this.skipQuestions = selected; } public boolean getAllowSkipQuestions() { return skipQuestions; } public void setUnlimitedMode(boolean unlimitedMode) { this.unlimitedMode = unlimitedMode; } public boolean isUnlimitedAlterMode() { return this.unlimitedMode; } }