MatchPhraseDictionary.java

/*
** Module   : MatchPhraseDictionary.java
** Abstract : Manages match phrase definitions for name conversion operations.
**
** Copyright (c) 2005-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- --JPRM-- ----------------Description-----------------------------------------
** 001 ECF 20050428   @20935 Created initial version. Manages match phrase
**                           definitions used for name conversion, which
**                           are loaded from an XML document.
** 002 ECF 20050429   @20968 Modified warning logic during load. Now warn
**                           only if a matchlist parameter was configured,
**                           but the file could not be accessed.
** 003 GES 20060310   @24971 Support a new forced full text match 
**                           attribute.
** 004 GES 20060311   @24974 Added load/reload from a named source list.
** 005 GES 20080722   @39164 Moved the well known "standard" mappings
**                           into this class to reduce the minimum project
**                           configuration needed before conversion.
** 006 GES 20090515   @42206 Imports change.
** 007 GES 20091117   @44418 Modified loading to provide external control over
**                           defaults AND to avoid an abend when no match list
**                           is specified.
** 008 SVL 20140320          Do not display "WARNING: could not find match phrase list!" in
**                           runtime mode.
** 009 OM  20150522          Generified class.
** 010 GBB 20230512          Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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.convert;

import java.io.*;
import java.util.*;
import java.util.logging.*;

import org.w3c.dom.*;
import com.goldencode.util.*;
import com.goldencode.p2j.cfg.*;

/**
 * A dictionary which manages a concrete set of match phrase definitions for
 * use with the {@link NameConverter} class. A match phrase definition
 * consists of a string token to be matched and a string value with which
 * to replace the token once it has been matched. <code>NameConverter</code>
 * uses these definitions to carry out its string conversion operations.
 * <p>
 * Several categories of match phrase definitions are managed by this
 * dictionary (identifier constants are defined in {@link
 * MatchPhraseConstants}):
 * <ul>
 * <li><code>STANDARD</code> - standard set of replacements which are
 *     broadly applicable across all name symbols. These typically include
 *     special characters, such as separators and similar symbols.
 * <li><code>CUSTOM</code> - a set of substring replacements which is
 *     specific to the naming conventions of a particular project or
 *     customer. These typically include acronym and abbreviation expansions.
 * <li><code>PRESERVE</code> - a set of substrings which represent tokens
 *     to be left unconverted in the output string. This provides a
 *     mechanism to override other, less specific match phrase definitions
 *     which might otherwise cause unexpected or undesirable conversions.
 * </ul>
 *
 * The definitions are read from an XML file defined in the global
 * global configuration (the <code>matchlist</code> parameter).
 */
public final class MatchPhraseDictionary
implements MatchPhraseConstants
{
   /** Match list parameter key in global configuration */
   private static final String CFG_MATCHLIST = "matchlist";
   
   /** XML element containing all standard match phrases */
   private static final String ELEM_STANDARD = "standard";
   
   /** XML element containing all custom match phrases */
   private static final String ELEM_CUSTOM = "custom";
   
   /** XML element containing all preserve match phrases */
   private static final String ELEM_PRESERVE = "preserve";
   
   /** XML match phrase element */
   private static final String ELEM_PHRASE = "phrase";
   
   /** XML attribute defining the match token */
   private static final String ATTR_MATCH = "match";
   
   /** XML attribute defining the replacement string */
   private static final String ATTR_REPLACE = "replace";
   
   /** XML attribute defining the if only a fulltext match is valid. */
   private static final String ATTR_FULL = "full";
   
   /** A cached instance of this object */
   private static MatchPhraseDictionary cache = null;
   
   /** Logger */
   private static final ConversionStatus LOG = ConversionStatus.get(MatchPhraseDictionary.class);
   
   /** Map of objects containing the match phrase definitions */
   private Map<String, Map<String, String>> maps = new HashMap<>();
   
   /** Map of full text names. */
   private Map<String, String> fulltxt = new HashMap<>();
   
   /**
    * Constructor.
    *
    * @param    std
    *           The standard mappings that should always be available.
    */
   public MatchPhraseDictionary(Map<String, String> std)
   {
      HashMap<String, String> standard = new HashMap<>();
      standard.putAll(std);
      
      maps.put(ELEM_STANDARD, standard);
      maps.put(ELEM_CUSTOM, new HashMap<String, String>());
      maps.put(ELEM_PRESERVE, new HashMap<String, String>());
   }
   
   /**
    * Load or reload the match phrase dictionary definitions from the given
    * persisted match list defined for the current project. Any cached 
    * version of the dictionary is replaced if it has been read previously.
    *
    * @param    list
    *           The match list to use or <code>null</code> to use the default
    *           match list.
    * @param    std
    *           The standard mappings that should always be available.
    *
    * @throws   ConfigurationException
    *           if there is any error with the global configuration or an
    *           error loading the dictionary from persistence. A missing
    *           match list file is not fatal and <em>does not</em> trigger
    *           this exception, but does elicit a warning message to
    *           <code>stderr</code>.
    */
   public static synchronized MatchPhraseDictionary load(String list, Map<String, String> std)
   throws ConfigurationException
   {
      cache = new MatchPhraseDictionary(std);
      
      try
      {
         // Find the match list configuration file to read.
         if (list == null)
         {
            list = Configuration.getParameter(CFG_MATCHLIST);
         }
         
         File file = null;
         
         if (list != null)
         {
            file = Configuration.toFile(list);
         }
         
         // Warn if a filename was configured but was not accessible.
         if (file != null && (!file.exists() || !file.isFile()))
         {
            LOG.log(Level.WARNING, "Could not find match phrase list!");
            
            return cache;
         }
         
         // Extract mappings from XML, if the configuration file was given.
         if (file != null)
         {
            cache.loadEntries(XmlHelper.parse(file));
         }
      }
      
      catch (ConfigurationException exc)
      {
         exc.fillInStackTrace();
         throw exc;
      }
      
      catch (Exception exc)
      {
         throw new ConfigurationException(
            "Error loading default match phrase dictionary", exc);
      }
      
      return cache;
   }
   
   /**
    * Load the match phrase dictionary definitions from the default persisted
    * match list defined for the current project. A cached version of the
    * dictionary is returned if it has been read previously, unless the
    * <code>force</code> option is <code>true</code>, in which case a
    * reload from persistence is forced.
    *
    * @param    force
    *           <code>true</code> to force a load from persistence,
    *           regardless of whether a cached dictionary is available;
    *           <code>false</code> to lazily load the dictionary.
    * @param    std
    *           The standard mappings that should always be available.
    *
    * @throws   ConfigurationException
    *           if there is any error with the global configuration or an
    *           error loading the dictionary from persistence. A missing
    *           match list file is not fatal and <em>does not</em> trigger
    *           this exception, but does elicit a warning message to
    *           <code>stderr</code>.
    */
   public static synchronized MatchPhraseDictionary load(boolean force, Map<String, String> std)
   throws ConfigurationException
   {
      if (!force && cache != null)
      {
         return cache;
      }
      
      return load(null, std);
   }
   
   /**
    * Get a map containing all match phrase definitions included in the
    * requested subset of match phrase categories.
    *
    * @param   categories
    *          A bitwise OR'd combination of match phrase categories.
    *          Expects the category identifiers defined in {@link
    *          MatchPhraseConstants}.
    */
   public Map<String, String> getEntries(int categories)
   {
      Map<String, String> map = new HashMap<>();
      
      if ((categories & STANDARD) != 0)
      {
         map.putAll(maps.get(ELEM_STANDARD));
      }
      
      if ((categories & CUSTOM) != 0)
      {
         map.putAll(maps.get(ELEM_CUSTOM));
      }
      
      if ((categories & PRESERVE) != 0)
      {
         map.putAll(maps.get(ELEM_PRESERVE));
      }
      
      return map;
   }
   
   /**
    * Get a map containing all forced full text matches.
    *
    * @return  A copy of the list of the replacements for all names that are 
    *          forced to be a "full text only" match.
    */
   public Map<String, String> getFullText()
   {
      // make a copy of the map
      Map<String, String> full = new HashMap<>();
      full.putAll(fulltxt);
      
      return full;
   }
   
   /**
    * Put a specific mapping into the forced full text matches list.
    *
    * @param    match
    *           The text to match.
    * @param    replace
    *           The replacement text.
    */
   public void putFullText(String match, String replace)
   {
      fulltxt.put(match, replace);
   }
   
   /**
    * Load the match phrase definitions from the specified XML document.
    * Each type of entry is stored in a different map, which is accessible
    * later using the {@link MatchPhraseConstants} category identifier
    * constants with the {@link #getEntries} method.
    *
    * @param   dom
    *          XML document object model containing the definitions.
    *
    * @throws  ConfigurationException
    *          if an illegal match phrase category is encountered.
    */
   private void loadEntries(Document dom)
   throws ConfigurationException
   {
      Element root = dom.getDocumentElement();
      
      NodeList nodes = root.getElementsByTagName(ELEM_PHRASE);
      int len = nodes.getLength();
      
      for (int i = 0; i < len; i++)
      {
         Element next = (Element) nodes.item(i);
         String match = next.getAttribute(ATTR_MATCH).toLowerCase();
         String replace = next.getAttribute(ATTR_REPLACE);
         
         if (!next.hasAttribute(ATTR_REPLACE))
         {
            replace = match;
         }
         
         // any full-text only match is forced, add this to the full text map
         if (next.hasAttribute(ATTR_FULL) &&
             next.getAttribute(ATTR_FULL).equalsIgnoreCase("true"))
         {
            // add this to the override list
            fulltxt.put(match, replace);
            
            // make sure this does not appear in any other replacement list
            continue;
         }
         
         String name = next.getParentNode().getNodeName();
         
         Map<String, String> map = maps.get(name);
         
         if (map == null)
         {
            throw new ConfigurationException(
               "Unrecognized match phrase category:  " + name);
         }
         
         map.put(match, replace);
      }
   }
}