/* * Copyright 2011 JBoss Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.drools.informer; import org.drools.definition.type.Modifies; import org.drools.definition.type.PropertyReactive; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.math.BigDecimal; import java.text.DateFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; /** * <p> * Represents a question to be answered by a user. * </p> * * <p> * <code>Question</code> has an <code>answerType</code> which must be one of: * </p> * * <ul> * <li><code>text</code></li> * <li><code>number</code></li> * <li><code>decimal</code></li> * <li><code>boolean</code></li> * <li><code>date</code></li> * <li><code>list</code></li> * </ul> * * <p> * or an extension of one of these using the notation <code><type>.<extension type> </code> e.g. <code>text.url</code> * or <code>decimal.currency</code>. * </p> * * <p> * The answer to a <code>Question</code> is maintained internally by the object. use <code>DomainModelAssociation</code> to map * the answers to a real domain model. * </p> * * TODO the get/setListAnswer methods should be using String[] not String for consistency with all the other methods that deal * with lists of values (e.g. Group.get/setItems). The list is represented INTERNALLY as a string but that detail should not be * exposed outside of this class. Note that the setter method will need to be overloaded though with a String version for use by * the Tohu built-in rules because the internal representation is what is sent to the client via XML and so Question.drl needs to * handle it when it comes back. The String version of this setter should be marked as "internal use only". See get/setDateAnswer * as a comparison as dates are stored internally as strings but the methods expose them as Date. ListAnswer should follow this * same pattern. * * @author Damon Horrell */ @PropertyReactive public class Question extends Item { private static final long serialVersionUID = 1L; private static final DateFormat DATE_TRANSPORT_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private transient DateFormat dateFormatter = DATE_TRANSPORT_FORMAT; protected DateFormat getDateFormatter() { return dateFormatter; } public void setDateFormat(String dateFormat) { if (dateFormat != null && dateFormat.length() > 0) { this.dateFormatter = new SimpleDateFormat(dateFormat); } } public static enum QuestionType { TYPE_TEXT("text"), TYPE_NUMBER("number"), TYPE_DECIMAL("decimal"), TYPE_BOOLEAN("boolean"), TYPE_DATE("date"), TYPE_LIST("list"); private String value; QuestionType(String val) { value = val; } public String getValue() { return value; } } private String preLabel; private String label; private String postLabel; private String reason; private boolean required; private QuestionType answerType; @AnswerField private String textAnswer; @AnswerField private Long numberAnswer; @AnswerField private BigDecimal decimalAnswer; @AnswerField private Boolean booleanAnswer; /** * Dates are stored internally as strings so that they are transported to the client as just yyyy-mm-dd and not with the * redundant time and timezone data on the end. * * (The Java Date class is really DateTime and is mis-named. If there is ever a need to support TIME or DATETIME in the future * then these should be defined as distinct types.) */ @AnswerField private String dateAnswer; /** * List is stored as a delimited string */ @AnswerField private String listAnswer; @AnswerField private String lastAnswer; private boolean finalAnswer = false; public Question() { } public Question(String type) { super(type); } public Question(String type, String label) { super(type); this.preLabel = label; } public String getPreLabel() { return preLabel; } public void setPreLabel(String preLabel) { this.preLabel = preLabel; } public String getPostLabel() { return postLabel; } public void setPostLabel(String postLabel) { this.postLabel = postLabel; } public String getReason() { return reason; } public void setReason(String reason) { this.reason = reason; } public boolean isRequired() { return required; } /** * If set to true then the Tohu built-in rules will create an <code>InvalidAnswer</code> if this question is not answered. * * @param required */ public void setRequired(boolean required) { this.required = required; } public boolean isFinalAnswer() { return finalAnswer; } public void setFinalAnswer(boolean finalAnswer) { this.finalAnswer = finalAnswer; } public QuestionType getAnswerType() { return answerType; } public void setAnswerType(QuestionType answerType) { QuestionType previousBasicAnswerType = this.getBasicAnswerType(); QuestionType basicAnswerType = answerType; if (basicAnswerType == null || (!basicAnswerType.equals(QuestionType.TYPE_TEXT) && !basicAnswerType.equals(QuestionType.TYPE_NUMBER) && !basicAnswerType.equals(QuestionType.TYPE_DECIMAL) && !basicAnswerType.equals(QuestionType.TYPE_BOOLEAN) && !basicAnswerType.equals(QuestionType.TYPE_DATE) && !basicAnswerType.equals(QuestionType.TYPE_LIST))) { throw new IllegalArgumentException("answerType " + answerType + " is invalid"); } this.answerType = answerType; if (!basicAnswerType.equals(previousBasicAnswerType)) { clearAnswer(); } } /** * Returns the basic answer type. * * @return */ public QuestionType getBasicAnswerType() { return answerType; } private QuestionType answerTypeToBasicAnswerType(String answerType) { if (answerType == null) { return null; } int i = answerType.indexOf('.'); if (i >= 0) { return QuestionType.valueOf(answerType.substring(0, i)); } return QuestionType.valueOf(answerType); } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public String getLastAnswer() { return lastAnswer; } @Modifies( "answered" ) public void setLastAnswer(String lastAnswer) { this.lastAnswer = lastAnswer; } public String getTextAnswer() { checkType(QuestionType.TYPE_TEXT); return textAnswer; } @Modifies( "answered" ) public void setTextAnswer(String textAnswer) { checkType(QuestionType.TYPE_TEXT); this.textAnswer = textAnswer; } public Long getNumberAnswer() { checkType(QuestionType.TYPE_NUMBER); return numberAnswer; } @Modifies( "answered" ) public void setNumberAnswer(Long numberAnswer) { checkType(QuestionType.TYPE_NUMBER); this.numberAnswer = numberAnswer; } public BigDecimal getDecimalAnswer() { checkType(QuestionType.TYPE_DECIMAL); return decimalAnswer; } @Modifies( "answered" ) public void setDecimalAnswer(BigDecimal decimalAnswer) { checkType(QuestionType.TYPE_DECIMAL); this.decimalAnswer = decimalAnswer; } public Boolean getBooleanAnswer() { checkType(QuestionType.TYPE_BOOLEAN); return booleanAnswer; } @Modifies( "answered" ) public void setBooleanAnswer(Boolean booleanAnswer) { checkType(QuestionType.TYPE_BOOLEAN); this.booleanAnswer = booleanAnswer; } public Date getDateAnswer() { checkType(QuestionType.TYPE_DATE); try { return dateAnswer == null ? null : getDateFormatter().parse(dateAnswer); } catch (ParseException e) { // can't actually happen because we formatted the string in the first place throw new IllegalStateException(); } } @Modifies( "answered" ) public void setDateAnswer(Date dateAnswer) { checkType(QuestionType.TYPE_DATE); this.dateAnswer = dateAnswer == null ? null : getDateFormatter().format(dateAnswer); } /** * For internal use only. * * @param dateAnswer * @throws java.text.ParseException */ @Modifies( "answered" ) public void setDateAnswer(String dateAnswer) throws ParseException { checkType(QuestionType.TYPE_DATE); this.dateAnswer = dateAnswer == null ? null : getDateFormatter().format(getDateFormatter().parse(dateAnswer)); } public String getListAnswer() { checkType(QuestionType.TYPE_LIST); return listAnswer; } @Modifies( "answered" ) public void setListAnswer(String listAnswer) { checkType(QuestionType.TYPE_LIST); this.listAnswer = listAnswer; } public List<String> getAnswerAsList() { checkType(QuestionType.TYPE_LIST); if (this.listAnswer == null) { return new ArrayList<String>(); } return Arrays.asList(split(this.listAnswer, ",")); } @Modifies( { "lastAnswer", "decimalAnswer", "numberAnswer", "textAnswer", "booleanAnswer", "listAnswer", "dateAnswer", "answered" } ) public void setAnswer(Object answer) { if (answerType == null) { throw new IllegalStateException("answerType has not been specified"); } QuestionType basicAnswerType = getBasicAnswerType(); setLastAnswer( (answer != null) ? answer.toString() : null); if (basicAnswerType.equals(QuestionType.TYPE_TEXT)) { setTextAnswer((String) answer); } if (basicAnswerType.equals(QuestionType.TYPE_NUMBER)) { if (answer != null) { setNumberAnswer(((Number) answer).longValue()); } else { setNumberAnswer(null); } } if (basicAnswerType.equals(QuestionType.TYPE_DECIMAL)) { setDecimalAnswer((BigDecimal) answer); } if (basicAnswerType.equals(QuestionType.TYPE_BOOLEAN)) { setBooleanAnswer((Boolean) answer); } if (basicAnswerType.equals(QuestionType.TYPE_DATE)) { setDateAnswer((Date) answer); } if (basicAnswerType.equals(QuestionType.TYPE_LIST)) { setListAnswer((String) answer); } } @Modifies( { "numberAnswer", "answered" } ) public void setAnswer(long l) { setNumberAnswer(l); } @Modifies( { "decimalAnswer", "answered" } ) public void setAnswer(double d) { setDecimalAnswer(new BigDecimal(d)); } @Modifies( { "booleanAnswer", "answered" } ) public void setAnswer(boolean b) { setBooleanAnswer(b); } public Object getAnswer() { if (answerType == null) { throw new IllegalStateException("answerType has not been specified"); } QuestionType basicAnswerType = getBasicAnswerType(); if (basicAnswerType.equals(QuestionType.TYPE_TEXT)) { return textAnswer; } if (basicAnswerType.equals(QuestionType.TYPE_NUMBER)) { return numberAnswer; } if (basicAnswerType.equals(QuestionType.TYPE_DECIMAL)) { return decimalAnswer; } if (basicAnswerType.equals(QuestionType.TYPE_BOOLEAN)) { return booleanAnswer; } if (basicAnswerType.equals(QuestionType.TYPE_DATE)) { return getDateAnswer(); } if (basicAnswerType.equals(QuestionType.TYPE_LIST)) { return listAnswer; } throw new IllegalStateException(); } public boolean isAnswered() { return getAnswer() != null; } @Modifies( { "lastAnswer", "decimalAnswer", "numberAnswer", "textAnswer", "booleanAnswer", "listAnswer", "dateAnswer", "answered" } ) public void fit(String answerValue, QuestionType basicAnswerType) throws NumberFormatException, ParseException { if (answerValue == null) { setAnswer(null); } else if (basicAnswerType.equals(QuestionType.TYPE_TEXT)) { setTextAnswer(answerValue); } else if (basicAnswerType.equals(QuestionType.TYPE_NUMBER)) { setNumberAnswer(new Long(answerValue)); } else if (basicAnswerType.equals(QuestionType.TYPE_DECIMAL)) { setDecimalAnswer(new BigDecimal(answerValue)); } else if (basicAnswerType.equals(QuestionType.TYPE_BOOLEAN)) { if ( "true".equalsIgnoreCase( answerValue ) || "false".equalsIgnoreCase( answerValue ) ) { setBooleanAnswer(new Boolean(answerValue)); } else { throw new ParseException("Unable to parse " + answerValue + " as boolean" , -1); } } else if (basicAnswerType.equals(QuestionType.TYPE_DATE)) { setDateAnswer(answerValue); } else if (basicAnswerType.equals(QuestionType.TYPE_LIST)) { setListAnswer(answerValue); } setLastAnswer(answerValue); } /** * Checks that the supplied answer type is correct. * * @param answerType */ private void checkType(QuestionType answerType) { if (this.answerType == null) { throw new IllegalStateException("answerType has not been specified"); } QuestionType basicAnswerType = getBasicAnswerType(); if (!basicAnswerType.equals(answerType)) { throw new IllegalStateException("Supplied answer type " + answerType + " differs from the expected type " + basicAnswerType + " for " + getId()); } } @Modifies( { "presentationStyles", "stylesList" } ) public void removePresentationStyle(String presentationStyle) { super.removePresentationStyle(presentationStyle); } @Modifies( { "presentationStyles", "stylesList" } ) public void addPresentationStyle(String presentationStyle) { super.addPresentationStyle(presentationStyle); } /** * Clears any previous answer (which may be of a different data type). */ @Modifies( { "lastAnswer", "decimalAnswer", "numberAnswer", "textAnswer", "booleanAnswer", "listAnswer", "dateAnswer", "answered" } ) private void clearAnswer() { textAnswer = null; numberAnswer = null; decimalAnswer = null; booleanAnswer = null; dateAnswer = null; listAnswer = null; lastAnswer = null; } /** * Splits some text into words delimited by the specified delimiter. Make public for use within rule logic. * * Occurrences of the delimiter d within the text are expected to be escaped as \d * * @param text * @param delimiter * @return */ public String[] split(String text, String delimiter) { List<String> result = new ArrayList<String>(); String[] split = text.split(delimiter, -1); for (int i = 0; i < split.length; i++) { } int i = 0; String s = ""; while (i < split.length) { boolean continues = split[i].endsWith("\\"); if (continues) { s += split[i].substring(0, split[i].length() - 1) + delimiter; } else { s += split[i]; result.add(s); s = ""; } i++; } return result.toArray(new String[] {}); } @Override public String toString() { return "Question{" + "preLabel='" + preLabel + '\'' + ", label='" + label + '\'' + ", postLabel='" + postLabel + '\'' + ", required=" + required + ", answerType=" + answerType + ", lastAnswer=" + lastAnswer + ", textAnswer='" + textAnswer + '\'' + ", numberAnswer=" + numberAnswer + ", decimalAnswer=" + decimalAnswer + ", booleanAnswer=" + booleanAnswer + ", dateAnswer='" + dateAnswer + '\'' + ", listAnswer='" + listAnswer + '\'' + "} " + super.toString(); } /** * Annotation used by the ChangeCollector to identify answer fields. */ @Retention(RUNTIME) @Target( { FIELD }) public @interface AnswerField { } }