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