/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *
 *      Copyright 2006-2009 Sun Microsystems, Inc.
 *      Portions Copyright 2013-2015 ForgeRock AS.
 */
package org.opends.server.tools.makeldif;

import org.forgerock.i18n.LocalizableMessage;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.StringTokenizer;

import org.opends.server.core.DirectoryServer;
import org.opends.server.types.AttributeType;
import org.opends.server.types.DN;
import org.opends.server.types.InitializationException;

import static org.opends.messages.ToolMessages.*;
import static org.opends.server.util.StaticUtils.*;

/**
 * This class defines a template file, which is a collection of constant
 * definitions, branches, and templates.
 */
public class TemplateFile
{
  /** The name of the file holding the list of first names. */
  public static final String FIRST_NAME_FILE = "first.names";
  /** The name of the file holding the list of last names. */
  public static final String LAST_NAME_FILE = "last.names";


  /**
   * A map of the contents of various text files used during the parsing
   * process, mapped from absolute path to the array of lines in the file.
   */
  private final HashMap<String, String[]> fileLines = new HashMap<>();

  /** The index of the next first name value that should be used. */
  private int firstNameIndex;
  /** The index of the next last name value that should be used. */
  private int lastNameIndex;

  /**
   * A counter used to keep track of the number of times that the larger of the
   * first/last name list has been completed.
   */
  private int nameLoopCounter;
  /**
   * A counter that will be used in case we have exhausted all possible first
   * and last name combinations.
   */
  private int nameUniquenessCounter;

  /** The set of branch definitions for this template file. */
  private final LinkedHashMap<DN, Branch> branches = new LinkedHashMap<>();
  /** The set of constant definitions for this template file. */
  private final LinkedHashMap<String, String> constants = new LinkedHashMap<>();
  /** The set of registered tags for this template file. */
  private final LinkedHashMap<String, Tag> registeredTags = new LinkedHashMap<>();
  /** The set of template definitions for this template file. */
  private final LinkedHashMap<String, Template> templates = new LinkedHashMap<>();

  /** The random number generator for this template file. */
  private Random random;

  /** The next first name that should be used. */
  private String firstName;
  /** The next last name that should be used. */
  private String lastName;

  /**
   * The resource path to use for filesystem elements that cannot be found
   * anywhere else.
   */
  private String resourcePath;
  /** The path to the directory containing the template file, if available. */
  private String templatePath;

  /** The set of first names to use when generating the LDIF. */
  private String[] firstNames;
  /** The set of last names to use when generating the LDIF. */
  private String[] lastNames;



  /**
   * Creates a new, empty template file structure.
   *
   * @param  resourcePath  The path to the directory that may contain additional
   *                       resource files needed during the LDIF generation
   *                       process.
   */
  public TemplateFile(String resourcePath)
  {
    this(resourcePath, new Random());
  }



  /**
   * Creates a new, empty template file structure.
   *
   *
   * @param  resourcePath  The path to the directory that may contain additional
   *                       resource files needed during the LDIF generation
   *                       process.
   * @param  random        The random number generator for this template file.
   */
  public TemplateFile(String resourcePath, Random random)
  {
    this.resourcePath = resourcePath;
    this.random       = random;

    firstNames            = new String[0];
    lastNames             = new String[0];
    nameUniquenessCounter = 1;

    registerDefaultTags();

    try
    {
      readNameFiles();
    }
    catch (IOException ioe)
    {
      // FIXME -- What to do here?
      ioe.printStackTrace();
      firstNames = new String[] { "John" };
      lastNames  = new String[] { "Doe" };
    }
  }



  /**
   * Retrieves the set of tags that have been registered.  They will be in the
   * form of a mapping between the name of the tag (in all lowercase characters)
   * and the corresponding tag implementation.
   *
   * @return  The set of tags that have been registered.
   */
  public Map<String,Tag> getTags()
  {
    return registeredTags;
  }



  /**
   * Retrieves the tag with the specified name.
   *
   * @param  lowerName  The name of the tag to retrieve, in all lowercase
   *                    characters.
   *
   * @return  The requested tag, or <CODE>null</CODE> if no such tag has been
   *          registered.
   */
  public Tag getTag(String lowerName)
  {
    return registeredTags.get(lowerName);
  }



  /**
   * Registers the specified class as a tag that may be used in templates.
   *
   * @param  tagClass  The fully-qualified name of the class to register as a
   *                   tag.
   *
   * @throws  MakeLDIFException  If a problem occurs while attempting to
   *                             register the specified tag.
   */
  public void registerTag(String tagClass)
         throws MakeLDIFException
  {
    Class c;
    try
    {
      c = Class.forName(tagClass);
    }
    catch (Exception e)
    {
      LocalizableMessage message = ERR_MAKELDIF_CANNOT_LOAD_TAG_CLASS.get(tagClass);
      throw new MakeLDIFException(message, e);
    }

    Tag t;
    try
    {
      t = (Tag) c.newInstance();
    }
    catch (Exception e)
    {
      LocalizableMessage message = ERR_MAKELDIF_CANNOT_INSTANTIATE_TAG.get(tagClass);
      throw new MakeLDIFException(message, e);
    }

    String lowerName = toLowerCase(t.getName());
    if (registeredTags.containsKey(lowerName))
    {
      LocalizableMessage message =
          ERR_MAKELDIF_CONFLICTING_TAG_NAME.get(tagClass, t.getName());
      throw new MakeLDIFException(message);
    }
    else
    {
      registeredTags.put(lowerName, t);
    }
  }



  /**
   * Registers the set of tags that will always be available for use in
   * templates.
   */
  private void registerDefaultTags()
  {
    Class[] defaultTagClasses = new Class[]
    {
      AttributeValueTag.class,
      DNTag.class,
      FileTag.class,
      FirstNameTag.class,
      GUIDTag.class,
      IfAbsentTag.class,
      IfPresentTag.class,
      LastNameTag.class,
      ListTag.class,
      ParentDNTag.class,
      PresenceTag.class,
      RandomTag.class,
      RDNTag.class,
      SequentialTag.class,
      StaticTextTag.class,
      UnderscoreDNTag.class,
      UnderscoreParentDNTag.class
    };

    for (Class c : defaultTagClasses)
    {
      try
      {
        Tag t = (Tag) c.newInstance();
        registeredTags.put(toLowerCase(t.getName()), t);
      }
      catch (Exception e)
      {
        // This should never happen.
        e.printStackTrace();
      }
    }
  }



  /**
   * Retrieves the set of constants defined for this template file.
   *
   * @return  The set of constants defined for this template file.
   */
  public Map<String,String> getConstants()
  {
    return constants;
  }



  /**
   * Retrieves the value of the constant with the specified name.
   *
   * @param  lowerName  The name of the constant to retrieve, in all lowercase
   *                    characters.
   *
   * @return  The value of the constant with the specified name, or
   *          <CODE>null</CODE> if there is no such constant.
   */
  public String getConstant(String lowerName)
  {
    return constants.get(lowerName);
  }



  /**
   * Registers the provided constant for use in the template.
   *
   * @param  name   The name for the constant.
   * @param  value  The value for the constant.
   */
  public void registerConstant(String name, String value)
  {
    constants.put(toLowerCase(name), value);
  }



  /**
   * Retrieves the set of branches defined in this template file.
   *
   * @return  The set of branches defined in this template file.
   */
  public Map<DN,Branch> getBranches()
  {
    return branches;
  }



  /**
   * Retrieves the branch registered with the specified DN.
   *
   * @param  branchDN  The DN for which to retrieve the corresponding branch.
   *
   * @return  The requested branch, or <CODE>null</CODE> if no such branch has
   *          been registered.
   */
  public Branch getBranch(DN branchDN)
  {
    return branches.get(branchDN);
  }



  /**
   * Registers the provided branch in this template file.
   *
   * @param  branch  The branch to be registered.
   */
  public void registerBranch(Branch branch)
  {
    branches.put(branch.getBranchDN(), branch);
  }



  /**
   * Retrieves the set of templates defined in this template file.
   *
   * @return  The set of templates defined in this template file.
   */
  public Map<String,Template> getTemplates()
  {
    return templates;
  }



  /**
   * Retrieves the template with the specified name.
   *
   * @param  lowerName  The name of the template to retrieve, in all lowercase
   *                    characters.
   *
   * @return  The requested template, or <CODE>null</CODE> if there is no such
   *          template.
   */
  public Template getTemplate(String lowerName)
  {
    return templates.get(lowerName);
  }



  /**
   * Registers the provided template for use in this template file.
   *
   * @param  template  The template to be registered.
   */
  public void registerTemplate(Template template)
  {
    templates.put(toLowerCase(template.getName()), template);
  }



  /**
   * Retrieves the random number generator for this template file.
   *
   * @return  The random number generator for this template file.
   */
  public Random getRandom()
  {
    return random;
  }



  /**
   * Reads the contents of the first and last name files into the appropriate
   * arrays and sets up the associated index pointers.
   *
   * @throws  IOException  If a problem occurs while reading either of the
   *                       files.
   */
  private void readNameFiles()
          throws IOException
  {
    File f = getFile(FIRST_NAME_FILE);
    List<String> nameList = readLines(f);
    firstNames = new String[nameList.size()];
    nameList.toArray(firstNames);

    f = getFile(LAST_NAME_FILE);
    nameList = readLines(f);
    lastNames = new String[nameList.size()];
    nameList.toArray(lastNames);
  }

  private List<String> readLines(File f) throws IOException
  {
    try (BufferedReader reader = new BufferedReader(new FileReader(f)))
    {
      ArrayList<String> lines = new ArrayList<>();
      while (true)
      {
        String line = reader.readLine();
        if (line == null)
        {
          break;
        }
        lines.add(line);
      }
      return lines;
    }
  }



  /**
   * Updates the first and last name indexes to choose new values.  The
   * algorithm used is designed to ensure that the combination of first and last
   * names will never be repeated.  It depends on the number of first names and
   * the number of last names being relatively prime.  This method should be
   * called before beginning generation of each template entry.
   */
  public void nextFirstAndLastNames()
  {
    firstName = firstNames[firstNameIndex++];
    lastName  = lastNames[lastNameIndex++];


    // If we've already exhausted every possible combination, then append an
    // integer to the last name.
    if (nameUniquenessCounter > 1)
    {
      lastName += nameUniquenessCounter;
    }

    if (firstNameIndex >= firstNames.length)
    {
      // We're at the end of the first name list, so start over.  If the first
      // name list is larger than the last name list, then we'll also need to
      // set the last name index to the next loop counter position.
      firstNameIndex = 0;
      if (firstNames.length > lastNames.length)
      {
        lastNameIndex = ++nameLoopCounter;
        if (lastNameIndex >= lastNames.length)
        {
          lastNameIndex = 0;
          nameUniquenessCounter++;
        }
      }
    }

    if (lastNameIndex >= lastNames.length)
    {
      // We're at the end of the last name list, so start over.  If the last
      // name list is larger than the first name list, then we'll also need to
      // set the first name index to the next loop counter position.
      lastNameIndex = 0;
      if (lastNames.length > firstNames.length)
      {
        firstNameIndex = ++nameLoopCounter;
        if (firstNameIndex >= firstNames.length)
        {
          firstNameIndex = 0;
          nameUniquenessCounter++;
        }
      }
    }
  }



  /**
   * Retrieves the first name value that should be used for the current entry.
   *
   * @return  The first name value that should be used for the current entry.
   */
  public String getFirstName()
  {
    return firstName;
  }



  /**
   * Retrieves the last name value that should be used for the current entry.
   *
   * @return  The last name value that should be used for the current entry.
   */
  public String getLastName()
  {
    return lastName;
  }



  /**
   * Parses the contents of the specified file as a MakeLDIF template file
   * definition.
   *
   * @param  filename  The name of the file containing the template data.
   * @param  warnings  A list into which any warnings identified may be placed.
   *
   * @throws  IOException  If a problem occurs while attempting to read data
   *                       from the specified file.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   any of the MakeLDIF components.
   *
   * @throws  MakeLDIFException  If any other problem occurs while parsing the
   *                             template file.
   */
  public void parse(String filename, List<LocalizableMessage> warnings)
         throws IOException, InitializationException, MakeLDIFException
  {
    templatePath = null;
    File f = getFile(filename);
    if (f == null || !f.exists())
    {
      LocalizableMessage message = ERR_MAKELDIF_COULD_NOT_FIND_TEMPLATE_FILE.get(filename);
      throw new IOException(message.toString());
    }
    templatePath = f.getParentFile().getAbsolutePath();

    List<String> fileLines = readLines(f);
    String[] lines = new String[fileLines.size()];
    fileLines.toArray(lines);
    parse(lines, warnings);
  }



  /**
   * Parses the data read from the provided input stream as a MakeLDIF template
   * file definition.
   *
   * @param  inputStream  The input stream from which to read the template file
   *                      data.
   * @param  warnings     A list into which any warnings identified may be
   *                      placed.
   *
   * @throws  IOException  If a problem occurs while attempting to read data
   *                       from the provided input stream.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   any of the MakeLDIF components.
   *
   * @throws  MakeLDIFException  If any other problem occurs while parsing the
   *                             template file.
   */
  public void parse(InputStream inputStream, List<LocalizableMessage> warnings)
         throws IOException, InitializationException, MakeLDIFException
  {
    ArrayList<String> fileLines = new ArrayList<>();

    try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)))
    {
      while (true)
      {
        String line = reader.readLine();
        if (line == null)
        {
          break;
        }
        fileLines.add(line);
      }
    }

    String[] lines = new String[fileLines.size()];
    fileLines.toArray(lines);
    parse(lines, warnings);
  }



  /**
   * Parses the provided data as a MakeLDIF template file definition.
   *
   * @param  lines  The lines that make up the template file.
   * @param  warnings  A list into which any warnings identified may be placed.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   any of the MakeLDIF components.
   *
   * @throws  MakeLDIFException  If any other problem occurs while parsing the
   *                             template file.
   */
  public void parse(String[] lines, List<LocalizableMessage> warnings)
         throws InitializationException, MakeLDIFException
  {
    // Create temporary variables that will be used to hold the data read.
    LinkedHashMap<String,Tag> templateFileIncludeTags = new LinkedHashMap<>();
    LinkedHashMap<String,String> templateFileConstants = new LinkedHashMap<>();
    LinkedHashMap<DN,Branch> templateFileBranches = new LinkedHashMap<>();
    LinkedHashMap<String,Template> templateFileTemplates = new LinkedHashMap<>();

    for (int lineNumber=0; lineNumber < lines.length; lineNumber++)
    {
      String line = lines[lineNumber];

      line = replaceConstants(line, lineNumber,
                              templateFileConstants, warnings);

      String lowerLine = toLowerCase(line);
      if (line.length() == 0 || line.startsWith("#"))
      {
        // This is a comment or a blank line, so we'll ignore it.
        continue;
      }
      else if (lowerLine.startsWith("include "))
      {
        // This should be an include definition.  The next element should be the
        // name of the class.  Load and instantiate it and make sure there are
        // no conflicts.
        String className = line.substring(8).trim();

        Class tagClass;
        try
        {
          tagClass = Class.forName(className);
        }
        catch (Exception e)
        {
          LocalizableMessage message = ERR_MAKELDIF_CANNOT_LOAD_TAG_CLASS.get(className);
          throw new MakeLDIFException(message, e);
        }

        Tag tag;
        try
        {
          tag = (Tag) tagClass.newInstance();
        }
        catch (Exception e)
        {
          LocalizableMessage message = ERR_MAKELDIF_CANNOT_INSTANTIATE_TAG.get(className);
          throw new MakeLDIFException(message, e);
        }

        String lowerName = toLowerCase(tag.getName());
        if (registeredTags.containsKey(lowerName) ||
            templateFileIncludeTags.containsKey(lowerName))
        {
          LocalizableMessage message =
              ERR_MAKELDIF_CONFLICTING_TAG_NAME.get(className, tag.getName());
          throw new MakeLDIFException(message);
        }

        templateFileIncludeTags.put(lowerName, tag);
      }
      else if (lowerLine.startsWith("define "))
      {
        // This should be a constant definition.  The rest of the line should
        // contain the constant name, an equal sign, and the constant value.
        int equalPos = line.indexOf('=', 7);
        if (equalPos < 0)
        {
          LocalizableMessage message = ERR_MAKELDIF_DEFINE_MISSING_EQUALS.get(lineNumber);
          throw new MakeLDIFException(message);
        }

        String name  = line.substring(7, equalPos).trim();
        if (name.length() == 0)
        {
          LocalizableMessage message = ERR_MAKELDIF_DEFINE_NAME_EMPTY.get(lineNumber);
          throw new MakeLDIFException(message);
        }

        String lowerName = toLowerCase(name);
        if (templateFileConstants.containsKey(lowerName))
        {
          LocalizableMessage message =
              ERR_MAKELDIF_CONFLICTING_CONSTANT_NAME.get(name, lineNumber);
          throw new MakeLDIFException(message);
        }

        String value = line.substring(equalPos+1);
        if (value.length() == 0)
        {
          LocalizableMessage message = ERR_MAKELDIF_WARNING_DEFINE_VALUE_EMPTY.get(
                  name, lineNumber);
          warnings.add(message);
        }

        templateFileConstants.put(lowerName, value);
      }
      else if (lowerLine.startsWith("branch: "))
      {
        int startLineNumber = lineNumber;
        ArrayList<String> lineList = new ArrayList<>();
        lineList.add(line);
        while (true)
        {
          lineNumber++;
          if (lineNumber >= lines.length)
          {
            break;
          }

          line = lines[lineNumber];
          if (line.length() == 0)
          {
            break;
          }
          line = replaceConstants(line, lineNumber, templateFileConstants, warnings);
          lineList.add(line);
        }

        String[] branchLines = new String[lineList.size()];
        lineList.toArray(branchLines);

        Branch b = parseBranchDefinition(branchLines, lineNumber,
                                         templateFileIncludeTags,
            warnings);
        DN branchDN = b.getBranchDN();
        if (templateFileBranches.containsKey(branchDN))
        {
          LocalizableMessage message = ERR_MAKELDIF_CONFLICTING_BRANCH_DN.get(branchDN, startLineNumber);
          throw new MakeLDIFException(message);
        }
        else
        {
          templateFileBranches.put(branchDN, b);
        }
      }
      else if (lowerLine.startsWith("template: "))
      {
        int startLineNumber = lineNumber;
        ArrayList<String> lineList = new ArrayList<>();
        lineList.add(line);
        while (true)
        {
          lineNumber++;
          if (lineNumber >= lines.length)
          {
            break;
          }

          line = lines[lineNumber];
          if (line.length() == 0)
          {
            break;
          }
          line = replaceConstants(line, lineNumber, templateFileConstants, warnings);
          lineList.add(line);
        }

        String[] templateLines = new String[lineList.size()];
        lineList.toArray(templateLines);

        Template t = parseTemplateDefinition(templateLines, startLineNumber,
                                             templateFileIncludeTags,
                                             templateFileTemplates, warnings);
        String lowerName = toLowerCase(t.getName());
        if (templateFileTemplates.containsKey(lowerName))
        {
          LocalizableMessage message = ERR_MAKELDIF_CONFLICTING_TEMPLATE_NAME.get(t.getName(), startLineNumber);
          throw new MakeLDIFException(message);
        }
        templateFileTemplates.put(lowerName, t);
      }
      else
      {
        LocalizableMessage message =
            ERR_MAKELDIF_UNEXPECTED_TEMPLATE_FILE_LINE.get(line, lineNumber);
        throw new MakeLDIFException(message);
      }
    }


    // If we've gotten here, then we're almost done.  We just need to finalize
    // the branch and template definitions and then update the template file
    // variables.
    for (Branch b : templateFileBranches.values())
    {
      b.completeBranchInitialization(templateFileTemplates);
    }

    for (Template t : templateFileTemplates.values())
    {
      t.completeTemplateInitialization(templateFileTemplates);
    }

    registeredTags.putAll(templateFileIncludeTags);
    constants.putAll(templateFileConstants);
    branches.putAll(templateFileBranches);
    templates.putAll(templateFileTemplates);
  }


  /**
   * Parse a line and replace all constants within [ ] with their
   * values.
   *
   * @param line        The line to parse.
   * @param lineNumber  The line number in the template file.
   * @param constants   The set of constants defined in the template file.
   * @param warnings    A list into which any warnings identified may be
   *                    placed.
   * @return The line in which all constant variables have been replaced
   *         with their value
   */
  private String replaceConstants(String line, int lineNumber,
                                  Map<String,String> constants,
                                  List<LocalizableMessage> warnings)
  {
    int closePos = line.lastIndexOf(']');
    // Loop until we've scanned all closing brackets
    do
    {
      // Skip escaped closing brackets
      while (closePos > 0 &&
          line.charAt(closePos - 1) == '\\')
      {
        closePos = line.lastIndexOf(']', closePos - 1);
      }
      if (closePos > 0)
      {
        StringBuilder lineBuffer = new StringBuilder(line);
        int openPos = line.lastIndexOf('[', closePos);
        // Find the opening bracket. If it's escaped, then it's not a constant
        if ((openPos > 0 && line.charAt(openPos - 1) != '\\')
            || openPos == 0)
        {
          String constantName =
              toLowerCase(line.substring(openPos+1, closePos));
          String constantValue = constants.get(constantName);
          if (constantValue == null)
          {
            LocalizableMessage message = WARN_MAKELDIF_WARNING_UNDEFINED_CONSTANT.get(
                constantName, lineNumber);
            warnings.add(message);
          }
          else
          {
            lineBuffer.replace(openPos, closePos+1, constantValue);
          }
        }
        if (openPos >= 0)
        {
          closePos = openPos;
        }
        line = lineBuffer.toString();
        closePos = line.lastIndexOf(']', closePos);
      }
    } while (closePos > 0);
    return line;
  }

  /**
   * Parses the information contained in the provided set of lines as a MakeLDIF
   * branch definition.
   *
   *
   * @param  branchLines      The set of lines containing the branch definition.
   * @param  startLineNumber  The line number in the template file on which the
   *                          first of the branch lines appears.
   * @param  tags             The set of defined tags from the template file.
   *                          Note that this does not include the tags that are
   *                          always registered by default.
   * @param  warnings         A list into which any warnings identified may be
   *                          placed.
   *
   * @return  The decoded branch definition.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   any of the branch elements.
   *
   * @throws  MakeLDIFException  If some other problem occurs during processing.
   */
  private Branch parseBranchDefinition(String[] branchLines,
                                       int startLineNumber,
                                       Map<String, Tag> tags,
                                       List<LocalizableMessage> warnings)
          throws InitializationException, MakeLDIFException
  {
    // The first line must be "branch: " followed by the branch DN.
    String dnString = branchLines[0].substring(8).trim();
    DN branchDN;
    try
    {
      branchDN = DN.valueOf(dnString);
    }
    catch (Exception e)
    {
      LocalizableMessage message =
          ERR_MAKELDIF_CANNOT_DECODE_BRANCH_DN.get(dnString, startLineNumber);
      throw new MakeLDIFException(message);
    }


    // Create a new branch that will be used for the verification process.
    Branch branch = new Branch(this, branchDN);

    for (int i=1; i < branchLines.length; i++)
    {
      String line       = branchLines[i];
      String lowerLine  = toLowerCase(line);
      int    lineNumber = startLineNumber + i;

      if (lowerLine.startsWith("#"))
      {
        // It's a comment, so we should ignore it.
        continue;
      }
      else if (lowerLine.startsWith("subordinatetemplate: "))
      {
        // It's a subordinate template, so we'll want to parse the name and the
        // number of entries.
        int colonPos = line.indexOf(':', 21);
        if (colonPos <= 21)
        {
          LocalizableMessage message = ERR_MAKELDIF_BRANCH_SUBORDINATE_TEMPLATE_NO_COLON.
              get(lineNumber, dnString);
          throw new MakeLDIFException(message);
        }

        String templateName = line.substring(21, colonPos).trim();

        int numEntries;
        try
        {
          numEntries = Integer.parseInt(line.substring(colonPos+1).trim());
          if (numEntries < 0)
          {
            LocalizableMessage message =
              ERR_MAKELDIF_BRANCH_SUBORDINATE_INVALID_NUM_ENTRIES.
                  get(lineNumber, dnString, numEntries, templateName);
            throw new MakeLDIFException(message);
          }
          else if (numEntries == 0)
          {
            LocalizableMessage message = WARN_MAKELDIF_BRANCH_SUBORDINATE_ZERO_ENTRIES.get(
                    lineNumber, dnString,
                                        templateName);
            warnings.add(message);
          }

          branch.addSubordinateTemplate(templateName, numEntries);
        }
        catch (NumberFormatException nfe)
        {
          LocalizableMessage message =
            ERR_MAKELDIF_BRANCH_SUBORDINATE_CANT_PARSE_NUMENTRIES.
                get(templateName, lineNumber, dnString);
          throw new MakeLDIFException(message);
        }
      }
      else
      {
        TemplateLine templateLine = parseTemplateLine(line, lowerLine,
                                                      lineNumber, branch, null,
                                                      tags, warnings);
        branch.addExtraLine(templateLine);
      }
    }

    return branch;
  }



  /**
   * Parses the information contained in the provided set of lines as a MakeLDIF
   * template definition.
   *
   *
   * @param  templateLines     The set of lines containing the template
   *                           definition.
   * @param  startLineNumber   The line number in the template file on which the
   *                           first of the template lines appears.
   * @param  tags              The set of defined tags from the template file.
   *                           Note that this does not include the tags that are
   *                           always registered by default.
   * @param  definedTemplates  The set of templates already defined in the
   *                           template file.
   * @param  warnings          A list into which any warnings identified may be
   *                           placed.
   *
   * @return  The decoded template definition.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   any of the template elements.
   *
   * @throws  MakeLDIFException  If some other problem occurs during processing.
   */
  private Template parseTemplateDefinition(String[] templateLines,
                                           int startLineNumber,
                                           Map<String, Tag> tags,
                                           Map<String, Template>
                                               definedTemplates,
                                           List<LocalizableMessage> warnings)
          throws InitializationException, MakeLDIFException
  {
    // The first line must be "template: " followed by the template name.
    String templateName = templateLines[0].substring(10).trim();


    // The next line may start with either "extends: ", "rdnAttr: ", or
    // "subordinateTemplate: ".  Keep reading until we find something that's
    // not one of those.
    int                arrayLineNumber    = 1;
    Template           parentTemplate     = null;
    AttributeType[]    rdnAttributes      = null;
    ArrayList<String>  subTemplateNames   = new ArrayList<>();
    ArrayList<Integer> entriesPerTemplate = new ArrayList<>();
    for ( ; arrayLineNumber < templateLines.length; arrayLineNumber++)
    {
      int    lineNumber = startLineNumber + arrayLineNumber;
      String line       = templateLines[arrayLineNumber];
      String lowerLine  = toLowerCase(line);

      if (lowerLine.startsWith("#"))
      {
        // It's a comment.  Ignore it.
        continue;
      }
      else if (lowerLine.startsWith("extends: "))
      {
        String parentTemplateName = line.substring(9).trim();
        parentTemplate = definedTemplates.get(parentTemplateName.toLowerCase());
        if (parentTemplate == null)
        {
          LocalizableMessage message = ERR_MAKELDIF_TEMPLATE_INVALID_PARENT_TEMPLATE.get(
              parentTemplateName, lineNumber, templateName);
          throw new MakeLDIFException(message);
        }
      }
      else if (lowerLine.startsWith("rdnattr: "))
      {
        // This is the set of RDN attributes.  If there are multiple, they may
        // be separated by plus signs.
        ArrayList<AttributeType> attrList = new ArrayList<>();
        String rdnAttrNames = lowerLine.substring(9).trim();
        StringTokenizer tokenizer = new StringTokenizer(rdnAttrNames, "+");
        while (tokenizer.hasMoreTokens())
        {
          attrList.add(DirectoryServer.getAttributeTypeOrDefault(tokenizer.nextToken()));
        }

        rdnAttributes = new AttributeType[attrList.size()];
        attrList.toArray(rdnAttributes);
      }
      else if (lowerLine.startsWith("subordinatetemplate: "))
      {
        // It's a subordinate template, so we'll want to parse the name and the
        // number of entries.
        int colonPos = line.indexOf(':', 21);
        if (colonPos <= 21)
        {
          LocalizableMessage message = ERR_MAKELDIF_TEMPLATE_SUBORDINATE_TEMPLATE_NO_COLON.
              get(lineNumber, templateName);
          throw new MakeLDIFException(message);
        }

        String subTemplateName = line.substring(21, colonPos).trim();

        int numEntries;
        try
        {
          numEntries = Integer.parseInt(line.substring(colonPos+1).trim());
          if (numEntries < 0)
          {
            LocalizableMessage message =
              ERR_MAKELDIF_TEMPLATE_SUBORDINATE_INVALID_NUM_ENTRIES.
                  get(lineNumber, templateName, numEntries, subTemplateName);
            throw new MakeLDIFException(message);
          }
          else if (numEntries == 0)
          {
            LocalizableMessage message = WARN_MAKELDIF_TEMPLATE_SUBORDINATE_ZERO_ENTRIES
                    .get(lineNumber, templateName, subTemplateName);
            warnings.add(message);
          }

          subTemplateNames.add(subTemplateName);
          entriesPerTemplate.add(numEntries);
        }
        catch (NumberFormatException nfe)
        {
          LocalizableMessage message =
            ERR_MAKELDIF_TEMPLATE_SUBORDINATE_CANT_PARSE_NUMENTRIES.
                get(subTemplateName, lineNumber, templateName);
          throw new MakeLDIFException(message);
        }
      }
      else
      {
        // It's something we don't recognize, so it must be a template line.
        break;
      }
    }

    // Create a new template that will be used for the verification process.
    String[] subordinateTemplateNames = new String[subTemplateNames.size()];
    subTemplateNames.toArray(subordinateTemplateNames);

    int[] numEntriesPerTemplate = new int[entriesPerTemplate.size()];
    for (int i=0; i < numEntriesPerTemplate.length; i++)
    {
      numEntriesPerTemplate[i] = entriesPerTemplate.get(i);
    }

    TemplateLine[] parsedLines;
    if (parentTemplate == null)
    {
      parsedLines = new TemplateLine[0];
    }
    else
    {
      TemplateLine[] parentLines = parentTemplate.getTemplateLines();
      parsedLines = new TemplateLine[parentLines.length];
      System.arraycopy(parentLines, 0, parsedLines, 0, parentLines.length);
    }

    Template template = new Template(this, templateName, rdnAttributes,
                                     subordinateTemplateNames,
                                     numEntriesPerTemplate, parsedLines);

    for ( ; arrayLineNumber < templateLines.length; arrayLineNumber++)
    {
      String line       = templateLines[arrayLineNumber];
      String lowerLine  = toLowerCase(line);
      int    lineNumber = startLineNumber + arrayLineNumber;

      if (lowerLine.startsWith("#"))
      {
        // It's a comment, so we should ignore it.
        continue;
      }
      else
      {
        TemplateLine templateLine = parseTemplateLine(line, lowerLine,
                                                      lineNumber, null,
                                                      template, tags, warnings);
        template.addTemplateLine(templateLine);
      }
    }

    return template;
  }



  /**
   * Parses the provided line as a template line.  Note that exactly one of the
   * branch or template arguments must be non-null and the other must be null.
   *
   * @param  line        The text of the template line.
   * @param  lowerLine   The template line in all lowercase characters.
   * @param  lineNumber  The line number on which the template line appears.
   * @param  branch      The branch with which the template line is associated.
   * @param  template    The template with which the template line is
   *                     associated.
   * @param  tags        The set of defined tags from the template file.  Note
   *                     that this does not include the tags that are always
   *                     registered by default.
   * @param  warnings    A list into which any warnings identified may be
   *                     placed.
   *
   * @return  The template line that has been parsed.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   any of the template elements.
   *
   * @throws  MakeLDIFException  If some other problem occurs during processing.
   */
  private TemplateLine parseTemplateLine(String line, String lowerLine,
                                         int lineNumber, Branch branch,
                                         Template template,
                                         Map<String,Tag> tags,
                                         List<LocalizableMessage> warnings)
          throws InitializationException, MakeLDIFException
  {
    // The first component must be the attribute type, followed by a colon.
    int colonPos = lowerLine.indexOf(':');
    if (colonPos < 0)
    {
      if (branch == null)
      {
        LocalizableMessage message = ERR_MAKELDIF_NO_COLON_IN_TEMPLATE_LINE.get(
            lineNumber, template.getName());
        throw new MakeLDIFException(message);
      }
      else
      {
        LocalizableMessage message = ERR_MAKELDIF_NO_COLON_IN_BRANCH_EXTRA_LINE.get(
            lineNumber, branch.getBranchDN());
        throw new MakeLDIFException(message);
      }
    }
    else if (colonPos == 0)
    {
      if (branch == null)
      {
        LocalizableMessage message = ERR_MAKELDIF_NO_ATTR_IN_TEMPLATE_LINE.get(
            lineNumber, template.getName());
        throw new MakeLDIFException(message);
      }
      else
      {
        LocalizableMessage message = ERR_MAKELDIF_NO_ATTR_IN_BRANCH_EXTRA_LINE.get(
            lineNumber, branch.getBranchDN());
        throw new MakeLDIFException(message);
      }
    }

    AttributeType attributeType = DirectoryServer.getAttributeTypeOrDefault(lowerLine.substring(0, colonPos));

    // First, check whether the value is an URL value: <attrName>:< <url>
    int length = line.length();
    int pos    = colonPos + 1;
    boolean valueIsURL = false;
    boolean valueIsBase64 = false;
    if (pos < length)
    {
      if (lowerLine.charAt(pos) == '<')
      {
        valueIsURL = true;
        pos ++;
      }
      else if (lowerLine.charAt(pos) == ':')
      {
        valueIsBase64 = true;
        pos ++;
      }
    }
    //  Then, find the position of the first non-blank character in the line.
    while (pos < length && lowerLine.charAt(pos) == ' ')
    {
      pos++;
    }

    if (pos >= length)
    {
      // We've hit the end of the line with no value.  We'll allow it, but add a
      // warning.
      if (branch == null)
      {
        LocalizableMessage message = WARN_MAKELDIF_NO_VALUE_IN_TEMPLATE_LINE.get(
                lineNumber, template.getName());
        warnings.add(message);
      }
      else
      {
        LocalizableMessage message = WARN_MAKELDIF_NO_VALUE_IN_BRANCH_EXTRA_LINE.get(
                lineNumber, branch.getBranchDN());
        warnings.add(message);
      }
    }


    // Define constants that specify what we're currently parsing.
    final int PARSING_STATIC_TEXT     = 0;
    final int PARSING_REPLACEMENT_TAG = 1;
    final int PARSING_ATTRIBUTE_TAG   = 2;
    final int PARSING_ESCAPED_CHAR    = 3;

    int phase = PARSING_STATIC_TEXT;
    int previousPhase = PARSING_STATIC_TEXT;

    ArrayList<Tag> tagList = new ArrayList<>();
    StringBuilder buffer = new StringBuilder();

    for ( ; pos < length; pos++)
    {
      char c = line.charAt(pos);
      switch (phase)
      {
        case PARSING_STATIC_TEXT:
          switch (c)
          {
            case '\\':
              phase = PARSING_ESCAPED_CHAR;
              previousPhase = PARSING_STATIC_TEXT;
              break;
            case '<':
              if (buffer.length() > 0)
              {
                StaticTextTag t = new StaticTextTag();
                String[] args = new String[] { buffer.toString() };
                t.initializeForBranch(this, branch, args, lineNumber,
                    warnings);
                tagList.add(t);
                buffer = new StringBuilder();
              }

              phase = PARSING_REPLACEMENT_TAG;
              break;
            case '{':
              if (buffer.length() > 0)
              {
                StaticTextTag t = new StaticTextTag();
                String[] args = new String[] { buffer.toString() };
                t.initializeForBranch(this, branch, args, lineNumber,
                                      warnings);
                tagList.add(t);
                buffer = new StringBuilder();
              }

              phase = PARSING_ATTRIBUTE_TAG;
              break;
            default:
              buffer.append(c);
          }
          break;

        case PARSING_REPLACEMENT_TAG:
          switch (c)
          {
            case '\\':
              phase = PARSING_ESCAPED_CHAR;
              previousPhase = PARSING_REPLACEMENT_TAG;
              break;
            case '>':
              Tag t = parseReplacementTag(buffer.toString(), branch, template,
                                          lineNumber, tags, warnings);
              tagList.add(t);
              buffer = new StringBuilder();

              phase = PARSING_STATIC_TEXT;
              break;
            default:
              buffer.append(c);
              break;
          }
          break;

        case PARSING_ATTRIBUTE_TAG:
          switch (c)
          {
            case '\\':
              phase = PARSING_ESCAPED_CHAR;
              previousPhase = PARSING_ATTRIBUTE_TAG;
              break;
            case '}':
              Tag t = parseAttributeTag(buffer.toString(), branch, template,
                                        lineNumber, warnings);
              tagList.add(t);
              buffer = new StringBuilder();

              phase = PARSING_STATIC_TEXT;
              break;
            default:
              buffer.append(c);
              break;
          }
          break;

        case PARSING_ESCAPED_CHAR:
          buffer.append(c);
          phase = previousPhase;
          break;
      }
    }

    if (phase == PARSING_STATIC_TEXT)
    {
      if (buffer.length() > 0)
      {
        StaticTextTag t = new StaticTextTag();
        String[] args = new String[] { buffer.toString() };
        t.initializeForBranch(this, branch, args, lineNumber, warnings);
        tagList.add(t);
      }
    }
    else
    {
      LocalizableMessage message = ERR_MAKELDIF_INCOMPLETE_TAG.get(lineNumber);
      throw new InitializationException(message);
    }

    Tag[] tagArray = new Tag[tagList.size()];
    tagList.toArray(tagArray);
    return new TemplateLine(attributeType, lineNumber, tagArray, valueIsURL,
        valueIsBase64);
  }



  /**
   * Parses the provided string as a replacement tag.  Exactly one of the branch
   * or template must be null, and the other must be non-null.
   *
   * @param  tagString   The string containing the encoded tag.
   * @param  branch      The branch in which this tag appears.
   * @param  template    The template in which this tag appears.
   * @param  lineNumber  The line number on which this tag appears in the
   *                     template file.
   * @param  tags        The set of defined tags from the template file.  Note
   *                     that this does not include the tags that are always
   *                     registered by default.
   * @param  warnings    A list into which any warnings identified may be
   *                     placed.
   *
   * @return  The replacement tag parsed from the provided string.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   the tag.
   *
   * @throws  MakeLDIFException  If some other problem occurs during processing.
   */
  private Tag parseReplacementTag(String tagString, Branch branch,
                                  Template template, int lineNumber,
                                  Map<String,Tag> tags,
                                  List<LocalizableMessage> warnings)
          throws InitializationException, MakeLDIFException
  {
    // The components of the replacement tag will be separated by colons, with
    // the first being the tag name and the remainder being arguments.
    StringTokenizer tokenizer = new StringTokenizer(tagString, ":");
    String          tagName      = tokenizer.nextToken().trim();
    String          lowerTagName = toLowerCase(tagName);

    Tag t = getTag(lowerTagName);
    if (t == null)
    {
      t = tags.get(lowerTagName);
      if (t == null)
      {
        LocalizableMessage message = ERR_MAKELDIF_NO_SUCH_TAG.get(tagName, lineNumber);
        throw new MakeLDIFException(message);
      }
    }

    ArrayList<String> argList = new ArrayList<>();
    while (tokenizer.hasMoreTokens())
    {
      argList.add(tokenizer.nextToken().trim());
    }

    String[] args = new String[argList.size()];
    argList.toArray(args);


    Tag newTag;
    try
    {
      newTag = t.getClass().newInstance();
    }
    catch (Exception e)
    {
      throw new MakeLDIFException(ERR_MAKELDIF_CANNOT_INSTANTIATE_NEW_TAG.get(tagName, lineNumber, e), e);
    }


    if (branch == null)
    {
      newTag.initializeForTemplate(this, template, args, lineNumber, warnings);
    }
    else
    {
      if (newTag.allowedInBranch())
      {
        newTag.initializeForBranch(this, branch, args, lineNumber, warnings);
      }
      else
      {
        LocalizableMessage message = ERR_MAKELDIF_TAG_NOT_ALLOWED_IN_BRANCH.get(
            newTag.getName(), lineNumber);
        throw new MakeLDIFException(message);
      }
    }

    return newTag;
  }



  /**
   * Parses the provided string as an attribute tag.  Exactly one of the branch
   * or template must be null, and the other must be non-null.
   *
   * @param  tagString   The string containing the encoded tag.
   * @param  branch      The branch in which this tag appears.
   * @param  template    The template in which this tag appears.
   * @param  lineNumber  The line number on which this tag appears in the
   *                     template file.
   * @param  warnings    A list into which any warnings identified may be
   *                     placed.
   *
   * @return  The attribute tag parsed from the provided string.
   *
   * @throws  InitializationException  If a problem occurs while initializing
   *                                   the tag.
   *
   * @throws  MakeLDIFException  If some other problem occurs during processing.
   */
  private Tag parseAttributeTag(String tagString, Branch branch,
                                Template template, int lineNumber,
                                List<LocalizableMessage> warnings)
          throws InitializationException, MakeLDIFException
  {
    // The attribute tag must have at least one argument, which is the name of
    // the attribute to reference.  It may have a second argument, which is the
    // number of characters to use from the attribute value.  The arguments will
    // be delimited by colons.
    StringTokenizer   tokenizer = new StringTokenizer(tagString, ":");
    ArrayList<String> argList   = new ArrayList<>();
    while (tokenizer.hasMoreTokens())
    {
      argList.add(tokenizer.nextToken());
    }

    String[] args = new String[argList.size()];
    argList.toArray(args);

    AttributeValueTag tag = new AttributeValueTag();
    if (branch == null)
    {
      tag.initializeForTemplate(this, template, args, lineNumber, warnings);
    }
    else
    {
      tag.initializeForBranch(this, branch, args, lineNumber, warnings);
    }

    return tag;
  }



  /**
   * Retrieves a File object based on the provided path.  If the given path is
   * absolute, then that absolute path will be used.  If it is relative, then it
   * will first be evaluated relative to the current working directory.  If that
   * path doesn't exist, then it will be evaluated relative to the resource
   * path.  If that path doesn't exist, then it will be evaluated relative to
   * the directory containing the template file.
   *
   * @param  path  The path provided for the file.
   *
   * @return  The File object for the specified path, or <CODE>null</CODE> if
   *          the specified file could not be found.
   */
  public File getFile(String path)
  {
    // First, see if the file exists using the given path.  This will work if
    // the file is absolute, or it's relative to the current working directory.
    File f = new File(path);
    if (f.exists())
    {
      return f;
    }


    // If the provided path was absolute, then use it anyway, even though we
    // couldn't find the file.
    if (f.isAbsolute())
    {
      return f;
    }


    // Try a path relative to the resource directory.
    String newPath = resourcePath + File.separator + path;
    f = new File(newPath);
    if (f.exists())
    {
      return f;
    }


    // Try a path relative to the template directory, if it's available.
    if (templatePath != null)
    {
      newPath = templatePath = File.separator + path;
      f = new File(newPath);
      if (f.exists())
      {
        return f;
      }
    }

    return null;
  }



  /**
   * Retrieves the lines of the specified file as a string array.  If the result
   * is already cached, then it will be used.  If the result is not cached, then
   * the file data will be cached so that the contents can be re-used if there
   * are multiple references to the same file.
   *
   * @param  file  The file for which to retrieve the contents.
   *
   * @return  An array containing the lines of the specified file.
   *
   * @throws  IOException  If a problem occurs while reading the file.
   */
  public String[] getFileLines(File file) throws IOException
  {
    String absolutePath = file.getAbsolutePath();
    String[] lines = fileLines.get(absolutePath);
    if (lines == null)
    {
      List<String> lineList = readLines(file);

      lines = new String[lineList.size()];
      lineList.toArray(lines);
      lineList.clear();
      fileLines.put(absolutePath, lines);
    }

    return lines;
  }



  /**
   * Generates the LDIF content and writes it to the provided LDIF writer.
   *
   * @param  entryWriter  The entry writer that should be used to write the
   *                      entries.
   *
   * @return  The result that indicates whether processing should continue.
   *
   * @throws  IOException  If an error occurs while writing to the LDIF file.
   *
   * @throws  MakeLDIFException  If some other problem occurs.
   */
  public TagResult generateLDIF(EntryWriter entryWriter)
         throws IOException, MakeLDIFException
  {
    for (Branch b : branches.values())
    {
      TagResult result = b.writeEntries(entryWriter);
      if (!result.keepProcessingTemplateFile())
      {
        return result;
      }
    }

    entryWriter.closeEntryWriter();
    return TagResult.SUCCESS_RESULT;
  }
}

