/* * 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.write.questionnaire; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.*; import org.drools.informer.util.TemplateManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.drools.informer.Answer; import org.drools.informer.domain.questionnaire.Application; import org.drools.informer.domain.questionnaire.Page; import org.drools.informer.domain.questionnaire.PageElement; import org.drools.informer.domain.questionnaire.framework.ConditionConstants; import org.drools.informer.domain.questionnaire.framework.ListEntryTuple; import org.drools.informer.domain.questionnaire.framework.PageElementConstants; import org.drools.informer.write.questionnaire.helpers.FieldTypeHelper; /** * Write out an {@link PageElement} object to the {@link Page} drl file. * * See {@link PageElement} for details on the types written. * * Conditional display is propagated by each item having (at least) a condition that * their parent group is displayed. * * If a Group has items that do not exist they are ignored. This removed the need for explicit * insert/remove code for page elements and other group items. One exception is for Branch pages * where there needs to be additional logic to control the jump off to a new set of pages. Note: the page's group * is enabled via the same logic as the rule that creates a new navigationBranch. This means that the group, and the * associated data, can exist after the branch is returned from. The navigationBranch does not * require any special "return" logic as that is handled automatically by Tohu framework. * * Additional logic is also written for Functional and Alternate Impact (see {@link }) elements. * A functional impact is actually like a global fact that uses an accumulate function (sum, average, etc) to * provide the value. * * An alternate function can have multiple elements, each one setting a mutually exclusive value. This means that the * actual object needs to be created (once) and each element simply updates the value. Note: the first * element for the Alternate Impact is used to specify the default value. * * @author Derek Rendall */ public class PageElementTemplate implements PageElementConstants, ConditionConstants { private static final Logger logger = LoggerFactory.getLogger(PageElementTemplate.class); protected PageElement element; public PageElementTemplate(PageElement element) { super(); this.element = element; } /** * Impact objects are actually {@link } objects. * * @param itemId * @return */ protected String checkType(String itemId) { if (itemId.equals(ITEM_TYPE_NORMAL_IMPACT)) { return ITEM_TYPE_DATA_ITEM; } return itemId; } /** * Accumulate functions handled: * <ul> * <li><code>max</code></li> * <li><code>min</code></li> * <li><code>sum</code></li> * <li><code>average</code></li> * <li><code>count</code></li> * </ul> * * @param application * @param fmt * @throws IOException */ protected void writeFunctionalImpact(Application application, TemplateManager tm, Formatter fmt) { // TODO replace Logic Element with PageElementCondition if ((element.getLogicElement() == null)) { throw new IllegalArgumentException("You cannot have an empty logical element for a functional impact!"); } String functionName = null; String opName = element.getLogicElement().getOperation(); if (opName == null) { throw new IllegalArgumentException("You cannot have an empty logical operation for a functional impact!"); } if (opName.equalsIgnoreCase(FUNCTION_MAX)) { functionName = FUNCTION_MAX; } else if (opName.equalsIgnoreCase(FUNCTION_MIN)) { functionName = FUNCTION_MIN; } else if (opName.equalsIgnoreCase(FUNCTION_AVERAGE)) { functionName = FUNCTION_AVERAGE; } else if (opName.equalsIgnoreCase(FUNCTION_SUM)) { functionName = FUNCTION_SUM; } else if (opName.equalsIgnoreCase(FUNCTION_COUNT)) { functionName = FUNCTION_COUNT; } if (functionName == null) { throw new IllegalArgumentException("Invalid operation " + opName + " for a functional impact!"); } writeCreationOfAGlobalImpact(application, tm, fmt); String tempFactName = "Temp" + element.getId(); fmt.format("declare %s\n", tempFactName); fmt.format("\tnumber : Number\n"); fmt.format("end\n\n"); fmt.format("rule \"Function %s\"\nno-loop\n", element.getId()); fmt.format("when\n"); fmt.format("\t$total : Number()\n"); fmt.format("\t\tfrom accumulate (%s(%s == \"%s\", answered == true, $value : %s),\n %s ( $value ) )\n", checkType(element.getLogicElement().getItemId()), element.getLogicElement().getItemAttribute(), element.getLogicElement().getValue(), FieldTypeHelper.mapFieldTypeToBaseVariableName(element.getFieldType()), functionName); fmt.format("then\n"); fmt.format("\t%s temp = new %s();\n", tempFactName, tempFactName); fmt.format("\ttemp.setNumber($total);\n"); fmt.format("\tinsert(temp);\n"); fmt.format("end\n\n"); fmt.format("rule \"Assign %s\"\nno-loop\n", element.getId()); fmt.format("when\n"); fmt.format("\t$impact : %s(id == \"%s\");\n", ITEM_TYPE_DATA_ITEM, element.getId()); fmt.format("\t$v : %s();\n", tempFactName); fmt.format("then\n"); fmt.format("\t$impact.setAnswer(new %s($v.getNumber().%s));\n", FieldTypeHelper.mapFieldTypeToJavaClassName(element.getFieldType()), FieldTypeHelper.mapFieldTypeToJavaNumberClassMethodName(element.getFieldType())); fmt.format("\tretract($v);\n"); fmt.format("\tupdate($impact);\n"); fmt.format("end\n\n"); } /** * Write the common attribute setting code for a Tohu related fact * * @param application * @param fmt * @param showReason * @return * @throws IOException */ protected String writeCommonFactCreationCode(Application application, TemplateManager tm, Formatter fmt, boolean showReason) { String varName = "a" + element.getType(); HashMap<String,Object> map = new HashMap<String,Object>(); map.put("varName", varName); map.put("ansType",FieldTypeHelper.mapFieldTypeToQuestionType(element.getFieldType())); map.put("ansValue",FieldTypeHelper.formatValueStringAccordingToType(element.getDefaultValueStr(), element.getFieldType())); map.put("showReason",showReason); map.put("method",(element.isAQuestionType()) ? "PreLabel" : (element.isAnImpactType()) ? "Name" : "Label"); tm.applyTemplate("commonFactCreation.drlt",element,map,fmt); return varName; } /** * Insert an Impact (a {@link }) * * @param application * @param fmt * @throws IOException */ protected void writeCreationOfAGlobalImpact(Application application, TemplateManager tm, Formatter fmt) { ByteArrayOutputStream subBaos = new ByteArrayOutputStream(); Formatter slave = new Formatter(subBaos); String variableName = writeCommonFactCreationCode(application, tm, slave, false); HashMap<String,Object> map = new HashMap<String,Object>(); map.put("varName",variableName); map.put("body",new String(subBaos.toByteArray())); tm.applyTemplate("globalImpact.drlt",element,map,fmt); } /** * Conditional rule to logically insert an InvalidAnswer object attached to the previous question. * * @param application * @param fmt * @throws IOException */ public void writeValidationDRL(Application application, TemplateManager tm, Formatter fmt) { String body; String message; PageElement baseQuestion; body = new WhenClauseTemplate(element).writeLogicSectionDRL(application, false); message = element.getPreLabel(); element.setPreLabel(null); if (message == null) { message = "Invalid value"; logger.debug("Warning - validation " + element.getId() + " has no validation message defined."); } baseQuestion = element.findPreviousQuestion(); if (baseQuestion == null) { throw new IllegalStateException("Validation " + element.getId() + " has no previous question to attach to."); } HashMap<String,Object> map = new HashMap<String,Object>(); map.put("body",body); map.put("message",message); map.put("baseId",baseQuestion.getId()); tm.applyTemplate("validation.drlt",element,map,fmt); } /** * For a group or a multiple choice question, write out the items. * * If it is a multiple choice question, then list entries that have conditional logic are NOT written here. * * Note: it is valid to have no list specified for a Multiple List Question, if the list is being set * by other means (such as via logic in one of the include files loaded into the Questionnaire drl). * * @param application * @param fmt * @param possibleAnswers * @throws IOException */ protected void writeSubItems(Application application, TemplateManager tm, Formatter fmt, String variableName, boolean possibleAnswers) { if (possibleAnswers && ((element.getLookupTable() == null) || (element.getLookupTable().getEntries().size() == 0))) { // This is ok for a Lookup Object logger.debug("No entries for " + element.getId()); return; } List<ListEntryTuple> entries = null; if (possibleAnswers) { entries = element.getLookupTable().getEntries(); } else { if ((element.getChildren() == null) || (element.getChildren().size() == 0)) { throw new IllegalStateException("No children for group " + element.getId()); } entries = new ArrayList<ListEntryTuple>(); for (Iterator<PageElement> i = element.getChildren().iterator(); i.hasNext();) { PageElement e = (PageElement) i.next(); if (e.isARepeatingElement()) { PageElement temp = application.findPageElement(e.getId()); if (temp == null) { throw new IllegalArgumentException("A repeating element has no master element for id " + e.getId()); } e = temp; } if (e.isAGroupType() || e.isAQuestionType() || e.isANoteType()) { entries.add(new ListEntryTuple(e.getId())); } } if (entries.size() == 0) { throw new IllegalStateException("No group, note or question children for group " + element.getId()); } } HashMap<String,Object> map = new HashMap<String,Object>(); map.put("possibleAnswers",possibleAnswers); map.put("entries",entries); map.put("varName",variableName); tm.applyTemplate("subItems.drlt",element,map,fmt); } /** * In order to action a branch, the logic looks for an actual {@link Answer} object associated with the * Question. This only exists straight after the question value has changed, thus can be used to initiate the * branch. For the Branch page's group, use the question's answer attribute as this will remain accessible * after the Answer object has gone away. * * @param application * @param fmt * @throws IOException */ protected void writeInitiateBranchPageDRL(Application application, TemplateManager tm, Formatter fmt) { if (!element.isABranchedPage()) throw new IllegalArgumentException("Cannot process a normal page in writeInitiateBranchPageDRLFileContents :" + element.getId()); String premise = new WhenClauseTemplate(element).writeLogicSectionDRL(application, true); HashMap<String,Object> map = new HashMap<String,Object>(); map.put("premise",premise); map.put("displayAfter", element.getPostLabel()); element.setPostLabel(null); tm.applyTemplate("branching.drlt",element,map,fmt); } /** * The entry point for writing the element to file * * @param application * @param fmt * @throws IOException */ public void compileContentsToDRL(Application application, TemplateManager tm, Formatter fmt) { if (element.isARepeatingElement()) { logger.debug("Repeating item: " + element.getId()); // should have already been defined - don't want it defined again - just // referred to again in the parent element, which should have already been done return; } if (element.isAFunctionImpactItem()) { logger.debug("Functional Impact"); writeFunctionalImpact(application, tm, fmt); return; } if (element.isAnAlternateImpactItem()) { logger.debug("Alternate Impact"); if (application.addNewAlternateImpact(element.getId())) writeCreationOfAGlobalImpact(application, tm, fmt); } else if (element.isAnImpactType() && (element.getLogicElement() == null)) { writeCreationOfAGlobalImpact(application, tm, fmt); return; } else if (element.isAValidationElement()) { writeValidationDRL(application, tm, fmt); return; } if (element.isABranchedPage()) { writeInitiateBranchPageDRL(application, tm, fmt); } String ruleName = element.getId(); if (element.isAnAlternateImpactItem()) { ruleName = ruleName + String.valueOf(element.getRowNumber()); } fmt.format("rule \"%s\"\ndialect \"mvel\"\nno-loop\n", ruleName); boolean useGroupIds = ((element.getGroupIds() != null) && (element.getGroupIds().size() > 0)) && (element.isAQuestionType() || !element.isRequired()); if (useGroupIds || (element.getDisplayCondition() != null)) { fmt.format("when\n"); if (useGroupIds) { fmt.format("\t$group : Group ("); for (Iterator<String> i = element.getGroupIds().iterator(); i.hasNext();) { String id = (String) i.next(); if (!id.startsWith("\"")) { id = "\"" + id + "\""; } fmt.format("id == %s%s", id, i.hasNext() ? " || " : ""); } fmt.format(");\n"); } if (element.getDisplayCondition() != null) { new WhenClauseTemplate(element).writeLogicSectionDRL(application, false); } } if (element.isAnAlternateImpactItem()) { fmt.format("\taDataItem : %s (id == \"%s\")\n", ITEM_TYPE_DATA_ITEM, element.getId()); fmt.format("then\n"); String tempStr = FieldTypeHelper.formatValueStringAccordingToType(element.getDefaultValueStr(), element.getFieldType()); fmt.format("\taDataItem.setAnswer(%s);\n", tempStr); fmt.format("\tupdate(aDataItem);\n"); } else { fmt.format("then\n"); String variableName = writeCommonFactCreationCode(application, tm, fmt, true); if (!element.isAnImpactType()) { if (element.getPostLabel() != null) { fmt.format("\t%s.setPostLabel(\"%s\");\n", variableName, element.getPostLabel()); } if ((element.getType().equals(ITEM_TYPE_MULTI_CHOICE_Q)) || (element.getType().equals(ITEM_TYPE_GROUP))) { writeSubItems(application, tm, fmt, variableName, element.getType().equals(ITEM_TYPE_MULTI_CHOICE_Q)); } if (!element.getStyles().isEmpty()) { boolean firstOne = true; boolean onlyOne = element.getStyles().size() == 1; String indent = (onlyOne) ? "" : "\t\t"; String newLine = (onlyOne) ? "" : "\n"; fmt.format("\t%s.setPresentationStyles({", variableName); for (Iterator<String> i = element.getStyles().iterator(); i.hasNext();) { String style = (String) i.next(); if (firstOne) { firstOne = false; fmt.format("%s", newLine); } else { fmt.format(",%s", newLine); } if ((style != null) && (!style.startsWith("\""))) { style = "\"" + style + "\""; } fmt.format("%s%s", indent, style); } fmt.format("});\n"); } } fmt.format("\tinsertLogical(%s);\n", variableName); } fmt.format("end\n\n"); } }