# ConfigurationPersistence.java

    /*
    ** Module   : ConfigurationPersistence.java
    ** Abstract : Reads and writes analytics configuration using XML.
    **
    ** Copyright (c) 2017, Golden Code Development Corporation.
    **
    ** -#- -I- --Date-- --------------------------------Description----------------------------------
    ** 001 ECF 20170625 Created first version.
    */
    /*
    ** 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.report;

    import java.io.*;
    import java.sql.Date;
    import java.util.*;
    import javax.xml.bind.*;
    import javax.xml.parsers.*;
    import org.w3c.dom.*;
    import org.w3c.dom.Element;
    import org.xml.sax.*;
    import com.goldencode.p2j.report.server.*;
    import com.goldencode.p2j.report.server.FileFilter;
    import com.goldencode.util.*;

    /**
     * Helper object to persist and load report-related configuration to and from XML. This is used
     * to export and import various bits of configuration created by users using the web application,
     * which must survive database recreates.
     * <p>
     * Currently, the following information is handled by this mechanism:
     * <ul>
     * <li>User-defined report definitions.</li>
     * <li>User credentials.</li>
     * <li>Search history (TRPL expressions used in searches and custom reports).</li>
     * <li>Filter profile definitions.</li>
     * </ul>
     */
    public class ConfigurationPersistence
    implements XmlConstants,
               ReportConstants
    {
       /**
        * Default constructor.
        */
       public ConfigurationPersistence()
       {
       }

       /**
        * Read the given file, which is expected to contain an XML document defining one or more
        * reports. Add each report definition to the given list.
        *
        * @param   file
        *          File containing XML report definitions.
        * @param   reports
        *          List of report definitions to be populated with those read from the file.
        *
        * @return  Type of reports contained in the file; or 0 if file does not exist or is not valid.
        *
        * @throws  ParserConfigurationException
        *          if there is an error configuring the XML parser.
        * @throws  SAXException
        *          if the XML SAX parser encounters an error.
        * @throws  IOException
        *          if there is an error accessing the file.
        */
       public int readXmlReports(File file, List<ReportDefinition> reports)
       throws ParserConfigurationException,
              IOException,
              SAXException
       {
          int type = SRC_UNKNOWN;

          // is this a valid file?
          if (!file.exists() || !file.isFile())
          {
             return type;
          }

          Document dom  = XmlHelper.parse(file);
          Element root = dom.getDocumentElement();

          // read the database mode
          String db = root.getAttribute(ATTR_DB);

          if (db != null && db.length() > 0)
          {
             type = Boolean.parseBoolean(db) ? SRC_SCHEMA : SRC_CODE_CACHE;
          }

          // get the list of reports
          NodeList rpts = root.getElementsByTagName(ELEM_RPT);

          String trueVal = String.valueOf(Boolean.TRUE);

          int rlen = rpts.getLength();

          // process each report
          for (int i = 0; i < rlen; i++)
          {
             ReportDefinition rpt = new ReportDefinition();
             Element elem = (Element) rpts.item(i);

             // mandatory attributes

             rpt.title = elem.getAttribute(ATTR_TITLE);
             rpt.condition = elem.getAttribute(ATTR_COND);

             rpt.setTagList(elem.getAttribute(ATTR_TAG));

             // optional attributes

             rpt.user = trueVal.equalsIgnoreCase(elem.getAttribute(ATTR_USER));

             String dumpType = elem.getAttribute(ATTR_DUMPTYPE);

             if (dumpType != null && dumpType.length() > 0)
             {
                rpt.dumpType = dumpType;
             }

             String dumpExpr = elem.getAttribute(ATTR_DUMPEXPR);

             if (dumpExpr != null && dumpExpr.length() > 0)
             {
                rpt.dumpExpr = dumpExpr;
             }

             String dumpLevel = elem.getAttribute(ATTR_DUMPLVL);

             if (dumpLevel != null && dumpLevel.length() > 0)
             {
                rpt.dumpLevel = Integer.parseInt(dumpLevel);
             }

             String multiplexExpr = elem.getAttribute(ATTR_MULTIPLEX);

             if (multiplexExpr != null && multiplexExpr.length() > 0)
             {
                rpt.multiplexExpr = multiplexExpr;
             }

             String supportLvlExpr = elem.getAttribute(ATTR_SUPPORT);

             if (supportLvlExpr != null && supportLvlExpr.length() > 0)
             {
                rpt.supportLvlExpr = supportLvlExpr;
             }

             reports.add(rpt);
          }

          return type;
       }

       /**
        * Create a new XML document and export the given list of report definitions into it. Save
        * the document into this object's file, which will be created if it does not exist. If the
        * file already exists, it is overwritten without confirmation.
        *
        * @param   file
        *          Destination file.
        * @param   reports
        *          List of report definitions to write.
        * @param   schema
        *          {@code True} if reports are schema reports; {@code false} if they are code reports.
        *
        * @throws  ParserConfigurationException
        *          if there is an XML parser configuration error.
        * @throws  IOException
        *          if there is an error writing the XML content to the file system.
        */
       public void writeXmlReports(File file, List<ReportDefinition> reports, boolean schema)
       throws ParserConfigurationException,
              IOException
       {
          Document dom = XmlHelper.newDocument();
          Element listElem = dom.createElement(ELEM_LIST);
          dom.appendChild(listElem);
          XmlHelper.setAttribute(listElem, ATTR_DB, String.valueOf(schema));

          for (ReportDefinition def : reports)
          {
             Element rptElem = dom.createElement(ELEM_RPT);
             listElem.appendChild(rptElem);

             XmlHelper.setAttribute(rptElem, ATTR_TITLE, def.getTitle());

             XmlHelper.setAttribute(rptElem, ATTR_COND, def.getCondition());

             XmlHelper.setAttribute(rptElem, ATTR_USER, String.valueOf(def.user));

             Set<String> tagSet = def.getTags();
             if (tagSet != null)
             {
                StringBuilder buf = new StringBuilder();
                int i = 0;
                for (String tag : def.getTags())
                {
                   if (i++ > 0)
                   {
                      buf.append(",");
                   }
                   buf.append(tag);
                }
                String tags = buf.toString();
                XmlHelper.setAttribute(rptElem, ATTR_TAG, tags);
             }

             String dumpType = def.getDumpType();
             if (dumpType != null && dumpType.trim().length() > 0)
             {
                XmlHelper.setAttribute(rptElem, ATTR_DUMPTYPE, dumpType);
             }

             String dumpExpr = def.getDumpExpr();
             if (dumpExpr != null && dumpExpr.trim().length() > 0)
             {
                XmlHelper.setAttribute(rptElem, ATTR_DUMPEXPR, dumpExpr);
             }

             int dumpLevel = def.getDumpLevel();
             if (dumpLevel > 0)
             {
                XmlHelper.setAttribute(rptElem, ATTR_DUMPLVL, String.valueOf(dumpLevel));
             }

             String mplex = def.getMultiplexExpr();
             if (mplex != null && mplex.trim().length() > 0)
             {
                XmlHelper.setAttribute(rptElem, ATTR_MULTIPLEX, mplex);
             }

             String support = def.getSupportLvlExpr();
             if (support != null && support.trim().length() > 0)
             {
                XmlHelper.setAttribute(rptElem, ATTR_SUPPORT, support);
             }
          }

          XmlHelper.write(dom, file);
       }

       /**
        * Read the given file, which is expected to contain an XML document defining one or more
        * sets of user credentials. Return the list of entries read.
        *
        * @param   file
        *          File containing XML user credentials.
        *
        * @return  List of user credential entries; may be empty, but will not be {@code null}.
        *
        * @throws  ParserConfigurationException
        *          if there is an error configuring the XML parser.
        * @throws  SAXException
        *          if the XML SAX parser encounters an error.
        * @throws  IOException
        *          if there is an error accessing the file.
        */
       public List<UserRecord> readXmlUsers(File file)
       throws ParserConfigurationException,
              IOException,
              SAXException
       {
          // is this a valid file?
          if (!file.exists() || !file.isFile())
          {
             return Collections.emptyList();
          }

          List<UserRecord> list = new ArrayList<>();

          Document dom  = XmlHelper.parse(file);
          Element root = dom.getDocumentElement();

          // get the list of user credentials
          NodeList users = root.getElementsByTagName(ELEM_USER);

          String trueVal = String.valueOf(Boolean.TRUE);

          // process each search element
          int ulen = users.getLength();
          for (int i = 0; i < ulen; i++)
          {
             Element userElem = (Element) users.item(i);

             String name = userElem.getAttribute(ATTR_NAME);

             boolean admin = trueVal.equalsIgnoreCase(userElem.getAttribute(ATTR_ADMIN));

             byte[] salt = DatatypeConverter.parseHexBinary(userElem.getAttribute(ATTR_SALT));

             byte[] pass = DatatypeConverter.parseHexBinary(userElem.getAttribute(ATTR_PASSWORD));

             String passChanged = userElem.getAttribute(ATTR_PASS_CHANGED);
             Date date = passChanged != null && passChanged.length() > 0
                       ? Date.valueOf(passChanged)
                       : null;

             UserRecord user = new UserRecord(name, admin, salt, pass, date);

             list.add(user);
          }

          return list;
       }

       /**
        * Create a new XML document and export the given list of user credentials into it. Save
        * the document into this object's file, which will be created if it does not exist. If the
        * file already exists, it is overwritten without confirmation.
        *
        * @param   file
        *          Destination file.
        * @param   users
        *          List of user credential records to write.
        *
        * @throws  ParserConfigurationException
        *          if there is an XML parser configuration error.
        * @throws  IOException
        *          if there is an error writing the XML content to the file system.
        */
       public void writeXmlUsers(File file, List<UserRecord> users)
       throws ParserConfigurationException,
              IOException
       {
          Document dom = XmlHelper.newDocument();
          Element listElem = dom.createElement(ELEM_LIST);
          dom.appendChild(listElem);

          for (UserRecord user : users)
          {
             Element userElem = dom.createElement(ELEM_USER);
             listElem.appendChild(userElem);

             XmlHelper.setAttribute(userElem, ATTR_NAME, user.getName());

             XmlHelper.setAttribute(userElem, ATTR_ADMIN, String.valueOf(user.isAdmin()));

             String hexSalt = DatatypeConverter.printHexBinary(user.getSalt());
             XmlHelper.setAttribute(userElem, ATTR_SALT, hexSalt);

             String hexPass = DatatypeConverter.printHexBinary(user.getPassword());
             XmlHelper.setAttribute(userElem, ATTR_PASSWORD, hexPass);

             Date date = user.getPassChanged();
             if (date != null)
             {
                XmlHelper.setAttribute(userElem, ATTR_PASS_CHANGED, date.toString());
             }
          }

          XmlHelper.write(dom, file);
       }

       /**
        * Read the given file, which is expected to contain an XML document defining one or more
        * file filter profiles. Return the list of entries read.
        *
        * @param   file
        *          File containing XML filter profile data.
        *
        * @return  List of filter profiles; may be empty, but will not be {@code null}.
        *
        * @throws  ParserConfigurationException
        *          if there is an error configuring the XML parser.
        * @throws  SAXException
        *          if the XML SAX parser encounters an error.
        * @throws  IOException
        *          if there is an error accessing the file.
        */
       public List<FilterProfile> readXmlFilters(File file)
       throws ParserConfigurationException,
              IOException,
              SAXException
       {
          // is this a valid file?
          if (!file.exists() || !file.isFile())
          {
             return Collections.emptyList();
          }

          List<FilterProfile> list = new ArrayList<>();

          Document dom  = XmlHelper.parse(file);
          Element root = dom.getDocumentElement();

          // get the list of searches
          NodeList searches = root.getElementsByTagName(ELEM_PROFILE);

          String trueVal = String.valueOf(Boolean.TRUE);

          // process each search element
          int slen = searches.getLength();
          for (int i = 0; i < slen; i++)
          {
             Element profileElem = (Element) searches.item(i);

             String name = profileElem.getAttribute(ATTR_NAME);

             FilterProfile profile = new FilterProfile();
             profile.setName(name);

             // add filters associated with the profile
             NodeList filters = profileElem.getElementsByTagName(ELEM_FILTER);
             int ulen = filters.getLength();
             for (int j = 0; j < ulen; j++)
             {
                Element filterElem = (Element) filters.item(j);

                String spec = filterElem.getAttribute(ATTR_SPEC);
                boolean wild = trueVal.equalsIgnoreCase(filterElem.getAttribute(ATTR_WILD));

                FileFilter filter = new FileFilter();
                filter.setSpec(spec);
                filter.setWild(wild);

                profile.addFilter(filter);
             }

             list.add(profile);
          }

          return list;
       }

       /**
        * Create a new XML document and export the given list of file filter profiles into it. Save
        * the document into this object's file, which will be created if it does not exist. If the
        * file already exists, it is overwritten without confirmation.
        *
        * @param   file
        *          Destination file.
        * @param   profiles
        *          List of filter profiles to write.
        *
        * @throws  ParserConfigurationException
        *          if there is an XML parser configuration error.
        * @throws  IOException
        *          if there is an error writing the XML content to the file system.
        */
       public void writeXmlFilters(File file, List<FilterProfile> profiles)
       throws ParserConfigurationException,
              IOException
       {
          Document dom = XmlHelper.newDocument();
          Element listElem = dom.createElement(ELEM_LIST);
          dom.appendChild(listElem);

          for (FilterProfile profile : profiles)
          {
             Element profileElem = dom.createElement(ELEM_PROFILE);
             listElem.appendChild(profileElem);

             XmlHelper.setAttribute(profileElem, ATTR_NAME, profile.getName());

             List<FileFilter> filters = profile.getFilters();
             for (FileFilter filter : filters)
             {
                Element filterElem = dom.createElement(ELEM_FILTER);
                profileElem.appendChild(filterElem);
                XmlHelper.setAttribute(filterElem, ATTR_SPEC, filter.getSpec());
                XmlHelper.setAttribute(filterElem, ATTR_WILD, String.valueOf(filter.isWild()));
             }
          }

          XmlHelper.write(dom, file);
       }

       /**
        * Read the given file, which is expected to contain an XML document defining one or more
        * search history entries. Return the list of entries read.
        *
        * @param   file
        *          File containing XML search history.
        *
        * @return  List of search history entries; may be empty, but will not be {@code null}.
        *
        * @throws  ParserConfigurationException
        *          if there is an error configuring the XML parser.
        * @throws  SAXException
        *          if the XML SAX parser encounters an error.
        * @throws  IOException
        *          if there is an error accessing the file.
        */
       public List<SearchHistory> readXmlSearches(File file)
       throws ParserConfigurationException,
              IOException,
              SAXException
       {
          // is this a valid file?
          if (!file.exists() || !file.isFile())
          {
             return Collections.emptyList();
          }

          List<SearchHistory> list = new ArrayList<>();

          Document dom  = XmlHelper.parse(file);
          Element root = dom.getDocumentElement();

          // get the list of searches
          NodeList searches = root.getElementsByTagName(ELEM_SEARCH);

          // process each search element
          int slen = searches.getLength();
          for (int i = 0; i < slen; i++)
          {
             Element searchElem = (Element) searches.item(i);

             String attr = searchElem.getAttribute(ATTR_ID);
             long sid;
             try
             {
                sid = Long.parseLong(attr);
             }
             catch (NumberFormatException exc)
             {
                throw new RuntimeException("Invalid search ID attribute: " + attr, exc);
             }

             String condition = searchElem.getAttribute(ATTR_COND);

             SearchHistory hist = new SearchHistory(sid, condition);

             // add users of the search
             NodeList users = searchElem.getElementsByTagName(ELEM_USER);
             int ulen = users.getLength();
             for (int j = 0; j < ulen; j++)
             {
                Element userElem = (Element) users.item(j);

                String name = userElem.getAttribute(ATTR_NAME);

                hist.addUser(name);
             }

             list.add(hist);
          }

          return list;
       }

       /**
        * Create a new XML document and export the given list of search history entries into it. Save
        * the document into this object's file, which will be created if it does not exist. If the
        * file already exists, it is overwritten without confirmation.
        *
        * @param   file
        *          Destination file.
        * @param   searches
        *          List of search history entries to write.
        *
        * @throws  ParserConfigurationException
        *          if there is an XML parser configuration error.
        * @throws  IOException
        *          if there is an error writing the XML content to the file system.
        */
       public void writeXmlSearches(File file, List<SearchHistory> searches)
       throws ParserConfigurationException,
              IOException
       {
          Document dom = XmlHelper.newDocument();
          Element listElem = dom.createElement(ELEM_LIST);
          dom.appendChild(listElem);

          for (SearchHistory hist : searches)
          {
             Element searchElem = dom.createElement(ELEM_SEARCH);
             listElem.appendChild(searchElem);

             XmlHelper.setAttribute(searchElem, ATTR_ID, String.valueOf(hist.getSearchId()));

             XmlHelper.setAttribute(searchElem, ATTR_COND, hist.getCondition());

             List<String> users = hist.getUsers();
             for (String user : users)
             {
                Element userElem = dom.createElement(ELEM_USER);
                searchElem.appendChild(userElem);
                XmlHelper.setAttribute(userElem, ATTR_NAME, user);
             }
          }

          XmlHelper.write(dom, file);
       }
    }