org.openbravo.erpCommon.businessUtility.AuditTrailPopup.java Source code

Java tutorial

Introduction

Here is the source code for org.openbravo.erpCommon.businessUtility.AuditTrailPopup.java

Source

/*
 *************************************************************************
 * The contents of this file are subject to the Openbravo  Public  License
 * Version  1.1  (the  "License"),  being   the  Mozilla   Public  License
 * Version 1.1  with a permitted attribution clause; you may not  use this
 * file except in compliance with the License. You  may  obtain  a copy of
 * the License at http://www.openbravo.com/legal/license.html 
 * Software distributed under the License  is  distributed  on  an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific  language  governing  rights  and  limitations
 * under the License. 
 * The Original Code is Openbravo ERP. 
 * The Initial Developer of the Original Code is Openbravo SLU 
 * All portions are Copyright (C) 2009-2014 Openbravo SLU 
 * All Rights Reserved. 
 * Contributor(s):  ______________________________________.
 ************************************************************************
 */
package org.openbravo.erpCommon.businessUtility;

import java.io.IOException;
import java.io.PrintWriter;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang.StringEscapeUtils;
import org.hibernate.FetchMode;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Restrictions;
import org.openbravo.base.filter.IsIDFilter;
import org.openbravo.base.filter.IsPositiveIntFilter;
import org.openbravo.base.filter.RequestFilter;
import org.openbravo.base.filter.ValueListFilter;
import org.openbravo.base.model.Entity;
import org.openbravo.base.model.ModelProvider;
import org.openbravo.base.model.Property;
import org.openbravo.base.secureApp.HttpSecureAppServlet;
import org.openbravo.base.secureApp.VariablesSecureApp;
import org.openbravo.base.structure.BaseOBObject;
import org.openbravo.client.application.Process;
import org.openbravo.dal.core.OBContext;
import org.openbravo.dal.service.OBCriteria;
import org.openbravo.dal.service.OBDal;
import org.openbravo.dal.service.OBQuery;
import org.openbravo.data.FieldProvider;
import org.openbravo.erpCommon.obps.ActivationKey;
import org.openbravo.erpCommon.obps.ActivationKey.FeatureRestriction;
import org.openbravo.erpCommon.utility.ComboTableData;
import org.openbravo.erpCommon.utility.OBError;
import org.openbravo.erpCommon.utility.SQLReturnObject;
import org.openbravo.erpCommon.utility.Utility;
import org.openbravo.erpCommon.utility.UtilityData;
import org.openbravo.model.ad.access.AuditTrailRaw;
import org.openbravo.model.ad.access.User;
import org.openbravo.model.ad.datamodel.Column;
import org.openbravo.model.ad.datamodel.Table;
import org.openbravo.model.ad.domain.Callout;
import org.openbravo.model.ad.domain.ListTrl;
import org.openbravo.model.ad.domain.Reference;
import org.openbravo.model.ad.system.Language;
import org.openbravo.model.ad.ui.Element;
import org.openbravo.model.ad.ui.ElementTrl;
import org.openbravo.model.ad.ui.Field;
import org.openbravo.model.ad.ui.FieldTrl;
import org.openbravo.model.ad.ui.Form;
import org.openbravo.model.ad.ui.FormTrl;
import org.openbravo.model.ad.ui.Message;
import org.openbravo.model.ad.ui.MessageTrl;
import org.openbravo.model.ad.ui.ProcessTrl;
import org.openbravo.model.ad.ui.Tab;
import org.openbravo.model.ad.ui.TabTrl;
import org.openbravo.model.ad.ui.Window;
import org.openbravo.model.ad.ui.WindowTrl;
import org.openbravo.reference.ui.UIReference;
import org.openbravo.xmlEngine.XmlDocument;

public class AuditTrailPopup extends HttpSecureAppServlet {
    private static final long serialVersionUID = 1L;

    private static final String auditActionsReferenceId = "4C36DC179A5F40DC80B3F3798E121152";
    private static final String adMessageIdForProcess = "437";
    private static final String adMessageIdForWindow = "614";
    private static final String adMessageIdForCF = "32171A3EEE4847FABB69259868A34ED5";
    private static final String adMessageIdForForm = "D9912E810888475ABB8DFF416196FB5E";
    private static final String adMessageIdForCallout = "13F1AE1374AD4054BE7FD3743B56F266";
    private static final String adValRuleIdForFields = "9C6989B15CEA4987A502C0F5FF02B171";

    private static final String[] colNamesHistory = { "time", "action", "user", "process", "field", "old_value",
            "new_value", "rowkey" };
    private static final RequestFilter columnFilterHistory = new ValueListFilter(colNamesHistory);
    private static final RequestFilter directionFilter = new ValueListFilter("asc", "desc");

    public void init(ServletConfig config) {
        super.init(config);
        boolHist = false;
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        VariablesSecureApp vars = new VariablesSecureApp(request);

        // Prevent execution in Community instances
        if (!ActivationKey.getInstance().isActive()) {
            licenseError(classInfo.type, classInfo.id, FeatureRestriction.TIER1_RESTRICTION, response, request,
                    vars, true);
        }

        String accesTabId = vars.getGlobalVariable("inpTabId", "AuditTrail.tabId", IsIDFilter.instance);
        if (!hasGeneralAccess(vars, "W", accesTabId)) {
            // do security check based on tabId passed in
            bdErrorGeneralPopUp(request, response, Utility.messageBD(this, "Error", vars.getLanguage()),
                    Utility.messageBD(this, "AccessTableNoView", vars.getLanguage()));
        }

        // all code runs in adminMode to get read access to i.e. Tab,TabTrl,AD_Audit_Trail entities
        OBContext.setAdminMode();
        try {

            if (vars.commandIn("POPUP_HISTORY")) {
                // popup showing the history of a single record

                removePageSessionVariables(vars);
                vars.removeSessionValue("AuditTrail.tabId");
                vars.removeSessionValue("AuditTrail.tableId");
                vars.removeSessionValue("AuditTrail.recordId");

                // read request params, and save in session
                String tabId = vars.getGlobalVariable("inpTabId", "AuditTrail.tabId", IsIDFilter.instance);
                String tableId = vars.getGlobalVariable("inpTableId", "AuditTrail.tableId", IsIDFilter.instance);
                // recordId is optional as popup can be called from empty grid
                String recordId = vars.getGlobalVariable("inpRecordId", "AuditTrail.recordId", false, false, false,
                        "", IsIDFilter.instance);
                printPagePopupHistory(response, vars, tabId, tableId, recordId);

            } else if (vars.commandIn("STRUCTURE_HISTORY")) {
                // called from the DataGrid.js STRUCTURE request
                printGridStructureHistory(response, vars);

            } else if (vars.commandIn("DATA_HISTORY")) {
                // called from the DataGrid.js DATA request
                if (vars.getStringParameter("newFilter").equals("1")) {
                    removePageSessionVariables(vars);
                }
                String tableId = vars.getGlobalVariable("inpTableId", "AuditTrail.tableId", IsIDFilter.instance);
                String tabId = vars.getGlobalVariable("inpTabId", "AuditTrail.tabId", IsIDFilter.instance);
                // recordId is optional as popup can be called from empty grid
                String recordId = vars.getGlobalVariable("inpRecordId", "AuditTrail.recordId", false, false, false,
                        "", IsIDFilter.instance);

                // filter fields
                String userId = vars.getGlobalVariable("inpUser", "AuditTrail.userId", "", IsIDFilter.instance);
                String fieldId = vars.getGlobalVariable("inpField", "AuditTrail.fieldId", "", IsIDFilter.instance);
                String dateFrom = vars.getGlobalVariable("inpDateFrom", "AuditTrail.dateFrom", "");
                String dateTo = vars.getGlobalVariable("inpDateTo", "AuditTrail.dateTo", "");

                String strNewFilter = vars.getStringParameter("newFilter");
                String strOffset = vars.getStringParameter("offset", IsPositiveIntFilter.instance);
                String strPageSize = vars.getStringParameter("page_size", IsPositiveIntFilter.instance);
                // not used right now, as grid is defined as non-sortable
                String strSortCols = vars.getInStringParameter("sort_cols", columnFilterHistory);
                String strSortDirs = vars.getInStringParameter("sort_dirs", directionFilter);
                printGridDataHistory(response, vars, tableId, tabId, recordId, userId, fieldId, dateFrom, dateTo,
                        strSortCols, strSortDirs, strOffset, strPageSize, strNewFilter);

            } else if (vars.commandIn("POPUP_DELETED")) {
                // popup showing all deleted records of a single tab

                removePageSessionVariables(vars);
                vars.removeSessionValue("AuditTrail.recordId");
                vars.removeSessionValue("AuditTrail.tabId");
                vars.removeSessionValue("AuditTrail.tableId");

                // read request params, and save in session
                String tabId = vars.getGlobalVariable("inpTabId", "AuditTrail.tabId", IsIDFilter.instance);
                String tableId = vars.getGlobalVariable("inpTableId", "AuditTrail.tableId", IsIDFilter.instance);

                // recordId is optional as popup can be called from empty grid
                String recordId = vars.getGlobalVariable("inpRecordId", "AuditTrail.recordId", false, false, false,
                        "", IsIDFilter.instance);
                printPagePopupDeleted(response, vars, recordId, tabId, tableId);

            } else if (vars.commandIn("STRUCTURE_DELETED")) {
                // called from the DataGrid.js STRUCTURE request
                String tabId = vars.getGlobalVariable("inpTabId", "AuditTrail.tabId", IsIDFilter.instance);
                String tableId = vars.getGlobalVariable("inpTableId", "AuditTrail.tableId", IsIDFilter.instance);
                printGridStructureDeleted(response, vars, tabId, tableId);

            } else if (vars.commandIn("DATA_DELETED")) {
                // called from the DataGrid.js DATA request
                if (vars.getStringParameter("newFilter").equals("1")) {
                    removePageSessionVariables(vars);
                }
                String tabId = vars.getGlobalVariable("inpTabId", "AuditTrail.tabId", IsIDFilter.instance);
                String tableId = vars.getGlobalVariable("inpTableId", "AuditTrail.tableId", IsIDFilter.instance);

                // filter fields
                String dateFrom = vars.getGlobalVariable("inpDateFrom", "AuditTrail.dateFrom", "");
                String dateTo = vars.getGlobalVariable("inpDateTo", "AuditTrail.dateTo", "");
                String userId = vars.getGlobalVariable("inpUser", "AuditTrail.userId", "", IsIDFilter.instance);

                String strNewFilter = vars.getStringParameter("newFilter");
                String strOffset = vars.getStringParameter("offset", IsPositiveIntFilter.instance);
                String strPageSize = vars.getStringParameter("page_size", IsPositiveIntFilter.instance);
                // not used right now, as grid is defined as non-sortable
                String strSortCols = vars.getInStringParameter("sort_cols", columnFilterHistory);
                String strSortDirs = vars.getInStringParameter("sort_dirs", directionFilter);
                printGridDataDeleted(response, vars, tabId, tableId, dateFrom, dateTo, userId, strSortCols,
                        strSortDirs, strOffset, strPageSize, strNewFilter);
            } else {
                pageError(response);
            }
        } finally {
            OBContext.restorePreviousMode();
        }
    }

    private void removePageSessionVariables(VariablesSecureApp vars) {
        vars.removeSessionValue("AuditTrail.userId");
        vars.removeSessionValue("AuditTrail.fieldId");
        vars.removeSessionValue("AuditTrail.dateFrom");
        vars.removeSessionValue("AuditTrail.dateTo");
        // only remove the following values on initial open of the popup, not on a filter change
        if (vars.getStringParameter("newFilter").equals("1")) {
            return;
        }
        vars.removeSessionValue("AuditTrail.fkColumnName");
        vars.removeSessionValue("AuditTrail.fkId");
    }

    private void printPagePopupHistory(HttpServletResponse response, VariablesSecureApp vars, String tabId,
            String tableId, String recordId) throws IOException {
        log4j.debug("POPUP-HISTORY - tabId: " + tabId + ", tableId: " + tableId + ", inpRecordId: " + recordId);

        XmlDocument xmlDocument = xmlEngine
                .readXmlTemplate("org/openbravo/erpCommon/businessUtility/AuditTrailPopupHistory")
                .createXmlDocument();

        xmlDocument.setParameter("directory", "var baseDirectory = \"" + strReplaceWith + "/\";\n");
        xmlDocument.setParameter("language", "defaultLang=\"" + vars.getLanguage() + "\";");
        // calendar language has extra parameter
        xmlDocument.setParameter("calendar", vars.getLanguage().substring(0, 2));
        xmlDocument.setParameter("theme", vars.getTheme());

        xmlDocument.setParameter("jsFocusOnField", Utility.focusFieldJS("paramDateFrom"));

        xmlDocument.setParameter("grid", "20");
        xmlDocument.setParameter("grid_Offset", "");
        xmlDocument.setParameter("grid_SortCols", "1");
        xmlDocument.setParameter("grid_SortDirs", "DESC");
        xmlDocument.setParameter("grid_Default", "0");

        // display/save-formats for the datetime fields
        xmlDocument.setParameter("dateFromdisplayFormat", vars.getSessionValue("#AD_SqlDateTimeFormat"));
        xmlDocument.setParameter("dateFromsaveFormat", vars.getJavaDataTimeFormat());
        xmlDocument.setParameter("dateTodisplayFormat", vars.getSessionValue("#AD_SqlDateTimeFormat"));
        xmlDocument.setParameter("dateTosaveFormat", vars.getJavaDataTimeFormat());

        // user combobox (restricted to login users only)
        try {
            ComboTableData cmd = new ComboTableData(vars, this, "19", "AD_User_ID", "",
                    "C48E4CAE3C2A4C5DBC2E011D8AD2C428",
                    Utility.getContext(this, vars, "#AccessibleOrgTree", "AuditTrailPopup"),
                    Utility.getContext(this, vars, "#User_Client", "AuditTrailPopup"), 0);
            cmd.fillParameters(null, "AuditTrailPopup", "");
            xmlDocument.setData("reportAD_User_ID", "liststructure", cmd.select(false));
        } catch (Exception e) {
            log4j.error("Error getting adUser combo content", e);
        }

        // fields combobox (filtered to be in same tab)
        try {
            // ad_reference.19 == TableDir
            ComboTableData cmd = new ComboTableData(vars, this, "19", "AD_Field_ID", "", adValRuleIdForFields,
                    Utility.getContext(this, vars, "#AccessibleOrgTree", "AuditTrailPopup"),
                    Utility.getContext(this, vars, "#User_Client", "AuditTrailPopup"), 0);
            SQLReturnObject params = new SQLReturnObject();
            params.setData("AD_Tab_ID", tabId); // parameter for the validation
            cmd.fillParameters(params, "AuditTrailPopup", "");
            xmlDocument.setData("reportAD_Field_ID", "liststructure", cmd.select(false));
        } catch (Exception e) {
            log4j.error("Error getting adField combo content", e);
        }

        String recordStatus;
        String identifier;
        Table table = OBDal.getInstance().get(Table.class, tableId);

        // popup called from a record or empty grid?
        if (recordId.isEmpty()) {
            recordStatus = "AUDIT_HISTORY_RECORD_NONE";
            identifier = "";
        } else {
            // called with a recordId
            // fill current record status/data table
            BaseOBObject bob = OBDal.getInstance().get(table.getName(), recordId);
            if (bob != null) {
                // for existing records we use the current identifier
                recordStatus = "AUDIT_HISTORY_RECORD_EXISTS";
                identifier = StringEscapeUtils.escapeHtml(bob.getIdentifier());
            } else {
                // for deleted record we build the identifier manually
                recordStatus = "AUDIT_HISTORY_RECORD_DELETED";
                Entity tableEntity = ModelProvider.getInstance().getEntity(table.getName());
                identifier = try2GetIdentifierViaPK(vars, tableEntity, recordId);
            }
        }

        // name of the business element shown (i.e. Business Partner)
        String elementNameDisplay = getElementNameForTable(table, OBContext.getOBContext().getLanguage());

        String text = Utility.messageBD(this, recordStatus, vars.getLanguage());
        text = text.replace("@recordidentifier@", identifier);
        text = text.replace("@elementname@", elementNameDisplay);

        xmlDocument.setParameter("recordIdentifierText", text);

        String excludeAuditColumnNames = getExcludeAuditColumnNames(vars, tableId);
        if (excludeAuditColumnNames.length() > 0) {
            String auditText = Utility.messageBD(this, "AUDIT_HISTORY_EXCLUDE_AUDITCOLUMNS", vars.getLanguage());
            auditText = auditText.replace("@excludeauditcolumns@", excludeAuditColumnNames);
            xmlDocument.setParameter("excludeAuditColumnText", auditText);
        } else {
            xmlDocument.setParameter("excludeAuditColumnText", "");
        }

        // param for building 'View deleted records' link
        xmlDocument.setParameter("recordId", recordId);
        xmlDocument.setParameter("tabId", tabId);
        xmlDocument.setParameter("tableId", tableId);

        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println(xmlDocument.print());
        out.close();
    }

    /**
     * Gets the translated name of the business element stored in a table.
     * 
     * Done by a lookup of <tableName>+'_ID' in ad_element and ad_element_trl.
     */
    private String getElementNameForTable(Table table, Language language) {
        // name of the business element shown (i.e. Business Partner)
        String elementName = table.getDBTableName() + "_ID";
        String elementNameDisplay;
        String hql = "as e where upper(e.dBColumnName) = :elementName";
        OBQuery<Element> qe = OBDal.getInstance().createQuery(Element.class, hql);
        qe.setNamedParameter("elementName", elementName.toUpperCase());
        List<Element> lElem = qe.list();
        if (lElem.isEmpty()) {
            elementNameDisplay = "(deleted)";
        } else {
            elementNameDisplay = getTranslatedElementName(lElem.get(0), OBContext.getOBContext().getLanguage());
        }
        return elementNameDisplay;
    }

    private String getTranslatedElementName(Element element, Language language) {
        OBCriteria<ElementTrl> c = OBDal.getInstance().createCriteria(ElementTrl.class);
        c.add(Restrictions.eq(ElementTrl.PROPERTY_APPLICATIONELEMENT, element));
        c.add(Restrictions.eq(ElementTrl.PROPERTY_LANGUAGE, language));
        ElementTrl trl = (ElementTrl) c.uniqueResult();
        if (trl == null) {
            return element.getName();
        }
        return trl.getName();
    }

    private void printPagePopupDeleted(HttpServletResponse response, VariablesSecureApp vars, String recordId,
            String tabId, String tableId) throws IOException, ServletException {
        log4j.debug("POPUP_DELETED - recordId: " + recordId + ", tabId: " + tabId + ", tableId: " + tableId);

        // first check, if this is an open following a history -> deleted link? or a follow into a child
        // tab link from the deleted records view
        String parentLinkFilter = vars.getStringParameter("inpParentLinkFilter", IsIDFilter.instance);
        boolean haveParentLink = (parentLinkFilter != null && !parentLinkFilter.isEmpty());
        List<String> discards = new ArrayList<String>();
        if (haveParentLink) {
            discards.add("discardLinkBack");
        }

        // check if child-tabs exists and build links below the grid
        AuditTrailPopupData[] childTabs = AuditTrailPopupData.selectSubtabs(this, tabId);
        StringBuilder links = new StringBuilder();
        if (childTabs.length == 0) {
            discards.add("childTabsLinksArea");
        } else {
            links.append(Utility.messageBD(this, "AUDIT_HISTORY_CHILDTAB_TEXT", vars.getLanguage()));
            links.append(' ');
            for (AuditTrailPopupData childTab : childTabs) {
                Tab cTab = OBDal.getInstance().get(Tab.class, childTab.tabid);
                String translatedTabName = getTranslatedTabName(cTab);
                String childTableId = cTab.getTable().getId();
                String oneLink = "<a class=\"LabelLink_noicon\" href=\"#\" onclick=\"gotoChild('" + childTab.tabid
                        + "', '" + childTableId + "'); return false;\">" + translatedTabName + "</a>";
                links.append(oneLink).append(", ");
            }
            links.setLength(links.length() - 2);
        }
        String[] discard = new String[discards.size()];
        discards.toArray(discard);
        XmlDocument xmlDocument = xmlEngine
                .readXmlTemplate("org/openbravo/erpCommon/businessUtility/AuditTrailPopupDeleted", discard)
                .createXmlDocument();

        xmlDocument.setParameter("childTabsLinksArea", links.toString());

        xmlDocument.setParameter("directory", "var baseDirectory = \"" + strReplaceWith + "/\";\n");
        xmlDocument.setParameter("language", "defaultLang=\"" + vars.getLanguage() + "\";");
        // calendar language has extra parameter
        xmlDocument.setParameter("calendar", vars.getLanguage().substring(0, 2));
        xmlDocument.setParameter("theme", vars.getTheme());

        xmlDocument.setParameter("jsFocusOnField", Utility.focusFieldJS("paramDateFrom"));

        xmlDocument.setParameter("grid", "20");
        xmlDocument.setParameter("grid_Offset", "");
        xmlDocument.setParameter("grid_SortCols", "1");
        xmlDocument.setParameter("grid_SortDirs", "DESC");
        xmlDocument.setParameter("grid_Default", "0");

        // display/save-formats for the datetime fields
        xmlDocument.setParameter("dateFromdisplayFormat", vars.getSessionValue("#AD_SqlDateTimeFormat"));
        xmlDocument.setParameter("dateFromsaveFormat", vars.getSessionValue("#AD_SqlDateTimeFormat"));
        xmlDocument.setParameter("dateTodisplayFormat", vars.getSessionValue("#AD_SqlDateTimeFormat"));
        xmlDocument.setParameter("dateTosaveFormat", vars.getSessionValue("#AD_SqlDateTimeFormat"));

        // user combobox (restricted to login users only)
        try {
            ComboTableData cmd = new ComboTableData(vars, this, "19", "AD_User_ID", "",
                    "C48E4CAE3C2A4C5DBC2E011D8AD2C428",
                    Utility.getContext(this, vars, "#AccessibleOrgTree", "AuditTrailPopup"),
                    Utility.getContext(this, vars, "#User_Client", "AuditTrailPopup"), 0);
            cmd.fillParameters(null, "AuditTrailPopup", "");
            xmlDocument.setData("reportAD_User_ID", "liststructure", cmd.select(false));
        } catch (Exception e) {
            log4j.error("Error getting adUser combo content", e);
        }

        // param for building 'Back to history' link
        xmlDocument.setParameter("recordId", recordId);
        xmlDocument.setParameter("tabId", tabId);
        xmlDocument.setParameter("tableId", tableId);

        // check if opened from a non-toplevel tab, if so then filter by parent record
        // in both cases add message to info area, telling the user about filter/no-filter
        Tab tab = OBDal.getInstance().get(Tab.class, tabId);
        String parentTabId = AuditTrailPopupData.selectParentTab(myPool, tabId);
        if (parentTabId == null) {
            // top-level tab
            log4j.debug("Deleted records view opened for top-level tab");
            String text = Utility.messageBD(this, "AUDIT_HISTORY_DELETED_TOPLEVELTAB", vars.getLanguage());
            String elementCurrentTab = getElementNameForTable(tab.getTable(),
                    OBContext.getOBContext().getLanguage());
            text = text.replace("@elementnameCurrentTab@", elementCurrentTab);
            xmlDocument.setParameter("recordIdentifierText", text);

            // exclude audit column names in info bar
            String excludeAuditColumnNames = getExcludeAuditColumnNames(vars, tableId);
            if (excludeAuditColumnNames.length() > 0) {
                String auditText = Utility.messageBD(this, "AUDIT_HISTORY_EXCLUDE_AUDITCOLUMNS",
                        vars.getLanguage());
                auditText = auditText.replace("@excludeauditcolumns@", excludeAuditColumnNames);
                xmlDocument.setParameter("excludeAuditColumnText", auditText);
            } else {
                xmlDocument.setParameter("excludeAuditColumnText", "");
            }

        } else {
            // child-tab of another tab
            Tab parentTab = OBDal.getInstance().get(Tab.class, parentTabId);
            log4j.debug("Deleted records view opened for child tab. Parent is: " + parentTabId);

            // code taken from wad to get the columns linking to the parent tab
            AuditTrailPopupData[] parentsFieldsData;
            parentsFieldsData = AuditTrailPopupData.parentsColumnName(this, parentTabId, tabId);

            if (parentsFieldsData == null || parentsFieldsData.length == 0) {
                parentsFieldsData = AuditTrailPopupData.parentsColumnReal(this, parentTabId, tabId);
            }
            // according to wad-generated windows, should only be a single field
            if (parentsFieldsData != null && parentsFieldsData.length > 0) {
                AuditTrailPopupData atpd = parentsFieldsData[0];

                // followed child link?
                String parentValue;
                if (!parentLinkFilter.isEmpty()) {
                    // use value from incoming request
                    parentValue = parentLinkFilter;
                    log4j.debug("Link to parent - columnName: " + atpd.name + " requestValue: " + parentValue);
                } else {
                    // try to get value from session
                    String windowId = tab.getWindow().getId();
                    String sessionVar = windowId + "|" + atpd.name;
                    parentValue = vars.getSessionValue(sessionVar);
                    log4j.debug("Link to parent - columnName: " + atpd.name + " sessionValue: " + parentValue);
                }

                // name of the business element shown (i.e. Business Partner)
                String elementCurrentTab = getElementNameForTable(tab.getTable(),
                        OBContext.getOBContext().getLanguage());
                String elementParentTab = getElementNameForTable(parentTab.getTable(),
                        OBContext.getOBContext().getLanguage());

                // get identifier for current record in parent tab (existing or deleted record)
                String parentIdentifier;
                BaseOBObject bob = OBDal.getInstance().get(parentTab.getTable().getName(), parentValue);
                if (bob != null) {
                    parentIdentifier = bob.getIdentifier();
                } else {
                    Entity tableEntity = ModelProvider.getInstance().getEntity(parentTab.getTable().getName());
                    parentIdentifier = try2GetIdentifierViaPK(vars, tableEntity, parentValue);
                }

                String text = Utility.messageBD(this, "AUDIT_HISTORY_DELETED_CHILDTAB", vars.getLanguage());
                text = text.replace("@elementnameCurrentTab@", elementCurrentTab);
                text = text.replace("@elementnameParentTab@", elementParentTab);
                text = text.replace("@parentIdentifier@", StringEscapeUtils.escapeHtml(parentIdentifier));

                xmlDocument.setParameter("recordIdentifierText", text);

                // exclude audit column names in info bar
                String excludeAuditColumnNames = getExcludeAuditColumnNames(vars, tableId);
                if (excludeAuditColumnNames.length() > 0) {
                    String auditText = Utility.messageBD(this, "AUDIT_HISTORY_EXCLUDE_AUDITCOLUMNS",
                            vars.getLanguage());
                    auditText = auditText.replace("@excludeauditcolumns@", excludeAuditColumnNames);
                    xmlDocument.setParameter("excludeAuditColumnText", auditText);
                } else {
                    xmlDocument.setParameter("excludeAuditColumnText", "");
                }

                // save values for use in DATA request
                vars.setSessionValue("AuditTrail.fkColumnName", atpd.name);
                vars.setSessionValue("AuditTrail.fkId", parentValue);
            }
        }

        response.setContentType("text/html; charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println(xmlDocument.print());
        out.close();
    }

    private void printGridStructureHistory(HttpServletResponse response, VariablesSecureApp vars)
            throws IOException, ServletException {
        SQLReturnObject[] data = getHeadersHistory(vars);
        printGridStructureGeneric(data, response, vars);
    }

    private SQLReturnObject[] getHeadersHistory(VariablesSecureApp vars) {
        SQLReturnObject[] data = new SQLReturnObject[colNamesHistory.length];
        boolean[] colSortable = { false, false, false, false, false, false, false, true };
        String[] colWidths = { "120", "60", "60", "120", "96", "150", "150", "0" };
        for (int i = 0; i < colNamesHistory.length; i++) {
            SQLReturnObject dataAux = new SQLReturnObject();
            dataAux.setData("columnname", colNamesHistory[i]);
            dataAux.setData("gridcolumnname", colNamesHistory[i]);
            dataAux.setData("isidentifier", (colNamesHistory[i].equals("rowkey") ? "true" : "false"));
            dataAux.setData("iskey", (colNamesHistory[i].equals("rowkey") ? "true" : "false"));
            dataAux.setData("isvisible",
                    (colNamesHistory[i].endsWith("_id") || colNamesHistory[i].equals("rowkey") ? "false" : "true"));
            String name = Utility.messageBD(this, "AUDIT_HISTORY_" + colNamesHistory[i].toUpperCase(),
                    vars.getLanguage());
            dataAux.setData("name", (name.startsWith("AUDIT_HISTORY_") ? colNamesHistory[i] : name));
            dataAux.setData("type", "string");
            dataAux.setData("width", colWidths[i]);
            dataAux.setData("issortable", colSortable[i] ? "true" : "false");
            data[i] = dataAux;
        }
        return data;
    }

    private void printGridDataHistory(HttpServletResponse response, VariablesSecureApp vars, String tableId,
            String tabId, String recordId, String userId, String fieldId, String strDateFrom, String strDateTo,
            String strOrderCols, String strOrderDirs, String strOffset, String strPageSize, String strNewFilter)
            throws IOException, ServletException {

        log4j.debug("DATA_HISTORY: tableId: " + tableId + " recordId: " + recordId);

        long s1 = System.currentTimeMillis();

        String hql = "as f where f.tab.id = :tabId and (f.displayed = true or f.column.reference.id = '13')";
        OBQuery<Field> qf = OBDal.getInstance().createQuery(Field.class, hql);
        qf.setNamedParameter("tabId", tabId);
        List<Field> fieldList = qf.list();
        Map<String, Field> fields = new HashMap<String, Field>(fieldList.size());
        for (Field f : fieldList) {
            fields.put(f.getColumn().getId(), f);
        }

        FieldProvider[] data = null;
        String type = "Hidden";
        String title = "";
        String description = "";
        String strNumRows = "0";
        int offset = Integer.valueOf(strOffset).intValue();
        int pageSize = Integer.valueOf(strPageSize).intValue();

        // parse dateTime filter fields
        String strDateFormat = vars.getJavaDataTimeFormat();
        SimpleDateFormat dateFormat = new SimpleDateFormat(strDateFormat);
        Date dateFrom = parseDate(strDateFrom, dateFormat);
        Date dateTo = parseDate(strDateTo, dateFormat);

        // get columnId matching the fieldId
        String columnId = "";
        if (fieldId != null && !fieldId.isEmpty()) {
            Field field = OBDal.getInstance().get(Field.class, fieldId);
            columnId = field.getColumn().getId();
        }

        try {
            if (strNewFilter.equals("1") || strNewFilter.equals("")) {
                // New filter or first load -> get total rows for filter
                strNumRows = getCountRowsHistory(tableId, fields.keySet(), recordId, userId, columnId, dateFrom,
                        dateTo);
                vars.setSessionValue("AuditTrail.numrows", strNumRows);
            } else {
                strNumRows = vars.getSessionValue("AuditTrail.numrows");
            }

            // get data (paged by offset, pageSize)
            data = getDataRowsHistory(vars, tableId, tabId, fields, recordId, userId, columnId, dateFrom, dateTo,
                    offset, pageSize, vars.getLanguage());
        } catch (ServletException e) {
            log4j.error("Error getting row data: ", e);
            OBError myError = Utility.translateError(this, vars, vars.getLanguage(), e.getMessage());
            if (!myError.isConnectionAvailable()) {
                bdErrorAjax(response, "Error", "Connection Error", "No database connection");
                return;
            } else {
                type = myError.getType();
                title = myError.getTitle();
                if (!myError.getMessage().startsWith("<![CDATA["))
                    description = "<![CDATA[" + myError.getMessage() + "]]>";
                else
                    description = myError.getMessage();
            }
        } catch (Exception e) {
            log4j.error("Error getting row data: ", e);
            type = "Error";
            title = "Error";
            if (e.getMessage().startsWith("<![CDATA["))
                description = "<![CDATA[" + e.getMessage() + "]]>";
            else
                description = e.getMessage();
        }

        if (!type.startsWith("<![CDATA["))
            type = "<![CDATA[" + type + "]]>";
        if (!title.startsWith("<![CDATA["))
            title = "<![CDATA[" + title + "]]>";
        if (!description.startsWith("<![CDATA["))
            description = "<![CDATA[" + description + "]]>";
        StringBuffer strRowsData = new StringBuffer();
        strRowsData.append("<xml-data>\n");
        strRowsData.append("  <status>\n");
        strRowsData.append("    <type>").append(type).append("</type>\n");
        strRowsData.append("    <title>").append(title).append("</title>\n");
        strRowsData.append("    <description>").append(description).append("</description>\n");
        strRowsData.append("  </status>\n");
        strRowsData.append("  <rows numRows=\"").append(strNumRows).append("\">\n");
        if (data != null && data.length > 0) {
            for (int j = 0; j < data.length; j++) {
                strRowsData.append("    <tr>\n");
                for (String columnname : colNamesHistory) {
                    strRowsData.append("      <td><![CDATA[");
                    // formatting already done
                    strRowsData.append(data[j].getField(columnname));
                    strRowsData.append("]]></td>\n");
                }
                strRowsData.append("    </tr>\n");
            }
        }
        strRowsData.append("  </rows>\n");
        strRowsData.append("</xml-data>\n");

        response.setContentType("text/xml; charset=UTF-8");
        response.setHeader("Cache-Control", "no-cache");
        PrintWriter out = response.getWriter();
        out.print(strRowsData.toString());
        out.close();
        long s2 = System.currentTimeMillis();
        log4j.debug("printGridDataHistory took: " + (s2 - s1));
    }

    private String getCountRowsHistory(String tableId, Collection<String> columnIds, String recordId, String userId,
            String columnId, Date dateFrom, Date dateTo) {
        long s1 = System.currentTimeMillis();

        OBCriteria<AuditTrailRaw> crit = OBDal.getInstance().createCriteria(AuditTrailRaw.class);
        crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_TABLE, tableId));
        crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_RECORDID, recordId));
        // filter to only show rows which have their column shown in the tab
        crit.add(Restrictions.in(AuditTrailRaw.PROPERTY_COLUMN, columnIds));
        if (!userId.isEmpty()) {
            crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_USERCONTACT, userId));
        }
        if (!columnId.isEmpty()) {
            crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_COLUMN, columnId));
        }
        if (dateFrom != null) {
            crit.add(Restrictions.ge(AuditTrailRaw.PROPERTY_EVENTTIME, dateFrom));
        }
        if (dateTo != null) {
            crit.add(Restrictions.le(AuditTrailRaw.PROPERTY_EVENTTIME, dateTo));
        }
        int count = crit.count();

        long s2 = System.currentTimeMillis();
        log4j.debug("getCountRowsHistory took: " + (s2 - s1) + " result= " + count);

        return String.valueOf(count);
    }

    private FieldProvider[] getDataRowsHistory(VariablesSecureApp vars, String tableId, String tabId,
            Map<String, Field> tabFields, String recordId, String userId, String columnId, Date dateFrom,
            Date dateTo, int offset, int pageSize, String language) throws ServletException {

        OBCriteria<AuditTrailRaw> crit = OBDal.getInstance().createCriteria(AuditTrailRaw.class);
        crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_TABLE, tableId));
        crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_RECORDID, recordId));
        // filter to only show rows which have their column shown in the tab
        crit.add(Restrictions.in(AuditTrailRaw.PROPERTY_COLUMN, tabFields.keySet()));
        crit.addOrder(Order.desc(AuditTrailRaw.PROPERTY_EVENTTIME));
        if (!userId.isEmpty()) {
            crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_USERCONTACT, userId));
        }
        if (!columnId.isEmpty()) {
            crit.add(Restrictions.eq(AuditTrailRaw.PROPERTY_COLUMN, columnId));
        }
        if (dateFrom != null) {
            crit.add(Restrictions.ge(AuditTrailRaw.PROPERTY_EVENTTIME, dateFrom));
        }
        if (dateTo != null) {
            crit.add(Restrictions.le(AuditTrailRaw.PROPERTY_EVENTTIME, dateTo));
        }
        crit.setFirstResult(offset);
        crit.setMaxResults(pageSize);
        List<AuditTrailRaw> rows = crit.list();

        /*
         * beautify result logic is kept simple: iterate over main query result and do needed extra
         * queries to get the beautified column output pro: - simple, keep criteria for main query
         * instead of manual hql . with hibernate session cache: query per distinct pk value only con: -
         * subqueries for column beautification instead of a bigger main query
         */
        UtilityData[] actions = UtilityData.selectReference(this, language, auditActionsReferenceId);

        List<SQLReturnObject> resRows = new ArrayList<SQLReturnObject>(rows.size());
        for (AuditTrailRaw row : rows) {
            SQLReturnObject resRow = new SQLReturnObject();
            resRow.setData("time", getFormattedTime(vars, row.getEventTime()));
            resRow.setData("action", getFormattedAction(actions, row.getAction()));
            resRow.setData("user", getFormattedUser(vars, row.getUserContact()));
            resRow.setData("process", getFormattedProcess(row.getProcessType(), row.getProcess()));
            resRow.setData("field", getFormattedField(tabFields, row));
            resRow.setData("old_value", getFormattedValue(vars, row, false));
            resRow.setData("new_value", getFormattedValue(vars, row, true));
            resRow.setData("rowkey", row.getId());
            resRows.add(resRow);
        }

        FieldProvider[] res = new FieldProvider[resRows.size()];
        res = resRows.toArray(res);
        return res;
    }

    private void printGridStructureDeleted(HttpServletResponse response, VariablesSecureApp vars, String tabId,
            String tableId) throws IOException, ServletException {
        SQLReturnObject[] data = getHeadersDeleted(vars, tabId, tableId);
        printGridStructureGeneric(data, response, vars);
    }

    private SQLReturnObject[] getHeadersDeleted(VariablesSecureApp vars, String tabId, String tableId) {

        List<SQLReturnObject> data = new ArrayList<SQLReturnObject>();

        // rowkey
        SQLReturnObject gridCol = new SQLReturnObject();
        gridCol.setData("columnname", "rowkey");
        gridCol.setData("gridcolumnname", "rowkey");
        gridCol.setData("isidentifier", "true");
        gridCol.setData("iskey", "true");
        gridCol.setData("isvisible", "false");
        gridCol.setData("name", "rowkey");
        gridCol.setData("type", "string");
        gridCol.setData("width", "0");
        gridCol.setData("issortable", "true");
        data.add(gridCol);

        // time
        gridCol = new SQLReturnObject();
        gridCol.setData("columnname", "audittrailtime");
        gridCol.setData("gridcolumnname", "audittrailtime");
        gridCol.setData("isidentifier", "false");
        gridCol.setData("iskey", "false");
        gridCol.setData("isvisible", "true");
        String translatedName = Utility.messageBD(this, "AUDIT_HISTORY_TIME", vars.getLanguage());
        gridCol.setData("name", translatedName);
        gridCol.setData("type", "string");
        gridCol.setData("width", "120");
        gridCol.setData("issortable", "true");
        data.add(gridCol);

        // user
        gridCol = new SQLReturnObject();
        gridCol.setData("columnname", "audittrailuser");
        gridCol.setData("gridcolumnname", "audittrailuser");
        gridCol.setData("isidentifier", "false");
        gridCol.setData("iskey", "false");
        gridCol.setData("isvisible", "true");
        translatedName = Utility.messageBD(this, "AUDIT_HISTORY_USER", vars.getLanguage());
        gridCol.setData("name", translatedName);
        gridCol.setData("type", "string");
        gridCol.setData("width", "60");
        gridCol.setData("issortable", "false");
        data.add(gridCol);

        // process
        gridCol = new SQLReturnObject();
        gridCol.setData("columnname", "audittrailprocess");
        gridCol.setData("gridcolumnname", "audittrailprocess");
        gridCol.setData("isidentifier", "false");
        gridCol.setData("iskey", "false");
        gridCol.setData("isvisible", "true");
        translatedName = Utility.messageBD(this, "AUDIT_HISTORY_PROCESS", vars.getLanguage());
        gridCol.setData("name", translatedName);
        gridCol.setData("type", "string");
        gridCol.setData("width", "120");
        gridCol.setData("issortable", "false");
        data.add(gridCol);

        Tab tab = OBDal.getInstance().get(Tab.class, tabId);
        List<Field> fields = getFieldListForTab(vars, tab);
        for (Field field : fields) {
            Column col = field.getColumn();
            // Remove grid generation for non audited columns
            if (col == null || col.isExcludeAudit()) {
                continue;
            }
            gridCol = new SQLReturnObject();
            gridCol.setData("columnname", col.getDBColumnName());
            gridCol.setData("gridcolumnname", col.getDBColumnName());
            gridCol.setData("isidentifier", "false");
            gridCol.setData("iskey", "false");
            gridCol.setData("isvisible", "true");

            // TODO: optimize fetch for translation
            gridCol.setData("name", getTranslatedFieldName(field, OBContext.getOBContext().getLanguage()));
            gridCol.setData("type", "string");

            gridCol.setData("width", calculateColumnWidth(field.getDisplayedLength()));
            gridCol.setData("issortable", "false");
            data.add(gridCol);
        }

        SQLReturnObject[] result = new SQLReturnObject[data.size()];
        data.toArray(result);
        return result;
    }

    /**
     * Get the ordered list of fields shown in a grid view of a specific tab.
     */
    private List<Field> getFieldListForTab(VariablesSecureApp vars, Tab tab) {
        OBCriteria<Field> c = OBDal.getInstance().createCriteria(Field.class);
        c.add(Restrictions.eq(Field.PROPERTY_TAB, tab));
        c.add(Restrictions.eq(Field.PROPERTY_DISPLAYED, Boolean.TRUE));
        c.add(Restrictions.eq(Field.PROPERTY_SHOWINGRIDVIEW, Boolean.TRUE));
        c.setFetchMode("column", FetchMode.JOIN); // optimize column association
        c.addOrderBy(Field.PROPERTY_SEQUENCENUMBER, true);
        List<Field> fields = c.list();
        return fields;
    }

    private String getTranslatedFieldName(Field field, Language lang) {
        OBCriteria<FieldTrl> c = OBDal.getInstance().createCriteria(FieldTrl.class);
        c.add(Restrictions.eq(FieldTrl.PROPERTY_FIELD, field));
        c.add(Restrictions.eq(FieldTrl.PROPERTY_LANGUAGE, lang));
        FieldTrl trl = (FieldTrl) c.uniqueResult();
        if (trl == null) {
            return field.getName();
        }
        return trl.getName();
    }

    /**
     * Utility method to translate a displayLenth into a column width (in a DataGrid).
     * 
     * Logic needs to be synchronized with TableSQLData.getHeaders
     */
    private String calculateColumnWidth(Long displayLength) {
        long width = displayLength * 6;
        // cap with interval [23..300]
        width = Math.max(23, width);
        width = Math.min(300, width);
        return String.valueOf(width);
    }

    private void printGridDataDeleted(HttpServletResponse response, VariablesSecureApp vars, String tabId,
            String tableId, String strDateFrom, String strDateTo, String userId, String strOrderCols,
            String strOrderDirs, String strOffset, String strPageSize, String strNewFilter)
            throws IOException, ServletException {

        long s1 = System.currentTimeMillis();
        SQLReturnObject[] headers = getHeadersDeleted(vars, tabId, tableId);
        FieldProvider[] data = null;
        String type = "Hidden";
        String title = "";
        String description = "";
        String strNumRows = "0";
        int offset = Integer.valueOf(strOffset).intValue();
        int pageSize = Integer.valueOf(strPageSize).intValue();

        // read optional values for parent filter
        String fkColumnName = vars.getSessionValue("AuditTrail.fkColumnName");
        String fkId = vars.getSessionValue("AuditTrail.fkId");

        try {
            if (strNewFilter.equals("1") || strNewFilter.equals("")) {
                // New filter or first load
                strNumRows = getCountRowsDeleted(vars, tabId, tableId, fkColumnName, fkId, offset, pageSize,
                        strDateFrom, strDateTo, userId);
                vars.setSessionValue("AuditTrail.numrows", strNumRows);
            } else {
                strNumRows = vars.getSessionValue("AuditTrail.numrows");
            }

            // get data
            data = getDataRowsDeleted(vars, tabId, tableId, fkColumnName, fkId, offset, pageSize, strDateFrom,
                    strDateTo, userId);
        } catch (Exception e) {
            log4j.error("Error getting row data: ", e);
            type = "Error";
            title = "Error";
            if (e.getMessage().startsWith("<![CDATA["))
                description = "<![CDATA[" + e.getMessage() + "]]>";
            else
                description = e.getMessage();
        }

        if (!type.startsWith("<![CDATA["))
            type = "<![CDATA[" + type + "]]>";
        if (!title.startsWith("<![CDATA["))
            title = "<![CDATA[" + title + "]]>";
        if (!description.startsWith("<![CDATA["))
            description = "<![CDATA[" + description + "]]>";
        StringBuffer strRowsData = new StringBuffer();
        strRowsData.append("<xml-data>\n");
        strRowsData.append("  <status>\n");
        strRowsData.append("    <type>").append(type).append("</type>\n");
        strRowsData.append("    <title>").append(title).append("</title>\n");
        strRowsData.append("    <description>").append(description).append("</description>\n");
        strRowsData.append("  </status>\n");
        strRowsData.append("  <rows numRows=\"").append(strNumRows).append("\">\n");
        if (data != null && data.length > 0) {
            for (int j = 0; j < data.length; j++) {
                strRowsData.append("    <tr>\n");
                for (int k = 0; k < headers.length; k++) {
                    strRowsData.append("      <td><![CDATA[");
                    String columnname = headers[k].getField("columnname");

                    String value = data[j].getField(columnname);
                    strRowsData.append(value);
                    strRowsData.append("]]></td>\n");
                }
                strRowsData.append("    </tr>\n");
            }
        }
        strRowsData.append("  </rows>\n");
        strRowsData.append("</xml-data>\n");

        response.setContentType("text/xml; charset=UTF-8");
        response.setHeader("Cache-Control", "no-cache");
        PrintWriter out = response.getWriter();
        out.print(strRowsData.toString());
        out.close();
        long s2 = System.currentTimeMillis();
        log4j.debug("printGridDataDeleted took: " + (s2 - s1));
    }

    private String getCountRowsDeleted(VariablesSecureApp vars, String tabId, String tableId, String fkColumnName,
            String fkId, int offset, int pageSize, String dateFrom, String dateTo, String userId) {
        long s1 = System.currentTimeMillis();
        FieldProvider[] rows = AuditTrailDeletedRecords.getDeletedRecords(this, vars, tabId, fkColumnName, fkId, 0,
                0, true, dateFrom, dateTo, userId);
        String countResult = rows[0].getField("counter");
        long t1 = System.currentTimeMillis();

        log4j.debug("getCountRowsDeleted took: " + (t1 - s1) + " result= " + countResult);
        return countResult;
    }

    private FieldProvider[] getDataRowsDeleted(VariablesSecureApp vars, String tabId, String tableId,
            String fkColumnName, String fkId, int offset, int pageSize, String dateFrom, String dateTo,
            String userId) {

        long s1 = System.currentTimeMillis();
        FieldProvider[] rows = AuditTrailDeletedRecords.getDeletedRecords(this, vars, tabId, fkColumnName, fkId,
                offset, pageSize, false, dateFrom, dateTo, userId);
        long s2 = System.currentTimeMillis();

        /*
         * beautify result logic is kept simple: iterate over main query result and do needed extra
         * queries to get the beautified column output pro: - simple, keep criteria for main query
         * instead of manual hql . with hibernate session cache: query per distinct pk value only con: -
         * subqueries for column beautification instead of a bigger main query
         */
        List<FieldProvider> resRows = new ArrayList<FieldProvider>();
        for (FieldProvider row : rows) {
            // copy and beautify
            SQLReturnObject resRow = new SQLReturnObject();
            Tab tab = OBDal.getInstance().get(Tab.class, tabId);
            // copy static fields to result
            resRow.setData("rowkey", row.getField("rowkey"));
            resRow.setData("audittrailtime", getFormattedTime(vars, row.getField("audittrailtime")));
            resRow.setData("audittrailuser", getFormattedUser(vars, row.getField("audittrailuser")));
            resRow.setData("audittrailprocess", getFormattedProcess(row.getField("audittrailprocesstype"),
                    row.getField("audittrailprocessid")));
            // copy and beautify data columns
            for (Field field : tab.getADFieldList()) {
                // no need to format hidden fields
                if (!field.isShowInGridView() || !field.isDisplayed()) {
                    continue;
                }
                Column col = field.getColumn();
                if (col == null) {
                    continue;
                }
                String value = row.getField(col.getDBColumnName());
                // beautify it
                value = getFormattedValue(vars, col, value);
                resRow.setData(col.getDBColumnName(), value);
            }
            resRows.add(resRow);

        }

        long s3 = System.currentTimeMillis();
        log4j.debug("getDataRowsDeleted: getRows:" + (s2 - s1) + " beautifyRows: " + (s3 - s2));

        FieldProvider[] res = new FieldProvider[resRows.size()];
        resRows.toArray(res);
        return res;
    }

    /**
     * Helper function which returns the old value from a audit row. Essentially
     * coalesce(old_char,old_nchar,old_date,old_number, old_text)
     * 
     * @param row
     *          audit record
     * @return the old value from that row
     */
    private static String getOld(AuditTrailRaw row) {
        String result;
        result = row.getOldChar();
        if (result == null) {
            result = row.getOldNChar();
        }
        if (result == null && row.getOldDate() != null) {
            result = String.valueOf(row.getOldDate());
        }
        if (result == null && row.getOldNumber() != null) {
            result = String.valueOf(row.getOldNumber());
        }
        if (result == null && row.getOldText() != null) {
            result = row.getOldText();
        }
        return result;
    }

    /**
     * Helper function which returns the new value from a audit row. Essentially coalesce(new_char,
     * new_nchar, new_date, new_number, new_text)
     * 
     * @param row
     *          audit record
     * @return the new value from that row
     */
    private static String getNew(AuditTrailRaw row) {
        String result;
        result = row.getNewChar();
        if (result == null) {
            result = row.getNewNChar();
        }
        if (result == null && row.getNewDate() != null) {
            result = String.valueOf(row.getNewDate());
        }
        if (result == null && row.getNewNumber() != null) {
            result = String.valueOf(row.getNewNumber());
        }
        if (result == null && row.getNewText() != null) {
            result = row.getNewText();
        }
        return result;
    }

    private static String getFormattedTime(VariablesSecureApp vars, Date time) {
        SimpleDateFormat format = new SimpleDateFormat(vars.getJavaDataTimeFormat());
        return format.format(time);
    }

    // used for deleted records view, time already comes in a sqldatetimeformat
    private static String getFormattedTime(VariablesSecureApp vars, String time) {
        // currently no-op
        return time;
    }

    /*
     * translate action reference value into translated reference list name, as list of distinct
     * values is small do one query for all outside of the loop
     */
    private static String getFormattedAction(final UtilityData[] actions, String action) {
        for (UtilityData theAction : actions) {
            if (theAction.value.equals(action)) {
                return theAction.name;
            }
        }
        return action;
    }

    private String getFormattedUser(VariablesSecureApp vars, String userId) {
        if (userId == null) {
            return " ";
        }
        User u = OBDal.getInstance().get(User.class, userId);
        if (u != null) {
            return u.getName();
        }
        // get value via deleted record audit data
        Entity userEntity = ModelProvider.getInstance().getEntity(User.ENTITY_NAME);
        return try2GetIdentifierViaPK(vars, userEntity, userId);
    }

    /**
     * Map a pair of (processType,process) to a user displayValue by looking up the matching model
     * object names.
     * 
     * The definition of processType is taken from org.openbravo.base.secureApp.ClassInfoData.select
     * 
     * @param processType
     *          char defining which table the process parameter points to
     * @param process
     *          uuid pointing to a model table defined via processType
     */
    private String getFormattedProcess(String processType, String process) {
        if (processType == null || process == null) {
            return " ";
        }

        if ("X".equals(processType)) {
            String formLabel = getTranslatedMessage(adMessageIdForForm);
            return formLabel + ": " + getTranslatedFormName(process);
        }
        if ("P".equals(processType) || "R".equals(processType)) {
            String processLabel = getTranslatedMessage(adMessageIdForProcess);
            return processLabel + ": " + getTranslatedProcessName(process);
        }
        if ("S".equals(processType)) {
            return "Reference: " + OBDal.getInstance().get(Reference.class, process).getName();
        }
        if ("C".equals(processType)) {
            String calloutLabel = getTranslatedMessage(adMessageIdForCallout);
            return calloutLabel + ": " + OBDal.getInstance().get(Callout.class, process).getName();
        }
        if ("CF".equals(processType)) {
            String processCFLabel = getTranslatedMessage(adMessageIdForCF);
            return processCFLabel + ": " + OBDal.getInstance().get(Table.class, process).getDBTableName();
        }
        if ("PD".equals(processType)) {
            String processCFLabel = getTranslatedMessage(adMessageIdForProcess);
            return processCFLabel + ": " + OBDal.getInstance().get(Process.class, process).getIdentifier();
        }
        // all other cases -> Tab
        String windowLabel = getTranslatedMessage(adMessageIdForWindow);
        return windowLabel + ": " + getTranslatedWindowName(process);
    }

    private String getTranslatedMessage(String msgId) {
        Message msg = OBDal.getInstance().get(Message.class, msgId);
        OBCriteria<MessageTrl> c = OBDal.getInstance().createCriteria(MessageTrl.class);
        c.add(Restrictions.eq(MessageTrl.PROPERTY_MESSAGE, msg));
        c.add(Restrictions.eq(MessageTrl.PROPERTY_LANGUAGE, OBContext.getOBContext().getLanguage()));
        MessageTrl trl = (MessageTrl) c.uniqueResult();
        if (trl == null) {
            return msg.getMessageText();
        }
        return trl.getMessageText();
    }

    private String getTranslatedTabName(Tab tab) {
        OBCriteria<TabTrl> c = OBDal.getInstance().createCriteria(TabTrl.class);
        c.add(Restrictions.eq(TabTrl.PROPERTY_TAB, tab));
        c.add(Restrictions.eq(TabTrl.PROPERTY_LANGUAGE, OBContext.getOBContext().getLanguage()));
        TabTrl trl = (TabTrl) c.uniqueResult();
        if (trl == null) {
            return tab.getName();
        }
        return trl.getName();
    }

    private String getTranslatedWindowName(String tabId) {
        Window w = OBDal.getInstance().get(Tab.class, tabId).getWindow();
        OBCriteria<WindowTrl> c = OBDal.getInstance().createCriteria(WindowTrl.class);
        c.add(Restrictions.eq(WindowTrl.PROPERTY_WINDOW, w));
        c.add(Restrictions.eq(WindowTrl.PROPERTY_LANGUAGE, OBContext.getOBContext().getLanguage()));
        WindowTrl trl = (WindowTrl) c.uniqueResult();
        if (trl == null) {
            return w.getName();
        }
        return trl.getName();
    }

    private String getTranslatedProcessName(String processId) {
        org.openbravo.model.ad.ui.Process p = OBDal.getInstance().get(org.openbravo.model.ad.ui.Process.class,
                processId);
        OBCriteria<ProcessTrl> c = OBDal.getInstance().createCriteria(ProcessTrl.class);
        c.add(Restrictions.eq(ProcessTrl.PROPERTY_PROCESS, p));
        c.add(Restrictions.eq(ProcessTrl.PROPERTY_LANGUAGE, OBContext.getOBContext().getLanguage()));
        ProcessTrl trl = (ProcessTrl) c.uniqueResult();
        if (trl == null) {
            return p.getName();
        }
        return trl.getName();
    }

    private String getTranslatedFormName(String formId) {
        Form f = OBDal.getInstance().get(Form.class, formId);
        OBCriteria<FormTrl> c = OBDal.getInstance().createCriteria(FormTrl.class);
        c.add(Restrictions.eq(FormTrl.PROPERTY_SPECIALFORM, f));
        c.add(Restrictions.eq(FormTrl.PROPERTY_LANGUAGE, OBContext.getOBContext().getLanguage()));
        FormTrl trl = (FormTrl) c.uniqueResult();
        if (trl == null) {
            return f.getName();
        }
        return trl.getName();
    }

    // TODO: optimize trl fetching
    private String getFormattedField(Map<String, Field> tabFields, AuditTrailRaw auditRow) {
        Field field = tabFields.get(auditRow.getColumn());

        String fieldNameTranslated = getTranslatedFieldName(field, OBContext.getOBContext().getLanguage());
        return fieldNameTranslated;
    }

    private String getFormattedValue(VariablesSecureApp vars, AuditTrailRaw auditRow, boolean newValue) {
        String value = newValue ? getNew(auditRow) : getOld(auditRow);
        Column col = OBDal.getInstance().get(Column.class, auditRow.getColumn());
        return getFormattedValue(vars, col, value);
    }

    private String getFormattedValue(VariablesSecureApp vars, Column col, String value) {
        if (col == null) {
            // column might have been deleted in the model
            return value;
        }

        // no need to follow a null into some reference
        if (value == null) {
            // translate null into an empty String for display
            return " ";
        }

        Table table = col.getTable();
        Entity tableEntity = ModelProvider.getInstance().getEntity(table.getName());

        String referenceName = col.getReference().getName();

        Property colProperty = tableEntity.getPropertyByColumnName(col.getDBColumnName());
        Entity targetEntity = colProperty.getTargetEntity();
        Property referencedCp = colProperty.getReferencedProperty();

        // cannot be handled by generic targetEntity/referencedProperty code below
        if (referenceName.equals("List")) {
            Reference colListRef = col.getReferenceSearchKey();
            return getTranslatedListValueName(colListRef, value);
        }

        // generic handling of fk-lookup via targetEntity/referencedProperty
        // handles Table,TableDir,Search for now, and all new reference types providing
        // targetEntity/referencedProperty
        if (targetEntity != null) {
            // try generic lookup or fk-target value
            if (referencedCp == null) {
                // use targetEntity's pk as lookup key (like in TableDir case)
                return getFkTargetIdentifierViaPK(vars, targetEntity, value);
            }
            return getFkTargetIdentifierViaReferencedColumn(vars, targetEntity, referencedCp, value);
        }

        // format all other value according to their defined reference
        String adReferenceId = col.getReference().getId();
        UIReference reference = org.openbravo.reference.Reference.getUIReference(adReferenceId, adReferenceId);
        return reference.formatGridValue(vars, value);
    }

    /**
     * Returns the identifier for a given Entity,pk-value combination. If the target record cannot be
     * found, a lookup of the record in the deleted audit-data is performed.
     */
    private String getFkTargetIdentifierViaPK(VariablesSecureApp vars, Entity targetEntity, String fkValue) {
        BaseOBObject bob = OBDal.getInstance().get(targetEntity.getName(), fkValue);
        if (bob != null) {
            return bob.getIdentifier();
        }
        // try to get identifier from audit data
        return try2GetIdentifierViaPK(vars, targetEntity, fkValue);
    }

    /**
     * Returns the identifier for a given Entity,fk-field,fk-field-value combination. If the target
     * record cannot be found, a lookup of the record in the deleted audit-data is performed.
     */
    private String getFkTargetIdentifierViaReferencedColumn(VariablesSecureApp vars, Entity targetEntity,
            Property referencedProperty, String value) {
        OBCriteria<BaseOBObject> c = OBDal.getInstance().createCriteria(targetEntity.getName());
        c.add(Restrictions.eq(referencedProperty.getName(), value));
        BaseOBObject bob = (BaseOBObject) c.uniqueResult();
        if (bob != null) {
            return bob.getIdentifier();
        }
        // try to get identifier from audit data
        return try2GetIdentifierViaReferencedColumn(vars, targetEntity, referencedProperty, value);
    }

    private String try2GetIdentifierViaReferencedColumn(VariablesSecureApp vars, Entity targetEntity,
            Property referencedProperty, String value) {
        // lookup record in audit data (for deleted records) to get its recordId
        OBCriteria<AuditTrailRaw> c = OBDal.getInstance().createCriteria(AuditTrailRaw.class);
        c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_ACTION, "D"));
        c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_TABLE, targetEntity.getTableId()));
        // c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_RECORDID, recordId));
        c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_COLUMN, referencedProperty.getColumnId()));
        c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_OLDCHAR, value));
        AuditTrailRaw atr = (AuditTrailRaw) c.uniqueResult();
        if (atr == null) {
            return "(unknown)";
        }
        // then use the record-id to do the identifier lookup
        return try2GetIdentifierViaPK(vars, targetEntity, atr.getRecordID());
    }

    private String try2GetIdentifierViaPK(VariablesSecureApp vars, Entity targetEntity, String recordId) {
        StringBuilder result = new StringBuilder();

        // loop over identifier columns and concatenate identifier value manually
        // if one of these identifiers cannot be retrieved use fall-back string for it
        for (Property prop : targetEntity.getIdentifierProperties()) {
            // get value for the property from audit data
            OBCriteria<AuditTrailRaw> c = OBDal.getInstance().createCriteria(AuditTrailRaw.class);
            c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_ACTION, "D"));
            c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_TABLE, targetEntity.getTableId()));
            c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_RECORDID, recordId));
            c.add(Restrictions.eq(AuditTrailRaw.PROPERTY_COLUMN, prop.getColumnId()));
            AuditTrailRaw atr = (AuditTrailRaw) c.uniqueResult();
            String value = "(unknown)";
            if (atr != null) {
                // get formatted old value
                value = getFormattedValue(vars, atr, false);
            }
            result.append(value);
            result.append(" - "); // delimiter between columns
        }
        // remove ' ' after last column
        result.setLength(result.length() - 3);
        return result.toString();
    }

    /**
     * The the translated (fall-back untranslated) name of a value of a specific list reference.
     * 
     * @param listRef
     *          the list reference
     * @param value
     *          the value
     * @return translated name of the value in the listRef
     */
    private String getTranslatedListValueName(Reference listRef, String value) {
        OBCriteria<org.openbravo.model.ad.domain.List> critList = OBDal.getInstance()
                .createCriteria(org.openbravo.model.ad.domain.List.class);
        critList.add(Restrictions.eq(org.openbravo.model.ad.domain.List.PROPERTY_REFERENCE, listRef));
        critList.add(Restrictions.eq(org.openbravo.model.ad.domain.List.PROPERTY_SEARCHKEY, value));
        org.openbravo.model.ad.domain.List list = (org.openbravo.model.ad.domain.List) critList.uniqueResult();
        if (list == null) {
            return value;
        }
        // check if we have a translation
        OBCriteria<ListTrl> critListTrl = OBDal.getInstance().createCriteria(ListTrl.class);
        critListTrl.add(Restrictions.eq(ListTrl.PROPERTY_LISTREFERENCE, list));
        critListTrl.add(Restrictions.eq(ListTrl.PROPERTY_LANGUAGE, OBContext.getOBContext().getLanguage()));
        ListTrl trl = (ListTrl) critListTrl.uniqueResult();
        if (trl != null) {
            return trl.getName();
        } else {
            return list.getName();
        }
    }

    private String getExcludeAuditColumnNames(VariablesSecureApp vars, String tableId) {
        OBCriteria<Column> col = OBDal.getInstance().createCriteria(Column.class);
        col.add(Restrictions.eq("table.id", tableId));
        col.add(Restrictions.eq(Column.PROPERTY_EXCLUDEAUDIT, Boolean.TRUE));
        StringBuilder result = new StringBuilder();
        // loop over ExcludeAudit columns and concatenate column name
        List<Column> columnList = col.list();
        for (Column c : columnList) {
            result.append(c.getName());
            result.append(","); // delimiter between columns
        }
        // remove ',' after last column
        if (result.length() > 0) {
            result.setLength(result.length() - 1);
        }
        return result.toString();
    }

    /**
     * Helper function to parse a string with the specified date-format into a date object.
     * 
     * @param inputDate
     *          string to parse
     * @param format
     *          dateFormat of the string
     * @return date object or null on parse error
     */
    private Date parseDate(String inputDate, SimpleDateFormat format) {
        if (inputDate != null && !inputDate.isEmpty()) {
            try {
                return format.parse(inputDate);
            } catch (ParseException pe) {
                log4j.error("Could not parse dateTo", pe);
            }
        }
        return null;
    }

    /**
     * Utility method which print the response to a DataGrid's STRUCTURE request.
     * 
     * @param headers
     *          array describing the list of column shown in the grid
     */
    private void printGridStructureGeneric(SQLReturnObject[] headers, HttpServletResponse response,
            VariablesSecureApp vars) throws IOException, ServletException {
        XmlDocument xmlDocument = xmlEngine.readXmlTemplate("org/openbravo/erpCommon/utility/DataGridStructure")
                .createXmlDocument();

        String type = "Hidden";
        String title = "";
        String description = "";

        xmlDocument.setParameter("type", type);
        xmlDocument.setParameter("title", title);
        xmlDocument.setParameter("description", description);
        xmlDocument.setData("structure1", headers);
        response.setContentType("text/xml; charset=UTF-8");
        response.setHeader("Cache-Control", "no-cache");
        PrintWriter out = response.getWriter();
        out.println(xmlDocument.print());
        out.close();
    }

}