/**
 * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies
 *
 * Please see distribution for license.
 */
package com.opengamma.component;

import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.io.Resource;

import com.opengamma.OpenGammaRuntimeException;

/**
 * Loads configuration from the INI format file.
 * <p>
 * The format is line-based as follows:<br>
 *  <code>#</code> or <code>;</code> for comment lines (at the start of the line)<br>
 *  </code>${key} = value</code> declares a replacement for use later in the file<br>
 *  </code>${key}</code> is replaced by an earlier replacement declaration<br>
 *  </code>[group]</code> defines the start of a named group of configs<br>
 *  </code>key = value</code> defines a single config element within a group<br>
 *  Everything is trimmed as necessary.
 */
public class ComponentConfigLoader {

  /**
   * Starts the components defined in the specified resource.
   * <p>
   * The specified properties are simple key=value pairs and must not be surrounded with ${}.
   * 
   * @param resource  the config resource to load, not null
   * @param properties  the default set of replacements, not null
   * @return the config, not null
   */
  public ComponentConfig load(Resource resource, ConcurrentMap<String, String> properties) {
    Map<String, String> iniProperties = extractIniProperties(properties);
    properties = adjustProperties(properties);
    
    List<String> lines = readLines(resource);
    ComponentConfig config = new ComponentConfig();
    String group = "global";
    int lineNum = 0;
    for (String line : lines) {
      lineNum++;
      line = line.trim();
      if (line.length() == 0 || line.startsWith("#") || line.startsWith(";")) {
        continue;
      }
      if (line.startsWith("${") && line.substring(2).contains("=") &&
          StringUtils.substringBefore(line, "=").trim().endsWith("}")) {
        parseReplacement(line, properties);
        
      } else {
        line = applyReplacements(line, properties);
        
        if (line.startsWith("[") && line.endsWith("]")) {
          group = line.substring(1, line.length() - 1);
        
        } else {
          String key = StringUtils.substringBefore(line, "=").trim();
          String value = StringUtils.substringAfter(line, "=").trim();
          if (key.length() == 0) {
            throw new IllegalArgumentException("Invalid key, line " + lineNum);
          }
          config.add(group, key, value);
        }
      }
    }
    
    // override config with properties prefixed by INI
    for (Entry<String, String> entry : iniProperties.entrySet()) {
      String iniGroup = StringUtils.substringBefore(entry.getKey(), ".");
      String iniKey = StringUtils.substringAfter(entry.getKey(), ".");
      config.getGroup(iniGroup).put(iniKey, entry.getValue());  // throws exception if iniGroup not found
    }
    return config;
  }

  /**
   * Extracts any properties that start with "INI.".
   * <p>
   * These directly override any INI file settings.
   * 
   * @param properties  the properties, not null
   * @return the extracted set of INI properties, not null
   */
  private Map<String, String> extractIniProperties(ConcurrentMap<String, String> properties) {
    Map<String, String> extracted = new HashMap<String, String>();
    for (String key : properties.keySet()) {
      if (key.startsWith("INI.") && key.substring(4).contains(".")) {
        extracted.put(key.substring(4), properties.get(key));
      }
    }
    return extracted;
  }

  //-------------------------------------------------------------------------
  private ConcurrentMap<String, String> adjustProperties(ConcurrentMap<String, String> input) {
    ConcurrentMap<String, String> map = new ConcurrentHashMap<String, String>();
    for (String key : input.keySet()) {
      map.put("${" + key + "}", input.get(key));
    }
    return map;
  }

  private void parseReplacement(String line, ConcurrentMap<String, String> properties) {
    String key = StringUtils.substringBefore(line, "=").trim();
    String value = StringUtils.substringAfter(line, "=").trim();
    // do not overwrite properties that were passed in
    properties.putIfAbsent(key, value);
  }

  private String applyReplacements(String line, ConcurrentMap<String, String> properties) {
    for (Entry<String, String> entry : properties.entrySet()) {
      line = StringUtils.replace(line, entry.getKey(), entry.getValue());
    }
    return line;
  }

  /**
   * Reads lines from the resource.
   * 
   * @param resource  the resource to read, not null
   * @return the lines, not null
   */
  private List<String> readLines(Resource resource) {
    try {
      return IOUtils.readLines(resource.getInputStream(), "UTF8");
    } catch (IOException ex) {
      throw new OpenGammaRuntimeException("Unable to read resource: " + resource, ex);
    }
  }

}

@@ -4160,32 +4160,718 @@
 et(key));%0A    %7D%0A
+    for (Map.Entry%3CString, String%3E entry : map.entrySet()) %7B%0A      boolean hasChanged = false;%0A      do %7B%0A        String value = entry.getValue();%0A        for (Map.Entry%3CString, String%3E replacementEntry : map.entrySet()) %7B%0A          if (value.contains(replacementEntry.getKey())) %7B%0A            if (replacementEntry.getKey().equals(entry.getKey())) %7B%0A              throw new OpenGammaRuntimeException(%22Property %22 + entry.getKey() + %22 references itself%22);%0A            %7D%0A            hasChanged = true;%0A            value = value.replace(replacementEntry.getKey(), replacementEntry.getValue());%0A          %7D%0A        %7D%0A        map.put(entry.getKey(), value);%0A      %7D while (hasChanged);%0A    %7D%0A
     return map;%0A

Allow properties to reference other property values