package org.akaza.openclinica.view.form;
import org.akaza.openclinica.bean.managestudy.StudyBean;
import org.akaza.openclinica.bean.submit.*;
import org.akaza.openclinica.control.managestudy.BeanFactory;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.ProcessingInstruction;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
import javax.xml.transform.Result;
/**
* This class generates a horizontal HTML table with multiple columns and
* headers.
*/
public class HorizontalFormBuilder extends DefaultFormBuilder {
// The object that will provide the content for the table's headers and
// cells.
// This is a List of group- or matrix-type tables. The List orders the
// tables on the section
// according to the ordinal of each DisplayItemGroupBean's
// ItemGroupMetadataBean.
private List<DisplayItemGroupBean> displayItemGroups;
// Used for displaying the section title, subtitle, and instructions,
// if necessary
private SectionBean sectionBean;
private StudyBean studyBean;
private EventCRFBean eventCRFbean;
// is the form a ViewSectionDataEntry form?
private boolean isDataEntry;
// Have the form values already been saved during initial or doubledata
// entry?
private boolean hasDbFormValues;
// A value that's needed to seed the tabindex values
private int tabindexSeed;
public HorizontalFormBuilder() {
this.displayItemGroups = new ArrayList<DisplayItemGroupBean>();
this.tabindexSeed = 1;
this.hasDbFormValues = false;
}
// The list of DisplayFormGroupBeans that provide the content for an XHTML
// table
public List<DisplayItemGroupBean> getDisplayItemGroups() {
// Threadsafe form of accessor method
return new ArrayList<DisplayItemGroupBean>(displayItemGroups);
}
public void setDisplayItemGroups(List<DisplayItemGroupBean> displayItems) {
this.displayItemGroups = displayItems;
}
public SectionBean getSectionBean() {
return sectionBean;
}
public void setSectionBean(SectionBean sectionBean) {
this.sectionBean = sectionBean;
}
@Override
public String createMarkup() {
// If the CRF is involved with ViewDataEntry and already has
// data associated with it, pass on the responsibility to another object
ViewPersistanceHandler persistanceHandler = new ViewPersistanceHandler();
if (isDataEntry) {
List<ItemDataBean> itemDataBeans;
persistanceHandler = new ViewPersistanceHandler();
itemDataBeans = persistanceHandler.fetchPersistedData(sectionBean.getId(), eventCRFbean.getId());
if (!itemDataBeans.isEmpty()) {
hasDbFormValues = true;
}
persistanceHandler.setItemDataBeans(itemDataBeans);
}
// Keep track of whether a group has any repeat behavior; true or false
boolean repeatFlag;
//Should the table have dark borders?
boolean hasBorders=false;
if(sectionBean != null){
hasBorders= (sectionBean.getBorders() > 0);
}
// The CellFactory object that generates the content for HTML table TD
// cells.
CellFactory cellFactory = new CellFactory();
RepeatManager repeatManager = new RepeatManager();
FormBeanUtil formUtil = new FormBeanUtil();
ViewBuilderUtil builderUtil = new ViewBuilderUtil();
// The number of repeating table rows that the group will start with.
int repeatNumber;
// the div tag that will be the root
Element divRoot = new Element("div");
divRoot.setAttribute("id", "tableRoot");
Document doc = new Document();
ProcessingInstruction pi = new ProcessingInstruction(Result.PI_DISABLE_OUTPUT_ESCAPING, "");
doc.addContent(pi);
doc.setRootElement(divRoot);
// Show the section's title, subtitle, or instructions
builderUtil.showTitles(divRoot, this.getSectionBean());
// One way to generate an id for the repeating tbody or tr element
int uniqueId = 0;
// The tabindex attribute for select and input tags
int tabindex = tabindexSeed;
boolean hasDiscrepancyMgt = false;
StudyBean studBean = this.getStudyBean();
if (studBean != null && studBean.getStudyParameterConfig().getDiscrepancyManagement().equalsIgnoreCase("true")) {
hasDiscrepancyMgt = true;
}
// Create a table for every DisplayItemGroupBean
// A DisplayItemGroupBean contains an ItemGroupBean and
// its list of DisplayItemBeans
for (DisplayItemGroupBean displayItemGroup : this.displayItemGroups) {
List<DisplayItemBean> currentDisplayItems = displayItemGroup.getItems();
// A Map that contains persistent (stored in a database), repeated
// rows
// in a matrix type table
// The Map index is the Item id of the first member of the row; the
// value is a List
// of item beans that make up the row
SortedMap<Integer, List<ItemDataBean>> ordinalItemDataMap = new TreeMap<Integer, List<ItemDataBean>>();
// Is this a persistent matrix table and does it already have
// repeated rows
// in the database?
boolean hasStoredRepeatedRows = false;
boolean unGroupedTable = displayItemGroup.getItemGroupBean().getName().equalsIgnoreCase(BeanFactory.UNGROUPED) || !displayItemGroup.getGroupMetaBean().isRepeatingGroup();
// Load any database values into the DisplayItemBeans
if (hasDbFormValues) {
currentDisplayItems = persistanceHandler.loadDataIntoDisplayBeans(currentDisplayItems, (!unGroupedTable));
/*
* The highest number ordinal represents how many repeated rows
* there are. If the ordinal in ItemDataBeans > 1, then we know
* that the group has persistent repeated rows. Get a structure
* that maps each ordinal (i.e., >= 2) to its corresponding List
* of ItemDataBeans. Then iterate the existing DisplayBeans,
* with the number of new rows equaling the highest ordinal
* number minus 1. For example, in a List of ItemDataBeans, if
* the highest ordinal property among these beans is 5, then the
* matrix table has 4 repeated rows from the database. Provide
* each new row with its values by using the ItemDataBeans.
*/
if (!unGroupedTable && persistanceHandler.hasPersistentRepeatedRows(currentDisplayItems)) {
hasStoredRepeatedRows = true;
// if the displayitems contain duplicate item ids, then
// these duplicates
// represent repeated rows. Separate them into a Map of new
// rows that
// will be appended to the HTML table.
ordinalItemDataMap = persistanceHandler.handleExtraGroupRows();
}
}// end if hasDbFormValues
// Does the table have a group header?
String groupHeader = displayItemGroup.getGroupMetaBean().getHeader();
boolean hasGroupHeader = groupHeader != null && groupHeader.length() > 0;
// Add group header, if there is one
if (hasGroupHeader) {
Element divGroupHeader = new Element("div");
// necessary?
divGroupHeader.setAttribute("class", "aka_group_header");
Element strong = new Element("strong");
strong.setAttribute("style", "float:none");
strong.addContent(groupHeader);
divGroupHeader.addContent(strong);
divRoot.addContent(divGroupHeader);
}
// This group represents "orphaned" items (those without a group) if
// the FormGroupBean has a group label of UNGROUPED
Element orphanTable = null;
if (unGroupedTable) {
orphanTable = formUtil.createXHTMLTableFromNonGroup(currentDisplayItems, tabindex, hasDiscrepancyMgt, hasDbFormValues, false);
// We have to track the point the tabindex has reached here
// The tabindex will increment by the size of the
// displayItemGroup List
tabindex += currentDisplayItems.size();
divRoot.addContent(orphanTable);
continue;
}// end if unGroupedTable
uniqueId++;
String repeatParentId = "repeatParent" + uniqueId;
repeatNumber = displayItemGroup.getGroupMetaBean().getRepeatNum();
// If the form has repeat behavior, this number is > 0
// Do not allow repeat numbers < 1
// repeatNumber = repeatNumber < 1 ? 1 : repeatNumber;
// And a limit of 12
repeatNumber = repeatNumber > 12 ? 12 : repeatNumber;
// This is always true during this iteration
repeatFlag = displayItemGroup.getGroupMetaBean().isRepeatingGroup();
Element table = createTable();
// add the thead element
Element thead = this.createThead();
table.addContent(thead);
divRoot.addContent(table);
// Add the first row for the th tags
Element thRow = new Element("tr");
thead.addContent(thRow);
// Does this group involve a Horizontal checkbox or radio button?
boolean hasResponseLayout =
builderUtil.hasResponseLayout(currentDisplayItems);
// add th elements to the thead element
// We have to create an extra thead column for the Remove Row
// button, if
// the table involves repeating rows
List<Element> thTags =
repeatFlag ? createTheadContentsFromDisplayItems(currentDisplayItems,
true, hasBorders) : createTheadContentsFromDisplayItems(currentDisplayItems, false, hasBorders);
for (Element el : thTags) {
thRow.addContent(el);
}
if (hasResponseLayout) {
Element thRowSubhead = new Element("tr");
thead.addContent(thRowSubhead);
addResponseLayoutRow(thRowSubhead, currentDisplayItems, hasBorders);
}
// Create the tbody tag
Element tbody;
Element row;
Element td;
tbody = this.createTbody();
// The table adds the tbody to the XML or markup
table.addContent(tbody);
// For each row in the table
// for (int i = 1; i <= repeatNumber; i++) {
row = new Element("tr");
// If the group has repeat behavior and repeats row by row,
// then the
// repetition model type attributes have to be added to the tr tag
int repeatMax = displayItemGroup.getGroupMetaBean().getRepeatMax();
// Make sure repeatMax >= 1
repeatMax = repeatMax < 1 ? 40 : repeatMax;
if (repeatFlag && !(isDataEntry && hasStoredRepeatedRows)) {
row = repeatManager.addParentRepeatAttributes(row, repeatParentId, repeatNumber, repeatMax);
}
// The content for the table cells. For each item...
for (DisplayItemBean displayBean : currentDisplayItems) {
// What type of input: text, radio, checkbox, etc.?
String responseName = displayBean.getMetadata().getResponseSet().getResponseType().getName();
// We have to create cells in a different way if the input
// is radio or checkbox, and the response_layout is horizontal
if (displayBean.getMetadata().getResponseLayout().equalsIgnoreCase("horizontal")
&& (responseName.equalsIgnoreCase("checkbox") || responseName.equalsIgnoreCase("radio"))) {
Element[] elements =
cellFactory.createCellContentsForChecks(
responseName, displayBean, displayBean.getMetadata().getResponseSet().getOptions().size(),
++tabindex, false, false);
for (Element el : elements) {
el = builderUtil.setClassNames(el);
if(hasBorders) {
this.createDarkBorders(el);
}
if (repeatFlag) {
el = repeatManager.addChildRepeatAttributes(el, repeatParentId, displayBean.getItem().getId(), null);
}
row.addContent(el);
}
// move to the next item
continue;
}
td = new Element("td");
td = builderUtil.setClassNames(td);
if(hasBorders) {
this.createDarkBorders(td);
}
// Create cells within each row
td = cellFactory.createCellContents(td, responseName, displayBean, ++tabindex, hasDiscrepancyMgt, hasDbFormValues, false);
if (repeatFlag) {
td = repeatManager.addChildRepeatAttributes(td, repeatParentId, displayBean.getItem().getId(), null);
}
row.addContent(td);
}// end for displayBean
// We need an extra cell for holding the "Remove Row" button
if (repeatFlag) {
builderUtil.addRemoveRowControl(row, repeatParentId, hasBorders);
}
tbody.addContent(row);
// }//end for every row
if (hasStoredRepeatedRows) {
List<Element> storedRepeatedRows =
builderUtil.generatePersistentMatrixRows(ordinalItemDataMap,
currentDisplayItems, tabindex, repeatParentId, hasDiscrepancyMgt, false, hasBorders);
// add these new rows to the table
for (Element newRow : storedRepeatedRows) {
tbody.addContent(newRow);
}
}
// Create a row for the Add Row button, if the group includes any
// repeaters
if (repeatFlag) {
builderUtil.createAddRowControl(tbody, repeatParentId, (builderUtil.calcNumberofColumns(displayItemGroup) + 1), hasBorders);
}
}// end for displayFormGroup
XMLOutputter outp = new XMLOutputter();
Format format = Format.getPrettyFormat();
format.setOmitDeclaration(true);
outp.setFormat(format);
Writer writer = new StringWriter();
try {
outp.output(doc, writer);
} catch (IOException e) {
e.printStackTrace();
}
return writer.toString();
}
private void addResponseLayoutRow(Element thRow,
List<DisplayItemBean> displayBeans, boolean hasDarkBorder) {
String responseName;
String responseLayout;
ItemFormMetadataBean metaBean;
// Now create the th row
Element th2;
ResponseSetBean respBean;
ResponseOptionBean optBean;
for (DisplayItemBean dBean : displayBeans) {
metaBean = dBean.getMetadata();
respBean = metaBean.getResponseSet();
responseName = respBean.getResponseType().getName();
if (responseName == null) {
responseName = "";
}
responseLayout = metaBean.getResponseLayout();
if (responseLayout == null) {
responseLayout = "";
}
// You could have a radio or checkbox whose layout is *not*
// horizontal,
// next to a rad or check with a hor layout.
if ((responseName.equalsIgnoreCase("radio") || responseName.equalsIgnoreCase("checkbox")) && responseLayout.equalsIgnoreCase("horizontal")) {
for (int i = 0; i < respBean.getOptions().size(); i++) {
optBean = (ResponseOptionBean) respBean.getOptions().get(i);
if (optBean != null) {
th2 = createThCell(optBean.getText(), 1);
if(hasDarkBorder) {
this.createDarkBorders(th2);
thRow.addContent(th2);
}
}
}
} else {
// create empty cells for non-radios or checks, or rads and
// checks
// without horizontal layout
th2 = createThCell("", 1);
if(hasDarkBorder) {
this.createDarkBorders(th2);
thRow.addContent(th2);
}
}
}
// now add the final empty th cell for the row
th2 = createThCell();
if(hasDarkBorder) {
this.createDarkBorders(th2);
}
thRow.addContent(th2);
}
@Override
public Element createTable() {
Element tab = super.createTable();
return setClassNames(tab);
}
public Element createThCell(String cellText, int colSpan) {
Element th = super.createThCell(cellText);
if (colSpan > 1) {
th.setAttribute("colspan", colSpan + "");
}
return setClassNames(th);
}
public Element createTHTagFromItemMeta(ItemFormMetadataBean itemFormBean,
boolean hasDarkBorders) {
// include quest number in the header
Element thTag;
String responseType = itemFormBean.getResponseSet().getResponseType().getName();
boolean hasQuestNumber = !"".equalsIgnoreCase(itemFormBean.getQuestionNumberLabel());
Element newSpan = new Element("span");
if (hasQuestNumber) {
newSpan = new Element("span");
newSpan.setAttribute("style", "margin-right:1em");
newSpan.addContent(itemFormBean.getQuestionNumberLabel());
}
String header = itemFormBean.getHeader();
if (header != null && header.length() == 0) {
header = itemFormBean.getLeftItemText();
}
// Implement colspan required for headers associated with
// cells containing checkboxes or radio buttons
if ((responseType.equalsIgnoreCase("radio") || responseType.equalsIgnoreCase("checkbox"))
&& itemFormBean.getResponseLayout().equalsIgnoreCase("horizontal")) {
thTag = this.createThCell(header,
itemFormBean.getResponseSet().getOptions().size());
} else {
thTag = this.createThCell(header, 1);
}
if(hasDarkBorders) {
createDarkBorders(thTag);
}
if (hasQuestNumber) {
thTag.addContent(0, newSpan);
}
return thTag;
}
public void createDarkBorders(Element element){
if(element == null) return;
//remove the existing class attribute and replace it with one that specifies
//darker cell borders
element.removeAttribute("class");
//Is it a th or td tag?
String cssRuleIdentifier = element.getName();
String cssClasses = CssRules.getClassNamesForTag(cssRuleIdentifier+" borders_on");
element.setAttribute("class",cssClasses);
}
public List<Element> createTheadContentsFromDisplayItems(
List<DisplayItemBean> displayBeans, boolean generateExtraColumn,
boolean hasDarkBorders) {
List<Element> elements = new ArrayList<Element>();
ItemFormMetadataBean itemFormBean;
// Get the names for the table's headers;
// Use the item header first, then left item text if the
// header is blank; add question number labels potentially
for (DisplayItemBean displayBean : displayBeans) {
itemFormBean = displayBean.getMetadata();
elements.add(createTHTagFromItemMeta(itemFormBean, hasDarkBorders));
}
// Create an extra column for the cells that contain a Remove Row button
if (generateExtraColumn) {
if(! hasDarkBorders){
elements.add(this.createThCell("",0));
} else {
Element thElement = new Element("th");
String cssClasses = CssRules.getClassNamesForTag("th borders_on");
thElement.setAttribute("class",cssClasses);
elements.add(thElement);
}
}
return elements;
}
@Override
public Element setClassNames(Element styledElement) {
String cssClasses = CssRules.getClassNamesForTag(styledElement.getName());
return cssClasses.length() == 0 ? styledElement : styledElement.setAttribute("class", cssClasses);
}
public int getTabindexSeed() {
return tabindexSeed;
}
public void setTabindexSeed(int tabindexSeedint) {
// tabindexSeed is already initialized by the constructor to 1
if (tabindexSeedint > 1)
this.tabindexSeed = tabindexSeedint;
}
public boolean isDataEntry() {
return isDataEntry;
}
public void setDataEntry(boolean dataEntry) {
isDataEntry = dataEntry;
}
public EventCRFBean getEventCRFbean() {
return eventCRFbean;
}
public void setEventCRFbean(EventCRFBean eventCRFbean) {
this.eventCRFbean = eventCRFbean;
}
public StudyBean getStudyBean() {
return studyBean;
}
public void setStudyBean(StudyBean studyBean) {
this.studyBean = studyBean;
}
}