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);
   }
}