private FieldSpecCollection recursivelyConvertXFormsFieldsToFieldSpecs(
      FormEntryController formEntryController, List<IFormElement> children, boolean inGroup)
      throws Exception {
    FieldSpecCollection fieldsFromXForms = new FieldSpecCollection();
    for (IFormElement child : children) {
      if (child instanceof GroupDef) {
        GroupDef groupDef = (GroupDef) child;
        List<IFormElement> groupChildren = groupDef.getChildren();
        FieldSpecCollection gridChildrenFieldSpecs =
            recursivelyConvertXFormsFieldsToFieldSpecs(formEntryController, groupChildren, true);
        if (isRepeatGroup(groupDef)) {
          GridFieldSpec gridSpec = new GridFieldSpec();
          TreeReference thisTreeReference = (TreeReference) groupDef.getBind().getReference();
          gridSpec.setTag(createGridTag(thisTreeReference));
          gridSpec.addColumns(gridChildrenFieldSpecs);
          gridSpec.setLabel(getNonNullLabel(groupDef));
          String sectionTag = createSectionTag(thisTreeReference.toString());
          fieldsFromXForms.add(
              FieldSpec.createCustomField(
                  sectionTag, getNonNullLabel(groupDef), new FieldTypeSectionStart()));
          fieldsFromXForms.add(gridSpec);
        } else {
          String sectionTag = createSectionTag(groupDef);
          fieldsFromXForms.add(
              FieldSpec.createCustomField(
                  sectionTag, getNonNullLabel(groupDef), new FieldTypeSectionStart()));
          fieldsFromXForms.addAll(gridChildrenFieldSpecs);
        }
        inGroup = false;
      }

      if (child instanceof QuestionDef) {
        if (!inGroup) {
          ++unGroupedUniqueId;
          String sectionTag = X_FORM_UN_GROUPED_BASE_TAG + unGroupedUniqueId;
          fieldsFromXForms.add(
              FieldSpec.createCustomField(
                  sectionTag, UN_GROUPED_SECTION_LABEL, new FieldTypeSectionStart()));
          inGroup = true;
        }
        QuestionDef questionDef = (QuestionDef) child;
        FormEntryPrompt questionPrompt =
            findQuestion(formEntryController, (TreeReference) questionDef.getBind().getReference());
        FieldSpec fieldSpec = convertToFieldSpec(questionPrompt);
        if (fieldSpec != null) fieldsFromXForms.add(fieldSpec);
      }
    }

    return fieldsFromXForms;
  }
  private FieldSpec convertToFieldSpec(FormEntryPrompt questionPrompt) {
    QuestionDef question = questionPrompt.getQuestion();
    final int dataType = questionPrompt.getDataType();
    TreeReference reference = (TreeReference) question.getBind().getReference();
    String tag = reference.getNameLast();
    String questionLabel = questionPrompt.getQuestion().getLabelInnerText();

    if (questionPrompt.isReadOnly())
      return FieldSpec.createCustomField(tag, questionLabel, new FieldTypeMessage());

    if (isNormalFieldType(dataType))
      return FieldSpec.createCustomField(tag, questionLabel, new FieldTypeNormal());

    if (dataType == Constants.DATATYPE_DATE)
      return FieldSpec.createCustomField(tag, questionLabel, new FieldTypeDate());

    if (shouldTreatSingleItemChoiceListAsBooleanField(dataType, question))
      return FieldSpec.createCustomField(tag, questionLabel, new FieldTypeBoolean());

    if (dataType == Constants.DATATYPE_CHOICE) {
      Vector<ChoiceItem> convertedChoices = new Vector<ChoiceItem>();
      List<SelectChoice> choicesToConvert = question.getChoices();
      for (SelectChoice choiceToConvert : choicesToConvert) {
        // String choiceItemCode = choiceToConvert.getValue();
        String choiceItemLabel = choiceToConvert.getLabelInnerText();
        // Martus doesn't keep Code's when exporting so use Label twice instead
        convertedChoices.add(new ChoiceItem(choiceItemLabel, choiceItemLabel));
      }

      FieldSpec fieldSpec = new CustomDropDownFieldSpec(convertedChoices);
      fieldSpec.setTag(tag);
      fieldSpec.setLabel(questionLabel);
      return fieldSpec;
    }
    MartusLogger.log(
        "Warning: BulletinFromXFormsLoader.convertToFieldSpec unknown Field Type:"
            + String.valueOf(dataType));
    return null;
  }
  public void testPageReport() throws Exception {
    MockMartusApp app = MockMartusApp.create();
    FieldSpec[] topFields = {
      FieldSpec.createStandardField(Bulletin.TAGAUTHOR, new FieldTypeNormal()),
      FieldSpec.createCustomField("tag2", "Label 2", new FieldTypeDate()),
    };
    Bulletin b =
        new Bulletin(
            app.getSecurity(),
            new FieldSpecCollection(topFields),
            StandardFieldSpecs.getDefaultBottomSectionFieldSpecs());
    b.set(topFields[0].getTag(), "First");
    b.set(topFields[1].getTag(), "2005-04-07");
    b.set(Bulletin.TAGPRIVATEINFO, "Secret");
    app.saveBulletin(b, app.getFolderDraftOutbox());

    Bulletin b2 =
        new Bulletin(
            app.getSecurity(),
            new FieldSpecCollection(topFields),
            StandardFieldSpecs.getDefaultBottomSectionFieldSpecs());
    b2.set(topFields[0].getTag(), "Second");
    b2.set(topFields[1].getTag(), "2003-03-29");
    b2.set(Bulletin.TAGPRIVATEINFO, "Another secret");
    app.saveBulletin(b2, app.getFolderDraftOutbox());

    ReportFormat rf = new ReportFormat();
    rf.setBulletinPerPage(true);
    rf.setHeaderSection("Header\n");
    rf.setFooterSection("Footer\n");
    rf.setFakePageBreakSection("----\n");
    rf.setDetailSection(
        "TOP:\n"
            + "#foreach($field in $bulletin.getTopFields())\n"
            + "$field.getLocalizedLabel($localization) $field.html($localization)\n"
            + "#end\n"
            + "BOTTOM:\n"
            + "#foreach($field in $bulletin.getBottomFields())\n"
            + "$field.getLocalizedLabel($localization) $field.html($localization)\n"
            + "#end\n"
            + "");
    String expected0 =
        "Header\n"
            + "TOP:\n"
            + "<field:author> First\n"
            + "Label 2 04/07/2005\n"
            + "BOTTOM:\n"
            + "<field:privateinfo> Secret\n"
            + "Footer\n";
    String expected1 =
        "Header\n"
            + "TOP:\n"
            + "<field:author> Second\n"
            + "Label 2 03/29/2003\n"
            + "BOTTOM:\n"
            + "<field:privateinfo> Another secret\n"
            + "Footer\n";

    RunReportOptions options = new RunReportOptions();
    options.includePrivate = true;
    ReportOutput result = runReportOnAppData(rf, app, options);
    assertEquals("Wrong page report output?", expected0, result.getPageText(0));
    assertEquals("Wrong page report output?", expected1, result.getPageText(1));
    assertEquals("Didn't set fake page break?", "----\n", result.getFakePageBreak());
  }