/* * 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.load.questionnaire; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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.conditions.ConditionClause; import org.drools.informer.domain.questionnaire.conditions.PageElementCondition; import org.drools.informer.load.spreadsheet.SpreadsheetItem; import org.drools.informer.load.spreadsheet.SpreadsheetRow; import org.drools.informer.load.spreadsheet.sections.SpreadsheetSection; /** * Processes the Page element items for an Item Section on a Spreadsheet page (sheet), creating * the {@link PageElement} objects. Note that there can be multiple Tohu Pages (Group) * on a single Spreadsheet Page (Sheet). Pages can also be spread over multiple sheets (but * this will require specifying the appendAfter Page Id via the postLabel column). * * A Spreadsheet Page can contain multiple item sections (which will be attached to the same page * if no new page is specified). Note: until the first page is defined, only Global Impact type * elements can be defined (as they do not require any attachment to a Group as they are never * displayed in the UI). * * By using multiple ItemId columns, the nested depth of an element can be obtained, * and this is used to provide the nesting structure for groups and other objects. * * For very simple spreadsheets (single page), a page (Group) will be created * automatically, if one is not defined. * * Groups will be created automatically where required to handle nested questions. You * may want to list the group explicitly if you want to specify a label or style for it. * * Items will be created such that their inclusion depends on the parent group being * visible, which means that the Display Conditions are effectively propagated to all * child elements. * * Relates to Questionnaire type Spreadsheets. * * @author Derek Rendall */ public class ExtractItems implements SpreadsheetSectionConstants { private static final Logger logger = LoggerFactory.getLogger(ExtractItems.class); /** provides ability to lookup an element defined previously, if needed */ private Application application; protected Page currentPage; protected int currentDepth = 1; /** Used to grab the parent group (up a depth level) to attach items to etc */ private List<PageElement> currentElementsAtDepth = new ArrayList<PageElement>(); /** When backtracking up a depth need to know what the outer page was, in case just finished dealing with a Branch page */ private List<Page> currentPageAtDepth = new ArrayList<Page>(); /** Useful for name identification */ private String currentSheetName; public static final String ELEMENT_PAGE_UPPER = "PAGE"; public static final String ELEMENT_BRANCH_UPPER = "BRANCH"; /** * * @param application * @param currentPage * Useful in cases where there are multiple Item sections, as they will be * added to the "current" page. Will be null first time. */ public ExtractItems(Application application, Page currentPage) { super(); this.application = application; this.currentPage = currentPage; } /** * When we drop down a depth, we may need to create a group to contain the * elements at the new depth. * * @param element */ protected void createNormalIntermediateGroup(PageElement element) { PageElement currentParent = getElementAtDepth(currentDepth); PageElement newGroup = new PageElement(); String tempStr = "_CHILDREN"; logger.debug("Warning: creating new Intermediate Group: " + currentParent.getId()+tempStr + " current depth = " + currentDepth); newGroup.setId(currentParent.getId() + tempStr, currentDepth, element.getRowNumber()); newGroup.setType("Group"); if (!currentParent.isAGroupType()) { if (currentDepth == 1) { currentParent = currentPage.getParentPageElement(); } else { currentParent = getElementAtDepth(currentDepth - 1); } } currentPage.addElement(newGroup); currentParent.addChild(newGroup); currentElementsAtDepth.set(currentDepth - 1, newGroup); } /** * Store the element at the depth, so we can access it for assigning parent and * previous sibling information. Note: we can go down a couple of levels and then return to * adding more elements to a group. * * @param element */ protected void setElementAtDepth(PageElement element) { int depth = element.getDepth(); if ((depth < 1) || (depth > (currentElementsAtDepth.size() + 1))) { throw new IllegalArgumentException("Cannot set an element depth of " + String.valueOf(depth) + " when depth tree size is " + String.valueOf(currentElementsAtDepth.size())); } if (element.isAnImpactType()) { if (currentPage == null) { application.addGlobalElement(element); return; } if (depth == 1) { currentPage.getParentPageElement().addChild(element); } else { getElementAtDepth(depth - 1).addChild(element); } currentPage.addElement(element); return; } // now check to see if we need to add an automatic group // Note: depth == 1 will mean that there is a page group created, therefore no problem if ((depth > 1) && (depth > currentDepth) && (!getElementAtDepth(currentDepth).isAGroupType()) && (!element.isABranchedPage())) { //logger.debug("Item at current depth: " + getElementAtDepth(currentDepth).getId()); createNormalIntermediateGroup(element); } if (!element.isAPageElement() && (currentElementsAtDepth.size() == 0)) { //logger.debug("About to create a default master page"); PageElement masterElement = new PageElement(); masterElement.setId("DefaultPage", 0, 0); masterElement.setType("Page"); currentPage = new Page(currentSheetName, masterElement, currentPage); application.addPage(currentPage); } else if (element.isAPageElement()) { currentPage = new Page(currentSheetName, element, currentPage); application.addPage(currentPage); } // Add it into the right position on the working lists if (depth > currentElementsAtDepth.size()) { //logger.debug("Setting element " + element.getId() + " to depth " + currentElementsAtDepth.size() + 1); currentElementsAtDepth.add(element); currentPageAtDepth.add(currentPage); } else { //logger.debug("Setting element " + element.getId() + " at depth " + depth); currentElementsAtDepth.set(depth - 1, element); for (int i = (currentElementsAtDepth.size() - 1); i >= depth ; i--) { currentElementsAtDepth.remove(i); currentPageAtDepth.remove(i); } if (element.isAPageElement()) { currentPageAtDepth.set(depth - 1, currentPage); } else { currentPage = currentPageAtDepth.get(depth - 1); } } // Deal with linking elements on the page if (!element.isAPageElement()) { currentPage.addElement(element); PageElement tempElement = (depth == 1) ? currentPage.getParentPageElement() : getElementAtDepth(depth - 1); tempElement.addChild(element); } currentDepth = element.getDepth(); } /** * Depth starts at 1, so will access list at index of depth - 1 * * @param depth * @return */ protected PageElement getElementAtDepth(int depth) { if ((depth < 1) || (depth > currentElementsAtDepth.size())) { return null; } return currentElementsAtDepth.get(depth - 1); } /** * Will process a line of the spreadsheet. It will either be a new element, or a continuation of a * condition on the display/value of the previous element. In the latter case the {@link ConditionClause} * will be extracted from the current element (line) and added to the previous (real) element. * * @param section * @return * The current page, which may be a newer one than the one passed in. Callers * can then pass into the next Item Section to be processed. */ public Page processSectionData(SpreadsheetSection section) { List<SpreadsheetRow> rows = section.getSectionRows(); currentSheetName = section.getSheetName(); PageElement lastRealElement = null; for (Iterator<SpreadsheetRow> rowIter = rows.iterator(); rowIter.hasNext();) { SpreadsheetRow spreadsheetRow = (SpreadsheetRow) rowIter.next(); if (spreadsheetRow.getRowItems().size() == 0) { continue; } //logger.debug("Processing row " + spreadsheetRow.getRowNumber()); PageElement element = extractPageElement(section, spreadsheetRow); //logger.debug("Processing line " + spreadsheetRow.getRowNumber() + " item id " + element.getId() + " depth " + String.valueOf(element.getDepth())); if (element.getId() != null){ // ie not a display fact or impact extension setElementAtDepth(element); lastRealElement = element; } // Now deal with condition facts if ((element.getLogicElement() != null) && (!element.getLogicElement().isProcessed())) { if (lastRealElement.isAnImpactType()) { if (element.getId() != null) { if (element.getLogicElement() == null) { throw new IllegalArgumentException("You must specify a logic clause on an Impact " + element.getId()); } // Will turn this into a global by not requiring the parent to be visible element.setRequired("Yes"); } processConditionClauseLine(lastRealElement, element, spreadsheetRow.getRowNumber()); } else if (lastRealElement.isAValidationElement()) { processValidationClauseLine(lastRealElement, element, spreadsheetRow.getRowNumber()); } else { processConditionClauseLine(lastRealElement, element, spreadsheetRow.getRowNumber()); } } } return currentPage; } /** * Maps the row cells to values in the {@link PageElement} object. Uses the section heading row to identify * what each column (attribute) each cell represents. Order of columns is arbitrary, other than having * an Item Id as the first column. * * If an Impact type has no logic associated, the code will look for a parent with logic, to control * the creation/assigning of the logic for the impact. This results in writing the logic once. * * @param section * @param row * @return */ protected PageElement extractPageElement(SpreadsheetSection section, SpreadsheetRow row) { SpreadsheetRow headings = section.getHeaderRow(); PageElement element = new PageElement(); for (Iterator<SpreadsheetItem> iterator = row.getRowItems().iterator(); iterator.hasNext();) { SpreadsheetItem item = (SpreadsheetItem) iterator.next(); String key = headings.getHeaderTextForColumnInUpperCase(item.getColumn()); if (key == null) { // Comment item - ignore logger.debug("Ignoring value: " + item); continue; } String value = item.toString(); if (key.startsWith(PAGE_ITEMS_UPPER)) { element.setId(value, section.getHeaderDepthForColumn(item.getColumn()), item.getRow()); continue; } if (key.startsWith("SET")) { element.setDefaultValueStr(value); continue; } if (key.startsWith("STYLE")) { element.addStyle(value); continue; } if (key.startsWith("TYPE")) { element.setType(value); continue; } if (key.startsWith("REQUIRE")) { element.setRequired(value); continue; } if (key.startsWith("DATA")) { element.setFieldType(value); continue; } if (key.startsWith("PRE")) { element.setPreLabel(value); continue; } if (key.startsWith("POST")) { element.setPostLabel(value); continue; } if (key.startsWith("SELECTION")) { element.setLookupTableId(value); continue; } if (key.startsWith("CATEGORY")) { element.setCategory(value); continue; } if (key.startsWith("DEPENDS")) { element.setLogicDependsOnItemId(value); continue; } if (key.startsWith("ATTRIBUTE")) { element.setLogicAttribute(value); continue; } if (key.startsWith("OPERATION")) { element.setLogicOperation(value); continue; } if (key.startsWith("VALUE")) { element.setLogicValue(value); continue; } logger.debug("Unknown Section key: " + key); } if ((element.getId() != null) && (element.getType() == null)) { throw new IllegalArgumentException("Row " + String.valueOf(row.getRowNumber() + 1) + " has no type!"); } if ((element.getType() != null) && (element.isAnImpactType()) && (element.getLogicElement() == null) && (currentPage != null)) { int depth = currentDepth; try { while (depth > 0) { PageElement temp = getElementAtDepth(depth); if ((temp != null) && (temp.getLogicElement() != null)) { element.setDisplayCondition((PageElementCondition)temp.getDisplayCondition().clone()); element.setLogicElement(temp.getLogicElement()); depth = 0; } depth--; } if (element.getLogicElement() == null) { if (currentPage.getParentPageElement().getLogicElement() != null) { element.setDisplayCondition((PageElementCondition)currentPage.getParentPageElement().getDisplayCondition().clone()); element.setLogicElement(currentPage.getParentPageElement().getLogicElement()); } else { throw new IllegalArgumentException("Row " + String.valueOf(row.getRowNumber() + 1) + " has a impact with no condition or parent with a condition!"); } } } catch (CloneNotSupportedException e) { e.printStackTrace(); throw new IllegalStateException(e.getMessage()); } } return element; } /** * Manage the logic associated with a Validation line. * * @param masterElement * If the current Element has an Item Id, then this will be the current element. Otherwise will be the * previous real element - the one we want to attach the {@link ConditionClause} to. * @param element * Contains the {@link ConditionClause} that we need to create or add to the {@link PageElementCondition}. * @param row * Will be used to create rules that are uniquely named. */ protected void processValidationClauseLine(PageElement masterElement, PageElement element, int row) { // TODO handle repeated elements? ConditionClause le = element.getLogicElement(); if (element.getId() != null) { // first line String type = PageElementCondition.TYPE_VALIDATION; masterElement.setDisplayCondition(new PageElementCondition(type, masterElement.getId(), row)); } masterElement.getDisplayCondition().addElement(le); le.setProcessed(true); } /** * Manage the logic associated with a Non-validation related conditional line. If the * logic relates to a Page or a Branch then a special version of {@link PageElementCondition} is * created, containing relevant page information for creating the relevant rules. * * @param masterElement * If the current Element has an Item Id, then this will be the current element. Otherwise will be the * previous real element - the one we want to attach the {@link ConditionClause} to. * @param element * Contains the {@link ConditionClause} that we need to create or add to the {@link PageElementCondition}. * @param row * Will be used to create rules that are uniquely named - especially for AlternateImpact. */ protected void processConditionClauseLine(PageElement masterElement, PageElement element, int row) { // TODO handle repeated elements? ConditionClause le = element.getLogicElement(); if (element.getId() != null) { // first line String type = PageElementCondition.TYPE_INCLUSION; if (masterElement.isAPageElement()) { //logger.debug("Processing page displayFact: " + value); masterElement.setDisplayCondition(new PageElementCondition(type, masterElement.getId(), row, currentPage.getId(), currentPage.isBranchedPage(), currentPage.getDisplayAfter())); } else if (masterElement.isAnAlternateImpactItem()) { le.setExplanation(element.getPostLabel()); masterElement.setDisplayCondition(new PageElementCondition(type, masterElement.getId() + String.valueOf(row), row)); } else { masterElement.setDisplayCondition(new PageElementCondition(type, masterElement.getId(), row)); } } masterElement.getDisplayCondition().addElement(le); le.setProcessed(true); } }