SectionContent.java

/*
** Module   : SectionContent.java
** Abstract : Progress 4GL INI file content class representing one section of data
**
** Copyright (c) 2013-2017 Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 EVL 20130701 Created initial version.
** 004 MAG 20140908 Make key names case insensitive.
*/
/*
** 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.util;

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

/**
 * A class implementing the INI text file key=value section lines. Can contain several lines with
 * key values and also comment lines end empty lines in any place of the section.
 *
 * @author    EVL
 */
public class SectionContent
extends Content
{
   /** Map to store key=value pars. */
   private Map<String, String> keyMap = 
            new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
   
   /** The section name associated with the content. */
   private String name = null;

   /** List to control section line ordering. */
   private List<Renderable> ordering = new LinkedList<Renderable>();
   
   /** The position of the last key=value pair in the ordering list. */
   private int nextKeyNumber = 0;

   /**
    * Constructs an instance of the data section in INI file.
    *
    * @param name 
    *        The name of the section corresponding to the key=value pairs of the INI file.
    */
   public SectionContent(String name)
   {
      super();
      this.name = name;
   }
   
   /**
    * Getting the name of this section.
    * 
    * @return The String with the section name.
    */
   public String getName()
   {
      return name;
   }
   
   /**
    * Writes the content of the data section to the file via Writer object provided.
    * 
    * @param pw 
    *        The writer will be used to save content to the file.
    */
   public void write(PrintWriter pw)
   {
      if (ordering.size() > 0)
      {
         // Write the section name first
         pw.println("[" + name + "]");
         // Key names iterator
         Iterator<Renderable> iterLines = ordering.iterator();
         while (iterLines.hasNext())
         {
            // Get next available Renderable object
            Renderable lineToRender = (Renderable)iterLines.next();
            // And write them to the file
            String nextLine = lineToRender.render();
            // It is possible to have null pointer here if the key was previously removed
            if (nextLine != null)
            {
               pw.println(nextLine);
            }
         }
      }
   }
   
   /**
    * Checks if the content of the section is empty or not.
    * 
    * @return <code>true</code> in case of not emptry, <code>false</code> otherwise.
    */
   public boolean notEmpty()
   {
      // Not empty means we have some elements inside allocated map
      return ordering != null && ordering.size() > 0;
   }
   
   /**
    * Stores the key=value pair into the section internal map.
    * 
    * @param key 
    *        The String key name to store.
    * @param value 
    *        The String value to be associated with the given key name.
    */
   public void store(String key, String value)
   {
      // Find out if the key=value line already in the section
      boolean alreadyInMap = alreadyInSection(key); 
      // Put key=value pair into internal map 
      keyMap.put(key, value);
      // And refresh ordering list if the Renderer is still not there
      if (!alreadyInMap)
      {
         // We have to insert new key after last existed key, not at the end of the list
         // And increase then the next number
         ordering.add(nextKeyNumber++, new KeyRenderer(keyMap, key));
      }
   }

   /**
    * Removes the key=value pair from the section internal map. After this method the caller
    * must reload the INI file to sync up the internal maps. 
    * 
    * @param key 
    *        The String key name to remove.
    */
   public void remove(String key)
   {
      // Find out if the key=value line already in the section and remove them from map only
      if (alreadyInSection(key))
      {
         keyMap.remove(key);
      }
   }

   /**
    * Adds the key=value pair to the section internals assuming this is the new key.
    * 
    * @param key 
    *        The String key name to store.
    * @param value 
    *        The String value to be associated with the given key name.
    */
   public void add(String key, String value)
   {
      // Put key=value pair into internal map 
      keyMap.put(key, value);
      // And refresh ordering list if the Renderer is still not there
      ordering.add(new KeyRenderer(keyMap, key));
      // Remember the position of the next key to add in store() method
      nextKeyNumber = ordering.size();
   }

   /**
    * Stores the empty line or comment iside the section content.
    * 
    * @param textLine 
    *        The ignored text line to store.
    */
   public void store(String textLine)
   {
      // And refresh ordering list
      ordering.add(new TextRenderer(textLine));
   }
   
   /**
    * Stores the line iside the section content as pseudo key. The all next key-value pair will
    * be stored after this line. Simulation of 4GL CHUI crazy apprach.
    * 
    * @param textLine 
    *        The ignored text line to store.
    */
   public void storeAsPseudoKey(String textLine)
   {
      // And refresh ordering list
      ordering.add(nextKeyNumber++, new TextRenderer(textLine));
   }
   
   /**
    * Gets the value associated with the given key inside the section.
    * 
    * @param key 
    *        The String key name of the value to be retrieved.
    * 
    * @return The String value associate with the given key or <code>null</code> if there is no
    *         key=value pair inside the section.
    */
   public String getValueForKey(String key)
   {
      if (keyMap != null)
      {
         return keyMap.get(key);
      }
      
      return null;
   }

   /**
    * Checks if the given key already added to the section internals..
    * 
    * @param key 
    *        The String key name to check.
    * 
    * @return <code>true</code> if the key was already added to internal map, <code>false</code>
    *         otherwise.
    */
   public boolean alreadyInSection(String key)
   {
      if (keyMap != null)
      {
         return keyMap.containsKey(key);
      }
      
      return false;
   }
   
   /**
    * Getting the comma-separated list of the keys for this section.
    * 
    * @return The String with comma-separated key names.
    */
   public String getKeys()
   {
      StringBuilder sb = new StringBuilder();
      
      Iterator<Renderable> iterLines = ordering.iterator();
      int count = 0;
      while (iterLines.hasNext())
      {
         // Get next available Renderable object
         Renderable lineToCheck = (Renderable)iterLines.next();
         // Get the string internal of the renderable, key or text members
         String keyToCheck = lineToCheck.toString();
         // And check if it is a key from key-value map 
         if (alreadyInSection(keyToCheck))
         {
            // This is the comma-separator if there is more than 1 name
            if (count > 0)
            {
               sb.append(EnvironmentOps.PROPATH_SEPARATOR);
            }
            sb.append(keyToCheck);
            count++;
         }
      }
      
      return sb.toString();
   }
   
   /**
    * The interface to present single line and key-value pair in one ordered list.
    */
   public interface Renderable
   {
      /**
       * Gets the string representation of the instance.
       * 
       * @return The String value associated with the given key=value line of the file or line
       *         to be ignored.
       */
      public String render();

      /**
       * Gets the string representation of the instance internal string part.
       * 
       * @return The String value associated with the given key or whole line.
       */
      public String toString();
   }

   /**
    * The class to present single line in one ordered list.
    */
   private static class TextRenderer
   implements Renderable
   {
      /** The content of the whole line. */
      private String text = null;

      /**
       * Constructs an instance of the single line in INI file.
       *
       * @param text 
       *        The text corresponding to the ignored line of the INI file.
       */
      private TextRenderer(String text)
      {
         this.text = text;
      }

      /**
       * Gets the string representation of the ignored line.
       * 
       * @return The String value associated with the given to be ignored.
       */
      public String render()
      {
          return text;
      }

      /**
       * Gets the string representation of the instance internal string part.
       * 
       * @return The String value associated with the given key or whole line.
       */
      public String toString()
      {
         return render();
      }
   }

   /**
    * The class to present key-value pair in one ordered list.
    */
   private static class KeyRenderer
   implements Renderable
   {
      /** The key name.*/
      private String key = null;

      /** The Map containing key=value pairs.*/
      private Map<String, String> lookup = null;

      /**
       * Constructs an instance of the key entry line in INI file.
       *
       * @param lookup 
       *        The Map containing the key-value pairs to look for.
       * @param key 
       *        The key part string of the given key=value pair.
       */
      private KeyRenderer(Map<String, String> lookup, String key)
      {
         this.lookup = lookup;
         this.key    = key;
      }

      /**
       * Gets the string representation of the key=value pair.
       * 
       * @return The String value associated with the given key=value line of the file.
       */
      public String render()
      {
         // Get the value for the known key
         String value = lookup.get(key);

         if (value != null)
            return key + "=" + value;
         else
            return null;
      }

      /**
       * Gets the string representation of the instance internal string part.
       * 
       * @return The String value associated with the given key or whole line.
       */
      public String toString()
      {
         return key;
      }
   }
}