/*
 * AbstractResource 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 ts.util.text.StringSequence;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.List;
import java.util.ArrayList;

/**
 * リソースクラスの抽象クラス。
 * <br>
 * リソースクラスに共通の実装を定義する。
 *
 * @author  佐藤隆之. 
 * @version $Revision: 1.1.1.1 $, $Date: 2010-10-16 00:03:48 $
 */
public abstract class AbstractResource implements Resource
{
  /** キーの要素の区切り文字。 */
  private String elementSeparator_ = ".";

  /** キーの属性を囲む文字の配列。 */
  private String[] attributeParenthesis_ = { "(", ")" };

  /** キーの属性の区切り文字。 */
  private String attributeSeparator_ = ",";

  /** キーの属性の名前と値を結び付ける文字。 */
  private String attributeMatchMark_ = "=";

  /** リソースファイルパス。 */
  private String path_ = null;

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

  /**
   * コピーコンストラクタ。
   *
   * @param  res コピー元のリソースオブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public AbstractResource(AbstractResource res)
  {
    assert (res != null) : "@param:res is null.";

    setElementSeparator(res.elementSeparator());
    setAttributeSeparator(res.attributeSeparator());
    setAttributeMatchMark(res.attributeMatchMark());

    String[] par = res.attributeParenthesis();
    setAttributeParenthesis(par[0], par[1]);

    path_ = res.path_;
  }

  /**
   * キーの要素の区切り文字を設定する。
   *
   * @param  separator キーの要素の区切り文字。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #setElementSeparator(String)}を使用して下さい。
   */
  @Deprecated
  protected void setElementSeparator(char separator)
  {
    elementSeparator_ = String.valueOf(separator);
  }

  /**
   * キーの要素の区切り文字を設定する。
   * <br>
   * 引数の文字列が１文字でない場合は例外をスローする。
   *
   * @param  separator キーの要素の区切り文字。
   * @throws IllegalArgumentException 引数の文字列が１文字でない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）
   */
  protected void setElementSeparator(String separator)
  {
    assert (separator != null) : "@param:separator is null.";

    if (StringOperation.length(separator) != 1) {
      throw new IllegalArgumentException(
        "@param:separator is not a charactor.");
    }

    elementSeparator_ = separator;
  }

  /**
   * キーの要素の区切り文字を取得する。
   *
   * @return キーの要素の区切り文字。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #elementSeparator()}を使用して下さい。
   */
  @Deprecated
  public char getElementSeparator()
  {
    return elementSeparator_.charAt(0) ;
  }

  /**
   * キーの要素の区切り文字を取得する。
   *
   * @return キーの要素の区切り文字。
   */
  public String elementSeparator()
  {
    return elementSeparator_ ;
  }

  /**
   * キーの属性を囲む文字を設定する。
   *
   * @param  openPar キーの属性を囲む開き括弧。
   * @param  closePar キーの属性を囲む閉じ括弧。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #setAttributeParenthesis(String,String)}を使用して下さい。
   */
  @Deprecated
  protected void setAttributeParenthesis(char openPar, char closePar)
  {
    attributeParenthesis_[0] = String.valueOf(openPar);
    attributeParenthesis_[1] = String.valueOf(closePar);
  }

  /**
   * キーの属性を囲む文字を設定する。
   * <br>
   * 引数の文字列が１文字でない場合は例外をスローする。
   *
   * @param  openPar キーの属性を囲む開き括弧。
   * @param  closePar キーの属性を囲む閉じ括弧。
   * @throws IllegalArgumentException 引数の文字列が１文字でない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）
   */
  protected void setAttributeParenthesis(String openPar, String closePar)
  {
    assert (openPar != null) : "@param:openPar is null.";
    assert (closePar != null) : "@param:closePar is null.";

    if (StringOperation.length(openPar) != 1) {
      throw new IllegalArgumentException(
        "@param:openPar is not a charactor.");
    }

    if (StringOperation.length(closePar) != 1) {
      throw new IllegalArgumentException(
        "@param:closePar is not a charactor.");
    }

    attributeParenthesis_[0] = openPar;
    attributeParenthesis_[1] = closePar;
  }

  /**
   * キーの属性を囲む文字を取得する。
   *
   * @return キーの属性を囲む文字の配列。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #attributeParenthesis()}を使用して下さい。
   */
  @Deprecated
  public char[] getAttributeParenthesis()
  {
    char[] arr = new char[2];
    arr[0] = attributeParenthesis_[0].charAt(0);
    arr[1] = attributeParenthesis_[1].charAt(0);
    return arr;
  }

  /**
   * キーの属性を囲む文字を取得する。
   *
   * @return キーの属性を囲む文字の配列。
   */
  public String[] attributeParenthesis()
  {
    return attributeParenthesis_ ;
  }

  /**
   * キーの属性の区切り文字を設定する。
   *
   * @param  separator キーの属性の区切り文字。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #setAttributeSeparator(char)}を使用して下さい。
   */
  @Deprecated
  protected void setAttributeSeparator(char separator)
  {
    attributeSeparator_ = String.valueOf(separator);
  }

  /**
   * キーの属性の区切り文字を設定する。
   * <br>
   * 引数の文字列が１文字でない場合は例外をスローする。
   *
   * @param  separator キーの属性の区切り文字。
   * @throws IllegalArgumentException 引数の文字列が１文字でない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）
   */
  protected void setAttributeSeparator(String separator)
  {
    assert (separator != null) : "@param:separator is null.";

    if (StringOperation.length(separator) != 1) {
      throw new IllegalArgumentException(
        "@param:separator is not a charactor.");
    }

    attributeSeparator_ = separator;
  }

  /**
   * キーの属性の区切り文字を取得する。
   *
   * @return キーの属性の区切り文字。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #attributeSeparator()}を使用して下さい。
   */
  @Deprecated
  public char getAttributeSeparator()
  {
    return attributeSeparator_.charAt(0) ;
  }

  /**
   * キーの属性の区切り文字を取得する。
   *
   * @return キーの属性の区切り文字。
   */
  public String attributeSeparator()
  {
    return attributeSeparator_ ;
  }

  /**
   * キーの属性の名前と値を結び付ける文字を設定する。
   *
   * @param  mark キーの属性の名前と値を結び付ける文字。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #setAttributeMatchMark(String)}を使用して下さい。
   */
  @Deprecated
  protected void setAttributeMatchMark(char mark)
  {
    attributeMatchMark_ = String.valueOf(mark);
  }

  /**
   * キーの属性の名前と値を結び付ける文字を設定する。
   * <br>
   * 引数の文字列が１文字でない場合は例外をスローする。
   *
   * @param  mark キーの属性の名前と値を結び付ける文字。
   * @throws IllegalArgumentException 引数の文字列が１文字でない場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）
   */
  protected void setAttributeMatchMark(String mark)
  {
    assert (mark != null) : "@param:mark is null.";

    if (StringOperation.length(mark) != 1) {
      throw new IllegalArgumentException(
        "@param:mark is not a charactor.");
    }

    attributeMatchMark_ = mark;
  }

  /**
   * キーの属性の名前と値を結び付ける文字を取得する。
   *
   * @return キーの属性の名前と値を結び付ける文字を取得する。
   * @deprecated <char>char</code>変数の使用を避けるため廃止。代わりに{@link
   *   #attributeMatchMark()}を使用して下さい。
   */
  @Deprecated
  public char getAttributeMatchMark()
  {
    return attributeMatchMark_.charAt(0) ;
  }

  /**
   * キーの属性の名前と値を結び付ける文字を取得する。
   *
   * @return キーの属性の名前と値を結び付ける文字を取得する。
   */
  public String attributeMatchMark()
  {
    return attributeMatchMark_ ;
  }

  /**
   * 指定されたキー要素の要素名を取得する。
   * <br>
   * {@link #splitKey(String) splitKey}メソッドによって分割されたキー要素から
   * 要素名を取り出す。
   *
   * @param  keyElem キー要素。
   * @return 要素名。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected String getNameOfKeyElement(String keyElem)
  {
    assert (keyElem != null) : "@param:keyElem is null.";

    int index = StringOperation.indexOf(keyElem, attributeParenthesis()[0]);
    return (index < 0) ? keyElem : StringOperation.substring(keyElem, 0, index);
  }

  /**
   * 指定されたキー要素の属性の名前と値の組を列挙する。
   * <br>
   * {@link #splitKey(String) splitKey}メソッドによって分割されたキー要素から
   * 属性を取り出し、属性の名前と値の配列を列挙オブジェクトに格納して返す。
   *
   * @param  keyElem キー要素。
   * @return 属性の名前と値の配列を格納した列挙オブジェクト。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected List<String[]> listAttributesOfKeyElement(String keyElem)
  {
    assert (keyElem != null) : "@param:keyElem is null";

    final int openParLen = StringOperation.length(attributeParenthesis()[0]);
    final int closeParLen = StringOperation.length(attributeParenthesis()[1]);
    final int attrSepLen = StringOperation.length(attributeSeparator());
    final int matchMarkLen = StringOperation.length(attributeMatchMark());

    List<String[]> attrLst = new ArrayList<String[]>();

    StringSequence seq = new StringSequence(keyElem);
    if (! seq.nextUntil(attributeParenthesis()[0]).validIndex()) {
      return attrLst;
    }

    StringSequence bgn = seq.copy();
    String[] attr = new String[2];
    for ( ; seq.validIndex(); seq.next()) {
      if (attr[0] == null && seq.startsWith(attributeMatchMark())) { 
        attr[0] = bgn.followingString(seq);
        seq.next(matchMarkLen - 1);
        bgn = seq.copy();
      }
      else if (seq.startsWith(attributeSeparator())) {
        attr[1] = bgn.followingString(seq);
        attrLst.add(attr);
        attr = new String[2];
        seq.next(attrSepLen - 1);
        bgn = seq.copy();
      }
      else if (seq.startsWith(attributeParenthesis()[1])) {
        break;
      }
    }

    attr[1] = bgn.followingString(seq);
    attrLst.add(attr);
    return attrLst;
  }

  /**
   * キーを要素ごとに分割する。
   * <br>
   * 分割する際にキーの書式の検証も行い、以下に反する場合に例外をスローする。
   * <ul>
   * <li>キーが空文字列でも有効とする（基底要素を表す）</li>
   * <li>属性を持つ要素は、属性の閉じ括弧で終了していなければならない</li>
   * <li>属性の括弧が開いたまま、要素が終了してはならない</li>
   * <li>空の要素名は許されない</li>
   * <li>空の属性名は許されない</li>
   * <li>一つの属性の中に名前と値の等号が存在しなければならない。等号が複数
   *     存在しない場合は最初の等号のみ有効とし、二番目以降は値の一部として
   *     解釈する。</li>
   * </ul>
   *
   * @param  key キー。
   * @throws IllegalKeyException キーが不正な場合。
   */
  protected List<String> splitKey(String key) throws IllegalKeyException
  {
    assert (key != null) : "@param:key is null.";

    final int elemSepLen = StringOperation.length(elementSeparator());
    final int attrSepLen = StringOperation.length(attributeSeparator());
    final int openParLen = StringOperation.length(attributeParenthesis()[0]);
    final int closeParLen = StringOperation.length(attributeParenthesis()[1]);
    final int matchMarkLen = StringOperation.length(attributeMatchMark());

    List<String> lst = new ArrayList<String>();
    if (StringOperation.isEmpty(key)) {
      return lst;
    }

    StringSequence seq = new StringSequence(key, -1);
    StringSequence elemBgn = seq.copy();
    L0:for (seq.next(); seq.validIndex(); seq.next()) {
      if (seq.startsWith(elementSeparator())) {

        String elemName = elemBgn.followingString(seq);
        if (StringOperation.isEmpty(elemName)) {
          throw new ElementNameEmptyException(key);
        }
        lst.add(elemName);
        seq.next(elemSepLen - 1);
        elemBgn = seq.copy();
      }
      else if (seq.startsWith(attributeParenthesis()[0])) {

        String elemName = elemBgn.followingString(seq);
        if (StringOperation.isEmpty(elemName)) {
          throw new ElementNameEmptyException(key);
        }
        seq.next(openParLen - 1);

        StringSequence attrBgn = seq.copy();
        boolean existMatchMark = false;
        for ( ; seq.validIndex(); seq.next()) {
          if (!existMatchMark && seq.startsWith(attributeMatchMark())) {

            String attrName = attrBgn.followingString(seq);
            if (StringOperation.isEmpty(attrName)) {
              throw new AttributeNameEmptyException(key);
            }
            existMatchMark = true;
            seq.next(matchMarkLen - 1);
          }
          else if (seq.startsWith(attributeSeparator())) {
            if (! existMatchMark) {
              throw new AttributeNoMatchMarkException(key);
            }
            existMatchMark = false;
            seq.next(attrSepLen - 1);
            attrBgn = seq.copy();
          }
          else if (seq.startsWith(attributeParenthesis()[1])) {
            if (! existMatchMark) {
              throw new AttributeNoMatchMarkException(key);
            }
            existMatchMark = false;
            if (! seq.next(closeParLen).validIndex()) {
              break L0;
            }

            String elem = elemBgn.followingString(seq);
            lst.add(elem);
            if (! seq.startsWith(elementSeparator())) {
              throw new ElementBadEndCharException(key);
            }
            elemBgn = seq.copy();
            continue L0;
          }
        }
        throw new AttributeNoCloseParException(key);
      }
    }

    String elemLast = elemBgn.followingString(seq);
    if (StringOperation.isEmpty(elemLast)) {
      throw new ElementNameEmptyException(key);
    }
    lst.add(elemLast);

    return lst;
  }

  /**
   * ロードしたリソースファイルのパスを取得する。
   * <br>
   * まだ一度もリソースファイルの読み込みを行っていない場合は、空文字列を返す。
   *
   * @return ロードしたリソースファイルのパス。
   */
  public String getPath()
  {
    return path_ ;
  }

  /**
   * 指定されたパスのリソースファイルを読み込む。
   *
   * @param  path リソースファイルのパス。
   * @throws FileNotFoundException 指定されたパスのファイルが見つからなかった
   *          場合。
   * @throws IOException ファイルの読み込み中に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  public synchronized void load(String path)
    throws FileNotFoundException, IOException
  {
    assert (path != null) : "@param:path is null.";

    InputStream in = null;
    try {
      in = getInputStream(path);
      load(in);
      path_ = path;
    }
    finally {
      if (in != null) in.close();
    }
  }

  /**
   * 指定されたパスから入力ストリームを取得する。
   *
   * @param  path リソースファイルのパス。
   * @return 引数のパスに対する入力ストリーム。 
   * @throws FileNotFoundException 指定されたパスのファイルが見つからなかった
   *          場合。
   * @throws IOException 入力ストリームのオープン時に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected InputStream getInputStream(String path)
    throws FileNotFoundException, IOException
  {
    assert (path != null) : "@param:path is null.";

    return new FileInputStream(path);
  }

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

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

    OutputStream out = null;
    try {
      out = getOutputStream(path);
      save(out);
    } 
    finally {
      if (out != null) out.close();
    }
  }

  /**
   * 指定されたパスの出力ストリームを取得する。
   *
   * @param  path 保存先のファイルのパス。
   * @return 引数のパスに対する出力ストリーム。
   * @throws IOException 指定されたパスのオープン時に例外が発生した場合。
   * @throws AssertionError 引数がヌルの場合（デバッグモードのみ）。
   */
  protected OutputStream getOutputStream(String path) throws IOException
  {
    assert (path != null) : "@param:path is null.";

    return new FileOutputStream(path);
  }

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