/*
 * PropertyResource class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util.resource;

import ts.util.text.StringOperation;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Properties;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Date;

/**
 * プロパティファイルを対象とするリソースクラス。 
 * <br>
 * Javaプロパティファイルをロードして、その内容から指定されたキーに対する値を
 * 取得する。
 * <br>
 * {@link java.util.Properties}クラスでは、プロパティファイルのキーの
 * プロパティファイルのキーの書式は一般的に、
 * <pre>    要素1.要素2.要素3 = 値</pre>
 * のように記述され、プロパティキーと値を1対1でしか設定できない。
 * <br>
 * これに対し、このクラスでは{@link ts.util.resource.Resource}クラス
 * のリソースキーの書式に従って記述されたプロパティキーを解釈できるようにして
 * おり、これによって１つのキーが複数の値を持ったり、同じキーが属性値によって
 * 異なる値を持つことができるようにしている。
 * <br>
 * 属性を持ったプロパティキーの書式の例を以下に示す。
 * <pre>    要素1.要素2(属性名1\=属性値1,属性名2\=属性値2).要素3 = 値</pre>
 * 但し、属性の名前と値を結合する等号(=)は、エスケープする（'\'記号を前に付加
 * する）必要がある。
 *
 * @author  佐藤隆之. 
 * @version $Revision: 1.2 $, $Date: 2011-08-01 15:29:31 $
 */
public class PropertyResource extends DefaultResource
{
  /** シリアル・バージョン番号。 */
  static final long serialVersionUID = 6818536291353948226L;

  /**
   * デフォルトコンストラクタ。
   */
  public PropertyResource()
  {}

  /**
   * プロパティファイルを引数にとるコンストラクタ。
   *
   * @param  path プロパティファイルのパス。
   * @throws FileNotFoundException 指定したパスのファイルが見つからない場合。
   * @throws IOException ファイルの入出力中に例外が発生した場合。
   */
  public PropertyResource(String path) throws FileNotFoundException, IOException
  {
    load(path);
  }

  /**
   * コピーコンストラクタ。
   *
   * @param  res コピー元のプロパティリソースオブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected PropertyResource(PropertyResource res)
  {
    super(res);
  }

  /**
   * このオブジェクトと同じクラスのリソースオブジェクトを作成する。
   *
   * @return このオブジェクトと同じクラスのリソースオブジェクト。
   */
  @Override
  protected DefaultResource createResource()
  {
    return new PropertyResource(this);
  }

  /**
   * 引数の入力ストリームからリソースファイルの内容を読み込む。
   *
   * @param  stream 入力ストリーム。
   * @throws IOException 読み込み中に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  @Override
  protected void load(InputStream stream) throws IOException
  {
    assert (stream != null) : "@param:stream is null.";

    renew();

    PropertiesReader prop = new PropertiesReader();
    prop.load(stream);
  }

  /**
   * 引数の出力ストリームに、このオブジェクトが保持している内容を保存する。
   *
   * @param  stream 出力ストリーム。
   * @throws IOException ファイルの保存中に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  @Override
  public void save(OutputStream stream) throws IOException
  {
    assert (stream != null) : "@param:stream is null.";

    try {
      PropertiesWriter prop = new PropertiesWriter();
      prop.store(stream, null);
    }
    catch (IOException e) {
      throw e;
    }
    catch (Exception e) {
      IOException ex = new IOException(e.toString());
      ex.setStackTrace(e.getStackTrace());
      throw ex;
    }
  }

  /* -- inner class -- */

  /**
   * プロパティファイルの読み込みを行うためのクラス。
   */
  private class PropertiesReader extends Properties
  {
    /** シリアル番号。 */
    static final long serialVersionUID = 1121939455320561763L;

    /** キーの解釈に使用するリソースファイル。 */
    private PropertyResource loadRes_;

    /**
     * デフォルトコンストラクタ。
     */
    public PropertiesReader()
    {
      loadRes_ = new PropertyResource();
      loadRes_.setElementSeparator(".");
      loadRes_.setAttributeParenthesis("(", ")");
      loadRes_.setAttributeSeparator(",");
      loadRes_.setAttributeMatchMark("=");
    }

    /**
     * プロパティキーと値を設定する。
     *
     * @param  key プロパティキー。
     * @param  value プロパティ値。
     * @return 指定されたプロパティキーに前回設定されていた値。
     * @throws IllegalKeyException 指定されたキーが不正な場合。
     * @throws AssertionError キー又は値がヌルの場合（デバッグモードのみ）。
     */
    @Override
    public Object put(Object key, Object value) throws IllegalKeyException
    {
      assert (key != null) : "@param:key is null.";
      assert (value != null) : "@param:value is null.";
      assert (key instanceof String) : "@param:key is not a string.";
      assert (value instanceof String) : "@param:value is not a string.";

      String sKey = key.toString();
      String sValue = value.toString();

      if (StringOperation.isEmpty(sKey)) {
        if (getBaseElement() == getRootElement()) {
          throw new IllegalKeyException(sKey);
        }
      }

      List<String> keyElemLst = loadRes_.splitKey(sKey);
      int lastIndex = keyElemLst.size() - 1;

      List<String> parentKeyLst = keyElemLst.subList(0, lastIndex);
      Element elem = findOrCreateFirstElement(parentKeyLst);

      String lastKeyElem = keyElemLst.get(lastIndex);
      String elemName = getNameOfKeyElement(lastKeyElem);
      List<String[]> attrLst = listAttributesOfKeyElement(lastKeyElem);

      Element lastElem = new Element(elemName);
      for (String[] attr : attrLst) {
        lastElem.putAttribute(attr[0], attr[1]);
      }
      lastElem.setValue(sValue);

      elem.addChild(lastElem);
      return lastElem;
    }
  }

  /**
   * プロパティファイルの保存をするためのクラス。
   */
  private class PropertiesWriter extends Properties
  {
    /** シリアル番号。 */
    static final long serialVersionUID = -3779072493340204504L;

    /** キーの解釈に使用するリソースファイル。 */
    private PropertyResource loadRes_;

    /** キーと値の配列を格納するリスト。 */
    private List<String[]> elementLst_ = new LinkedList<String[]>();

    /** リストのインデックス。　*/
    private int index_ ;

    /**
     * デフォルトコンストラクタ。
     */
    public PropertiesWriter()
    {
      loadRes_ = new PropertyResource();
      loadRes_.setElementSeparator(".");
      loadRes_.setAttributeParenthesis("(", ")");
      loadRes_.setAttributeSeparator(",");
      loadRes_.setAttributeMatchMark("=");
    }

    /**
     * プロパティキーを格納した列挙オブジェクトを取得する。
     *
     * @return プロパティキーを格納した列挙オブジェクト。
     */
    @Override
    public Enumeration<Object> keys()
    {
      index_ = 0;
      elementLst_.clear();
      listAllElements(getBaseElement(), "", elementLst_);
      return new Enumeration<Object>() {
          public boolean hasMoreElements() {
            return (index_ < elementLst_.size());
          }
          public Object nextElement() {
            String[] kv = elementLst_.get(index_);
            index_ ++;
            return kv[0];
          }
        };
    }

    /**
     * 指定されたプロパティキーに対応するプロパティ値を取得する。
     * <br>
     * 実際には、内部で保持しているインデックスに対応する値を返す。
     * 内部で保持しているインデックスは、
     * {@link ts.util.resource.PropertyResource.PropertiesWriter#keys()}
     * メソッドによって初期化され、そのメソッドの戻り値である
     * {@link java.util.Enumeration Enumeration}オブジェクトの
     * {@link java.util.Enumeration#nextElement() nextElement()}メソッドを
     * 実行するごとにインクリメントされる。
     *
     * @param  key プロパティキー。
     * @return プロパティの値。
     * @throws IndexOutOfBoundsException 内部で保持しているインデックスが
     *           範囲外の場合。
     */
    @Override
    public Object get(Object key)
    {
      if (0 < index_ && index_ <= elementLst_.size()) {
        String[] kv = elementLst_.get(index_ - 1);
        return kv[1];
      }
      else {
        return null;
      }
    }

    /**
     * キーと値の配列をリストに再帰的に格納する。
     *
     * @param  parent 親要素。
     * @param  parentKey 親要素のキー。
     * @param  elemLst 要素を格納するリスト。
     * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
     */
    protected void listAllElements(
      Element parent, String parentKey, List<String[]> elemLst)
    {
      assert (parent != null) : "@param:parent is null.";
      assert (parentKey != null) : "@param:parentKey is null.";
      assert (elemLst != null) : "@param:elemLst is null.";

      List<Element> children = parent.getAllChildren();
      for (Element child : children) {
        StringBuilder kb = new StringBuilder();
        if (! StringOperation.isEmpty(parentKey)) {
          kb.append(parentKey).append(loadRes_.elementSeparator());
        }
        kb.append(child.getName());

        Iterator<Map.Entry<String, String>> attrIt;
        attrIt = child.getAttributes().entrySet().iterator();
        if (attrIt.hasNext()) {
          Map.Entry<String, String> attr = attrIt.next();
          kb.append(loadRes_.attributeParenthesis()[0]);
          kb.append(attr.getKey());
          kb.append(loadRes_.attributeMatchMark());
          kb.append(attr.getValue());
          while (attrIt.hasNext()) {
            attr = attrIt.next();
            kb.append(loadRes_.attributeSeparator());
            kb.append(attr.getKey());
            kb.append(loadRes_.attributeMatchMark());
            kb.append(attr.getValue());
          }
          kb.append(loadRes_.attributeParenthesis()[1]);
        }

        if (child.getValue() != null) {
          String[] kv = new String[2];
          kv[0] = kb.toString();
          kv[1] = child.getValue();
          elemLst.add(kv);
        }

        listAllElements(child, kb.toString(), elemLst);
      }
    }
  }
}
