# BrowseJasperReportTemplate.java

    /*
    ** Module   : BrowseJasperReportTemplate.java
    ** Abstract : Template for a browse Jasper report design, used by a browse.
    **
    ** Copyright (c) 2018-2025, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- ---------------------------------Description----------------------------------
    ** 001 SVL 20180207 Created initial version.
    ** 002 SVL 20181004 Support for arbitrary fonts and colors.
    ** 003 OM  20181005 Improve reports in small page space.
    **     SVL 20181006 Implemented arbitrary colors and fonts for column labels.
    ** 004 SVL 20200514 Added center column alignment.
    ** 005 RNC 20250131 If there are too many columns to be displayed in the given page width,
    *                   prepare the cells for the process of splitting the pages instead of just
    *                   shrinking the cells.
    */

    /*
    ** This program is free software: you can redistribute it and/or modify
    ** it under the terms of the GNU Affero General Public License as
    ** published by the Free Software Foundation, either version 3 of the
    ** License, or (at your option) any later version.
    **
    ** This program is distributed in the hope that it will be useful,
    ** but WITHOUT ANY WARRANTY; without even the implied warranty of
    ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    ** GNU Affero General Public License for more details.
    **
    ** You may find a copy of the GNU Affero GPL version 3 at the following
    ** location: https://www.gnu.org/licenses/agpl-3.0.en.html
    **
    ** Additional terms under GNU Affero GPL version 3 section 7:
    **
    **   Under Section 7 of the GNU Affero GPL version 3, the following additional
    **   terms apply to the works covered under the License.  These additional terms
    **   are non-permissive additional terms allowed under Section 7 of the GNU
    **   Affero GPL version 3 and may not be removed by you.
    **
    **   0. Attribution Requirement.
    **
    **     You must preserve all legal notices or author attributions in the covered
    **     work or Appropriate Legal Notices displayed by works containing the covered
    **     work.  You may not remove from the covered work any author or developer
    **     credit already included within the covered work.
    **
    **   1. No License To Use Trademarks.
    **
    **     This license does not grant any license or rights to use the trademarks
    **     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
    **     of Golden Code Development Corporation. You are not authorized to use the
    **     name Golden Code, FWD, or the names of any author or contributor, for
    **     publicity purposes without written authorization.
    **
    **   2. No Misrepresentation of Affiliation.
    **
    **     You may not represent yourself as Golden Code Development Corporation or FWD.
    **
    **     You may not represent yourself for publicity purposes as associated with
    **     Golden Code Development Corporation, FWD, or any author or contributor to
    **     the covered work, without written authorization.
    **
    **   3. No Misrepresentation of Source or Origin.
    **
    **     You may not represent the covered work as solely your work.  All modified
    **     versions of the covered work must be marked in a reasonable way to make it
    **     clear that the modified work is not originating from Golden Code Development
    **     Corporation or FWD.  All modified versions must contain the notices of
    **     attribution required in this license.
    */

    package com.goldencode.p2j.reporting;

    import com.goldencode.ast.*;
    import com.goldencode.p2j.ui.*;
    import com.goldencode.p2j.ui.client.*;
    import com.goldencode.p2j.xml.*;
    import com.goldencode.util.*;
    import net.sf.jasperreports.engine.*;
    import org.w3c.dom.*;
    import javax.xml.parsers.*;
    import java.io.*;
    import java.util.*;

    import static com.goldencode.p2j.reporting.BrowseJasperDataSource.*;
    import static com.goldencode.p2j.xml.XmlAst.*;

    /**
     * Template for a Jasper report design, used by a browse. First, user creates a Jasper report
     * design. This design is called "template" because basing on this design a design applicable to
     * the given browse is created (design should have the matching number of columns, same width,
     * alignment and data type of columns). The following transformations is used to transform a
     * design template into the design for the given browse:
     * <ul>
     *    <li>Only the first column in the design template is taken into consideration. It is
     *    duplicated to form other columns.</li>
     *    <li>Column headers and details bands are widened to the page width (excluding margins)</li>
     *    <li>Widths of columns are set in the design in the same proportion as in the browse.</li>
     *    <li>Text alignment of a column (including column label) is set as in the browse.</li>
     *    <li>Label of a column is set as in the browse.</li>
     * </ul>
     * Transformations are performed in two steps: {@link #prepare(String)} function which loads
     * template into XML tree and performs preparation steps, and
     * {@link #generateReport(BrowseWidget)} function which transforms the XML tree into report
     * applicable to the specific browse and compiles the report.
     */
    class BrowseJasperReportTemplate
    {
       /** Default column label bgcolor. */
       private static final String DEFAULT_COLUMN_LABEL_BGCOLOR = "#ADADAD";

       /** Root node of the prepared template. */
       private XmlAst templateRoot;

       /** Sub-tree representing template for a column header. */
       private XmlAst columnHeaderTemplate;

       /** Sub-tree representing template for a split table header. */
       private XmlAst headersRootTemplate;

       /** Sub-tree representing template for a split table details. */
       private XmlAst detailsRootTemplate;

       /** Sub-tree representing template for a column data area. */
       private XmlAst columnDataTemplate;

       /** Name of the design template file. */
       private String designTemplateFile;

       /** Page width specified in the report (assumed to be Portrait, width &lt; height). */
       private int templatePageWidth;

       /** Page height specified in the report (assumed to be Portrait, width &lt; height). */
       private int templatePageHeight;

       /** The left margin of the page, as read from the template file. */
       private int leftMargin;

       /** The right margin of the page, as read from the template file. */
       private int rightMargin;

       /** The top margin of the page, as read from the template file. */
       private int topMargin;

       /** The bottom margin of the page, as read from the template file. */
       private int bottomMargin;

       /**
        * Load given design template into XML tree and perform preparation steps, which in the future
        * allow to quickly transform this tree into a report suitable for the specific browse.
        *
        * @param  designTemplateFile
        *         Name of the design template file.
        *
        * @throws JasperReportException
        *         if design template file could not be read or parsed or has invalid structure.
        */
       void prepare(String designTemplateFile)
       throws JasperReportException
       {
          InputStream stream =
                FwdJasperExtensionRegistry.getRepositoryService().getInputStream(designTemplateFile);

          Document dom;
          try
          {
             dom = XmlHelper.parse(stream);
          }
          catch (Exception e)
          {
             throw new JasperReportException("Could not parse browse report template file",
                                             designTemplateFile, e);
          }
          templateRoot = domToAst(dom);
          this.designTemplateFile = designTemplateFile;

          XmlAst jasperReportsRoot = findChild(templateRoot, "jasperReport");
          cleanUUID(jasperReportsRoot);

          // get page width
          templatePageWidth = getAttributeInt(jasperReportsRoot, "pageWidth", 0);
          leftMargin = getAttributeInt(jasperReportsRoot, "leftMargin", 0);
          rightMargin = getAttributeInt(jasperReportsRoot, "rightMargin", 0);

          // get page height
          templatePageHeight = getAttributeInt(jasperReportsRoot, "pageHeight", 0);
          topMargin = getAttributeInt(jasperReportsRoot, "topMargin", 0);
          bottomMargin = getAttributeInt(jasperReportsRoot, "bottomMargin", 0);

          // Remove FIELD nodes
          List<XmlAst> fields = findChildren(jasperReportsRoot, "field");
          fields.forEach(AnnotatedAst::remove);

          // Handle column headers (columnHeader/band nodes)
          XmlAst columnHeadersRoot = findChildPath(jasperReportsRoot, "columnHeader/band");
          List<XmlAst> headers = findChildren(columnHeadersRoot, "textField");

          // Create template for column headers
          columnHeaderTemplate = (XmlAst) headers.get(0).duplicateFresh();

          // Remove original header nodes
          headers.forEach(AnnotatedAst::remove);

          // Handle column data (detail/band nodes)
          XmlAst columnsDataRoot = findChildPath(jasperReportsRoot, "detail/band");
          List<XmlAst> columns = findChildren(columnsDataRoot, "textField");

          // Create template for column data
          columnDataTemplate = (XmlAst) columns.get(0).duplicateFresh();

          // Remove original column data nodes
          columns.forEach(AnnotatedAst::remove);

          // just in case the table does not fit on page width
          headersRootTemplate = (XmlAst) findChildPath(jasperReportsRoot, "columnHeader").duplicateFresh();
          detailsRootTemplate = (XmlAst) findChildPath(jasperReportsRoot, "detail").duplicateFresh();
       }

       /**
        * Transform the XML tree representing prepared template ({@link #templateRoot} field) into the
        * report applicable to the specific browse and compile it.
        *
        * What to do when "Bands are larger than one page":
        * * use landscape page,
        * * https://community.jaspersoft.com/wiki/bands-larger-one-page
        *
        * @param  browse
        *         Browse for which the report is generated.
        *
        * @return compiled report applicable to the specific browse.
        *
        * @throws JasperReportException
        *         if there was an error during creation of the browse report or the report could not
        *         be compiled.
        */
       JasperReport generateReport(BrowseWidget browse)
       throws JasperReportException
       {
          XmlAst resultRoot = (XmlAst) templateRoot.duplicateFresh();
          XmlAst fieldsRoot = findChild(resultRoot, "jasperReport");
          XmlAst fieldsAnchor = findChild(fieldsRoot, "queryString");

          // calc total columns width
          List<BrowseColumnWidget> visibleColumns = browse.getVisibleColumns();
          int colNo = visibleColumns.size();
          if (colNo == 0)
          {
             throw new JasperReportException("Browse has no visible columns", designTemplateFile);
          }

          // calculate report columns' widths: widths should be in the same proportion as the browse
          // columns' widths
          int totalBrowseColumnsWidth = 0;
          int[] columnWidths = new int[colNo];
          for (int i = 0; i < colNo; i++)
          {
             BrowseColumnWidget column = visibleColumns.get(i);
             int columnWidth = column.getWidthPixels().toJavaIntegerType();
             columnWidths[i] = columnWidth;
             totalBrowseColumnsWidth += columnWidth;
          }

          boolean longBand = false;
          int pageWidth = templatePageWidth;
          int pageHeight = templatePageHeight;
          if (pageWidth > 0)
          {
             if (pageHeight <= 0)
             {
                // auto-fix to An paper:
                pageHeight = (int) (pageWidth * Math.sqrt(2));
             }

             if (totalBrowseColumnsWidth + leftMargin + rightMargin > pageWidth)
             {
                // well, the report does not fit. Assuming this is portrait, try landscape:
                if (totalBrowseColumnsWidth + leftMargin + rightMargin > pageHeight)
                {
                   // The columns will not fit even in landscape. Prepare for splitting the pages.
                   longBand = true;
                }
                int tmp = pageWidth;
                pageWidth = pageHeight;
                pageHeight = tmp;

                XmlAst footerNode = findChildPath(resultRoot, "jasperReport/pageFooter/band");
                List<XmlAst> footerReportElements = new ArrayList<>();
                for (XmlAst footerElement : findChildren(footerNode, "textField"))
                {
                   footerReportElements.add(findChild(footerElement, "reportElement"));
                }

                // re-center the footer because of the landscape mode
                setAttribute(footerReportElements.get(0), "width", String.valueOf(pageWidth));
                setAttribute(footerReportElements.get(1), "width", String.valueOf(pageWidth));

                // special case when we have a single column that is larger than the usable width
                if (colNo == 1)
                {
                   longBand = false;
                   columnWidths[0] = pageWidth - leftMargin - rightMargin;
                   totalBrowseColumnsWidth = columnWidths[0];
                }

                if (longBand)
                {
                   // compensate for the longer format (Page I of N, Rows A to B). After splitting,
                   // there will be only one footer element instead of 2
                   int oldXCoordinate = Integer.parseInt(getAttribute(footerReportElements.get(0), "x"));
                   setAttribute(footerReportElements.get(0), "x", String.valueOf(oldXCoordinate - 18));
                }
             }
          }
          else
          {
             // template does not care about width (like SSV, XLS or XLSX)
             pageWidth = totalBrowseColumnsWidth + leftMargin + rightMargin;
             pageHeight = 10000; // quite large; it will be ignored, anyway
          }

          int usableWidth = pageWidth - leftMargin - rightMargin;

          List<Integer> splits = new ArrayList<>();
          int counter = 0;
          int nextSplit = -1;
          if (longBand)
          {
             int offset = 0;
             int i;
             for (i = 0; i < colNo - 1; i++)
             {
                if (columnWidths[i] > usableWidth)
                {
                   // shrink column width only to usable width, there will be only one column on this page
                   columnWidths[i] = usableWidth;
                   if (i > 0 && columnWidths[i - 1] != usableWidth)
                   {
                      // if columnWidths[i - 1] == usableWidth, it means the last column occupied the whole
                      // page. In this case, avoid adding duplicate splits
                      // Furthermore, there is no point of setting the last column width one more time if we
                      // already know that it uses the whole page width
                      splits.add(i);
                      columnWidths[i - 1] += usableWidth - offset;
                   }
                   splits.add(i + 1);
                   offset = 0;
                   continue;
                }

                if (offset + columnWidths[i] <= usableWidth)
                {
                   // add the whole column to the page
                   offset += columnWidths[i];
                }
                else
                {
                   // the column doesn't fit anymore, so "move it" to the next page
                   columnWidths[i - 1] += usableWidth - offset;
                   splits.add(i);
                   i--;
                   offset = 0;
                }
             }

             if (offset + columnWidths[i] > usableWidth)
             {
                if (columnWidths[i] > usableWidth)
                {
                   columnWidths[i] = usableWidth;
                }

                if (columnWidths[i - 1] != usableWidth)
                {
                   columnWidths[i - 1] += usableWidth - offset;
                   splits.add(i);
                }
             }

             totalBrowseColumnsWidth = usableWidth;
             nextSplit = splits.get(0);
          }

          int tableX = (usableWidth - totalBrowseColumnsWidth) / 2;

          // center the title:
          XmlAst titleNode = findChildPath(resultRoot,
                                           "jasperReport/title/band/textField/reportElement");
          if (titleNode != null)
          {
             setAttribute(titleNode, "x", "0");
             setAttribute(titleNode, "width", Integer.toString(usableWidth));
          }

          // now set the page sizes to [fieldsRoot]
          setAttribute(fieldsRoot, "pageWidth", Integer.toString(pageWidth));
          setAttribute(fieldsRoot, "pageHeight", Integer.toString(pageHeight));
          setAttribute(fieldsRoot, "columnWidth", Integer.toString(usableWidth));

          XmlAst columnHeadersRoot = findChildPath(resultRoot, "jasperReport/columnHeader/band");
          XmlAst columnDataRoot = findChildPath(resultRoot, "jasperReport/detail/band");

          int xOffset = tableX;
          int splitSize = splits.size();
          for (int i = 0; i < colNo; i++)
          {
             BrowseColumnWidget column = visibleColumns.get(i);
             int columnWidth = columnWidths[i];

             // Create nodes for the field and its attributes
             String columnVarName = "column" + column.config().ordinal;
             XmlAst node = createFieldNode(columnVarName, fieldsRoot, fieldsAnchor);
             node = createFieldNode(columnVarName + ".forecolor", fieldsRoot, node);
             node = createFieldNode(columnVarName + ".fontName", fieldsRoot, node);
             node = createFieldNode(columnVarName + ".fontSize", fieldsRoot, node);
             node = createFieldNode(columnVarName + ".isBold", fieldsRoot, node);
             node = createFieldNode(columnVarName + ".isItalic", fieldsRoot, node);
             fieldsAnchor = createFieldNode(columnVarName + ".isUnderline", fieldsRoot, node);

             BrowseTextAlignment alignment = Browse.getTextAlign(column.config());

             // Create column header
             XmlAst columnHeaderNode = (XmlAst) columnHeaderTemplate.duplicateFresh();
             columnHeadersRoot.graft(columnHeaderNode);

             // Set label text
             String[] labelLines = Browse.getLabelVar(column.config());
             StringBuilder sb = new StringBuilder();
             for (int j = 0; j < labelLines.length; j++)
             {
                if (j != 0)
                {
                   sb.append("\\n"); // do not add the NL char, use the 2-char escape instead
                }
                sb.append(labelLines[j]);
             }

             node = findChild(columnHeaderNode, "textFieldExpression");
             node = (XmlAst) node.getImmediateChild(CDATA_SECTION_NODE, null);
             node = (XmlAst) node.getImmediateChild(CONTENT, null);
             node.setText("\"" + sb.toString() + "\"");

             XmlAst headerReportElement = findChild(columnHeaderNode, "reportElement");

             // label foreground color
             node = null;
             String fgColor = getElementColor(browse,
                                              column.config().ehLabelFgColor,
                                              column.config().labelFgColor,
                                              browse.config().ehLabelFgColor,
                                              browse.config().labelFgColor);

             if (fgColor == null)
             {
                fgColor = getElementColor(browse,
                                          column.config().ehFgColor,
                                          column.config().columnFgColor,
                                          browse.config().ehFgColor,
                                          browse.config().fgcolor);
             }

             if (fgColor != null)
             {
                node = createPropertyNode("net.sf.jasperreports.style.forecolor",
                                          fgColor,
                                          headerReportElement,
                                          node);
             }

             // label bg color
             String bgColor = getElementColor(browse,
                                              column.config().ehLabelBgColor,
                                              column.config().labelBgColor,
                                              browse.config().ehLabelBgColor,
                                              browse.config().labelBgColor);
             if (bgColor == null)
             {
                bgColor = DEFAULT_COLUMN_LABEL_BGCOLOR;
             }

             node = createPropertyNode("net.sf.jasperreports.style.backcolor",
                                       bgColor,
                                       headerReportElement,
                                       node);

             // font attributes
             FontDetails font = getColumnFont(column, true);
             if (isFontFamilyAvailable(font))
             {
                node = createPropertyNode("net.sf.jasperreports.style.fontName",
                                          font.fontName,
                                          headerReportElement,
                                          node);
             }
             node = createPropertyNode("net.sf.jasperreports.style.fontSize",
                                       Integer.toString(font.pointSize),
                                       headerReportElement,
                                       node);
             node = createPropertyNode("net.sf.jasperreports.style.isBold",
                                       Boolean.toString(font.style.isBold()),
                                       headerReportElement,
                                       node);
             node = createPropertyNode("net.sf.jasperreports.style.isItalic",
                                       Boolean.toString(font.style.isItalic()),
                                       headerReportElement,
                                       node);
             node = createPropertyNode("net.sf.jasperreports.style.isUnderline",
                                       Boolean.toString(font.style.isUnderline()),
                                       headerReportElement,
                                       node);

             setPositionAndWidth(columnHeaderNode, xOffset, columnWidth);
             setTextAlignment(columnHeaderNode, alignment);

             // Create column data
             XmlAst columnDataNode = (XmlAst) columnDataTemplate.duplicateFresh();
             columnDataRoot.graft(columnDataNode);

             String content = String.format("$F{%s}", columnVarName);
             node = findChild(columnDataNode, "textFieldExpression");
             node = (XmlAst) node.getImmediateChild(CDATA_SECTION_NODE, null);
             node = (XmlAst) node.getImmediateChild(CONTENT, null);
             node.setText(content);

             setPositionAndWidth(columnDataNode, xOffset, columnWidth);
             setTextAlignment(columnDataNode, alignment);

             // process attribute names
             node = findChild(columnDataNode, "reportElement");
             List<XmlAst> nodes = findChildren(node, "propertyExpression");
             for (XmlAst propNode : nodes)
             {
                XmlAst n = (XmlAst) propNode.getImmediateChild(CDATA_SECTION_NODE, null);
                n = (XmlAst) n.getImmediateChild(CONTENT, null);
                n.setText(n.getText().replace("dummy", columnVarName));
             }

             if (longBand && nextSplit == i + 1)
             {
                nextSplit = ++counter == splitSize ? -1 : splits.get(counter);
                xOffset = tableX;
             }
             else
             {
                xOffset += columnWidth;
             }
          }

          ByteArrayInputStream inputStream;
          try
          {
             String namespaceURI = getAttribute(fieldsRoot, "xmlns");
             Document doc = XmlAst.astToDom(resultRoot, namespaceURI);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
             XmlHelper.write(doc, outputStream);
             //DEBUG: XmlHelper.write(doc, "/tmp/generated-report.jrxml");
             inputStream = new ByteArrayInputStream(outputStream.toByteArray());
          }
          catch (ParserConfigurationException | IOException e)
          {
             throw new JasperReportException("Cannot create DOM from XML", designTemplateFile, e);
          }

          try
          {
             JasperReport report = JasperCompileManager.compileReport(inputStream);
             if (longBand)
             {
                // attach the required properties to the report for splitting the pages
                report.setProperty("colNo", String.valueOf(colNo));
                report.setProperty("splitsPerPage", String.valueOf(splitSize));
             }

             return report;
          }
          catch (JRException e)
          {
             throw new JasperReportException("Cannot compile generated browse report",
                                             designTemplateFile, e);
          }
       }

       /**
        * Set position and width of the single <code>reportElement</code> element under the given
        * parent node.
        *
        * @param   parent
        *          Parent node.
        * @param   xPos
        *          Element X position.
        * @param   columnWidth
        *          Element width.
        */
       private void setPositionAndWidth(XmlAst parent, int xPos, int columnWidth)
       {
          XmlAst element = findChild(parent, "reportElement");
          setAttribute(element, "width", Integer.toString(columnWidth));
          setAttribute(element, "x",     Integer.toString(xPos));
       }

       /**
        * Set text alignment of the single <code>textElement</code> element under the given
        * parent node.
        *
        * @param parent
        *        Parent node.
        * @param alignment
        *        Text alignment.
        */
       private void setTextAlignment(XmlAst parent, BrowseTextAlignment alignment)
       {
          XmlAst element = findChild(parent, "textElement");

          String alignmentStr = null;
          switch (alignment)
          {
             case LEFT:
                alignmentStr = "Left";
                break;
             case RIGHT:
                alignmentStr = "Right";
                break;
             case CENTER:
                alignmentStr = "Center";
                break;
          }
          setAttribute(element, "textAlignment", alignmentStr);
       }

       /**
        * Find the single available path under the given parent node, and return the last node in this
        * path.
        *
        * @param  parent
        *         Parent node.
        * @param  path
        *         "/"-separated down path, e.g. "jasperReport/columnHeader/band". Elements of the path
        *         are XML tags.
        *
        * @return the last node in the path, if such path exists, or <code>null</code> if there is no
        *         path or there are multiple matching paths.
        */
       private XmlAst findChildPath(XmlAst parent, String path)
       {
          String[] tags = path.split("/");

          XmlAst res = parent;
          for (String tag : tags)
          {
             res = findChild(res, tag);
             if (res == null)
             {
                break;
             }
          }

          return res;
       }

       /**
        * Find the single available child with the specified XML tag under the given parent node.
        *
        * @param  parent
        *         Parent node.
        * @param  tag
        *         XML tag.
        *
        * @return the single available child with the specified XML tag.
        *
        * @throws JasperReportXmlSearchException
        *         if there is no available node, or there are multiple matching nodes.
        */
       private XmlAst findChild(XmlAst parent, String tag)
       {
          return findChild(parent, tag, false);
       }

       /**
        * Find the single available child with the specified XML tag under the given parent node.
        *
        * @param  parent
        *         Parent node.
        * @param  tag
        *         XML tag.
        *
        * @return the single available child with the specified XML tag. If <code>noError</code> is
        *         <code>true</code>: returns <code>null</code> if there is no available node, or
        *         there are multiple matching nodes.
        *
        * @throws JasperReportXmlSearchException
        *         if <code>noError</code> is <code>false</code>: there is no available node, or
        *         there are multiple matching nodes.
        */
       private XmlAst findChild(XmlAst parent, String tag, boolean noError)
       {
          return findChild(parent, ELEMENT_NODE, tag, noError);
       }

       /**
        * Find the single available child with the specified node type and text under the given parent
        * node.
        *
        * @param  parent
        *         Parent node.
        * @param  nodeType
        *         Node type (see constants in {@link XmlTokenTypes}).
        * @param  nodeText
        *         Node text (e.g. XML tag or attribute name).
        *
        * @return the single available child with the specified node type and text. If
        *         <code>noError</code> is <code>true</code>: returns <code>null</code> if there are no
        *         available node, or there are multiple matching nodes.
        *
        * @throws JasperReportXmlSearchException
        *         if <code>noError</code> is <code>false</code>: there are no available node, or
        *         there are multiple matching nodes.
        */
       private XmlAst findChild(XmlAst parent, int nodeType, String nodeText, boolean noError)
       {
          List<XmlAst> children = findChildren(parent, nodeType, nodeText, true);
          if (children == null || children.size() != 1)
          {
             if (noError)
             {
                return null;
             }
             else
             {
                throw new JasperReportXmlSearchException(parent, nodeText, designTemplateFile);
             }
          }

          return children.get(0);
       }

       /**
        * Find the child nodes with the specified XML tag under the given parent node.
        *
        * @param  parent
        *         Parent node.
        * @param  tag
        *         XML tag.
        *
        * @return available child nodes with the specified XML tag.
        *
        * @throws JasperReportXmlSearchException
        *         there are no available node.
        */
       private List<XmlAst> findChildren(XmlAst parent, String tag)
       {
          return findChildren(parent, ELEMENT_NODE, tag);
       }

       /**
        * Find the child nodes with the specified node type and text under the given parent node.
        *
        * @param  parent
        *         Parent node.
        * @param  nodeType
        *         Type of the target nodes (see constants in {@link XmlTokenTypes}).
        * @param  nodeText
        *         Node text (e.g. XML tag or attribute name).
        *
        * @return available child nodes with the specified node type and text.
        *
        * @throws JasperReportXmlSearchException
        *         there are no available node.
        */
       private List<XmlAst> findChildren(XmlAst parent, int nodeType, String nodeText)
       {
          return findChildren(parent, nodeType, nodeText, false);
       }

       /**
        * Find the child nodes with the specified node type and text under the given parent node.
        *
        * @param  parent
        *         Parent node.
        * @param  nodeType
        *         Type of the target nodes (see constants in {@link XmlTokenTypes}).
        * @param  nodeText
        *         Node text (e.g. XML tag or attribute name).
        *
        * @return available child nodes with the specified node type and text. If
        *         <code>noError</code> is <code>true</code>: returns <code>null</code> if there are no
        *         available nodes.
        *
        * @throws JasperReportXmlSearchException
        *         if <code>noError</code> is <code>false</code>: there are no available nodes.
        */
       private List<XmlAst> findChildren(XmlAst parent,
                                         int nodeType,
                                         String nodeText,
                                         boolean noError)
       {
          List<XmlAst> children = null;
          XmlAst child = null;
          while (true)
          {
             child = (XmlAst) parent.getImmediateChild(nodeType, child);
             if (child == null)
             {
                break;
             }

             if (Objects.equals(nodeText, child.getText()))
             {
                if (children == null)
                {
                   children = new ArrayList<>();
                }
                children.add(child);
             }
          }

          if (children == null && !noError)
          {
             throw new JasperReportXmlSearchException(parent, nodeText, designTemplateFile);
          }

          return children;
       }

       /**
        * Remove all "uuid" attributes in the given tree.
        *
        * @param root
        *        Root of the tree.
        */
       private void cleanUUID(XmlAst root)
       {
          removeAttribute(root, "uuid");

          XmlAst child = null;
          while (true)
          {
             child = (XmlAst) root.getImmediateChild(ELEMENT_NODE, child);
             if (child == null)
             {
                return;
             }

             cleanUUID(child);
          }
       }

       /**
        * Create FIELD node for Jasper report template.
        *
        * @param  name
        *         Field name.
        * @param  parent
        *         Parent node.
        * @param  anchor
        *         Anchor node: created node will be positioned after the anchor node.
        *
        * @return created FIELD node.
        */
       private XmlAst createFieldNode(String name, XmlAst parent, XmlAst anchor)
       {
          XmlAst fieldNode = new XmlAst(ELEMENT_NODE, "field");
          setAttribute(fieldNode, "name", name);
          setAttribute(fieldNode, "class", "java.lang.String");
          parent.graftAt(fieldNode, anchor.getIndexPos() + 1);
          return fieldNode;
       }

       /**
        * Create PROPERTY node for Jasper report template.
        *
        * @param  name
        *         Property name.
        * @param  value
        *         Property value.
        * @param  parent
        *         Parent node.
        * @param  anchor
        *         Anchor node: created node will be positioned after the anchor node. Can be
        *         <code>null</code>.
        *
        * @return created PROPERTY node.
        */
       private XmlAst createPropertyNode(String name, String value, XmlAst parent, XmlAst anchor)
       {
          XmlAst node = new XmlAst(ELEMENT_NODE, "property");
          setAttribute(node, "name", name);
          setAttribute(node, "value", value);
          if (anchor == null)
          {
             parent.graft(node);
          }
          else
          {
             parent.graftAt(node, anchor.getIndexPos() + 1);
          }
          return node;
       }

       /**
        * Get value of the specified integer attribute of the specified node.
        *
        * @param  node
        *         Parent node.
        * @param  name
        *         Attribute name.
        * @param  defaultValue
        *         Default value of the attribute.
        *
        * @return value of the attribute or the default value if there is no such attribute.
        */
       private int getAttributeInt(XmlAst node, String name, int defaultValue)
       {
          String val = getAttribute(node, name);
          if (val != null)
          {
             try
             {
                return Integer.valueOf(val);
             }
             catch (NumberFormatException e)
             {
                return defaultValue;
             }
          }
          else
          {
             return defaultValue;
          }
       }

       /**
        * Get value of the specified attribute of the specified node.
        *
        * @param  node
        *         Parent node.
        * @param  name
        *         Attribute name.
        *
        * @return value of the attribute or <code>null</code> if there is no such attribute.
        */
       private String getAttribute(XmlAst node, String name)
       {
          // Structure of an attributed node:
          //
          // <node> [ELEMENT_NODE]
          //    attributes [ATTR_SET]
          //       <name> [ATTRIBUTE_NODE]
          //          #text [TEXT_NODE]
          //             <value> [CONTENT]

          XmlAst attrSet = (XmlAst) node.getImmediateChild(ATTR_SET, null);
          if (attrSet == null)
          {
             return null;
          }

          XmlAst attrNode = findChild(attrSet, ATTRIBUTE_NODE, name, true);
          if (attrNode == null)
          {
             return null;
          }

          XmlAst textNode = (XmlAst) attrNode.getImmediateChild(TEXT_NODE, null);
          if (textNode == null)
          {
             return null;
          }

          XmlAst contentNode = (XmlAst) textNode.getImmediateChild(CONTENT, null);
          if (contentNode == null)
          {
             return null;
          }
          else
          {
             return contentNode.getText();
          }
       }

       /**
        * Delete the specified attribute of the specified node.
        *
        * @param  node
        *         Parent node.
        * @param  name
        *         Attribute name.
        */
       private void removeAttribute(XmlAst node, String name)
       {
          // Structure of an attributed node:
          //
          // <node> [ELEMENT_NODE]
          //    attributes [ATTR_SET]
          //       <name> [ATTRIBUTE_NODE]
          //          #text [TEXT_NODE]
          //             <value> [CONTENT]

          XmlAst attrSet = (XmlAst) node.getImmediateChild(ATTR_SET, null);
          if (attrSet == null)
          {
             return;
          }

          XmlAst attrNode = findChild(attrSet, ATTRIBUTE_NODE, name, true);
          if (attrNode != null)
          {
             attrNode.remove();
          }
       }

       /**
        * Set the value of the specified attribute.
        *
        * @param node
        *        Parent node.
        * @param name
        *        Attribute name.
        * @param value
        *        Attribute value to set.
        */
       private void setAttribute(XmlAst node, String name, String value)
       {
          // Structure of an attributed node:
          //
          // <node> [ELEMENT_NODE]
          //    attributes [ATTR_SET]
          //       <name> [ATTRIBUTE_NODE]
          //          #text [TEXT_NODE]
          //             <value> [CONTENT]

          XmlAst attrSet = (XmlAst) node.getImmediateChild(ATTR_SET, null);
          if (attrSet == null)
          {
             attrSet = new XmlAst(ATTR_SET, "attributes");
             node.graft(attrSet);
          }

          XmlAst attrNode = findChild(attrSet, ATTRIBUTE_NODE, name, true);
          if (attrNode == null)
          {
             attrNode = new XmlAst(ATTRIBUTE_NODE, name);
             attrSet.graft(attrNode);
          }

          XmlAst textNode = (XmlAst) attrNode.getImmediateChild(TEXT_NODE, null);
          if (textNode == null)
          {
             textNode = new XmlAst(TEXT_NODE, "#text");
             attrNode.graft(textNode);
          }

          XmlAst contentNode = (XmlAst) textNode.getImmediateChild(CONTENT, null);
          if (contentNode == null)
          {
             contentNode = new XmlAst(CONTENT, value);
             textNode.graft(contentNode);
          }
          else
          {
             contentNode.setText(value);
          }
       }
    }