/*
 * DelimitedLineRecordReader.java
 * Copyright (C) 2008 Cyber Beans Corporation. All rights reserved.
 */
package jp.co.cybec.cb3.accessor.fileaccess.flat.delimited;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.StringTokenizer;

import jp.co.cybec.cb3.accessor.fileaccess.RecordFieldConstants;
import jp.co.cybec.cb3.accessor.fileaccess.RecordFieldReaderFormat;
import jp.co.cybec.cb3.accessor.fileaccess.RecordFieldReaderFormatable;
import jp.co.cybec.cb3.accessor.fileaccess.flat.FlatFileRecord;
import jp.co.cybec.cb3.accessor.fileaccess.flat.FlatFileRecordImpl;
import jp.co.cybec.cb3.accessor.fileaccess.flat.FlatFileRecordReaderImpl;
import jp.co.cybec.cb3.exception.AccessorAlreadyOpenedException;
import jp.co.cybec.cb3.exception.AccessorNotOpenException;
import jp.co.cybec.cb3.exception.ConfigFileDefinitionException;
import jp.co.cybec.cb3.exception.ConfigFileIOException;
import jp.co.cybec.cb3.exception.ConfigFileNotFoundException;
import jp.co.cybec.cb3.exception.ConfigFileParseException;
import jp.co.cybec.cb3.exception.DataFileIOException;
import jp.co.cybec.cb3.exception.DataFileNotFoundException;
import jp.co.cybec.cb3.exception.DataFormatException;
import jp.co.cybec.cb3.util.ConfigurationLoader;
import jp.co.cybec.cb3.util.logging.ErrorCodeConstant;
import jp.co.cybec.cb3.util.logging.LogMessageConstant;
import jp.co.cybec.cb3.util.logging.LogMessageFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * 可変長ファイルのレコード読み込みクラス。
 * @author Jianming Guo
 */
public class DelimitedLineRecordReader extends FlatFileRecordReaderImpl {
	/** ログ */
	private static final Log LOG = LogFactory.getLog(DelimitedLineRecordReader.class);

	/** メッセージ */
	private static final LogMessageFactory MESSAGE = new LogMessageFactory(
			LogMessageConstant.CB3_MESSAGE_FILENAME);

	/** コンフィグ：設定ファイル情報 */
	private DelimitedLineRecordReaderConfig config = null;

	/** ルールファイルパス */
	private String ruleFilepath = null;

	/** 設定ファイルパス */
	private String configFilepath = null;

	/** 行番号 */
	private int row = 0;

	/**
	 * ファイルをオープンします。
	 *
	 * @param filepath データファイルのパス
	 * @throws AccessorAlreadyOpenedException ファイルがすでにオープンされている場合に発生します。
	 * @throws DataFileNotFoundException ファイルが存在しない時に発生します。
	 * @throws DataFileIOException  レコードの読み込み時のIOエラーにより発生します。
	 */
	@Override
	public void open(String filepath) throws AccessorAlreadyOpenedException,
			DataFileNotFoundException, DataFileIOException {
		config.setFilepath(filepath);
		super.open(filepath);
	}

	/**
	 * 次のレコードを返却します。
	 *
	 * @return 次のレコード。存在しない場合は、nullを返す。
	 * @throws AccessorNotOpenException ファイルがオープンされていない場合に発生します。
	 * @throws DataFileIOException レコードの読み込み時のIOエラーにより発生します。
	 * @throws DataFormatException レコード項目のフォーマットに失敗した場合に発生します。
	 */
	public FlatFileRecord nextRecord() throws AccessorNotOpenException, DataFileIOException, DataFormatException {

		try {
			// ファイルがオープンされていない場合、例外をスローします。
			if (!isOpen()) {
				AccessorNotOpenException ex = new AccessorNotOpenException(MESSAGE.getErrorMessage(
						ErrorCodeConstant.ACCESSOR_FILEACCESS_FLAT_DELIMITED_001,
						LogMessageConstant.CB3_ACCESSOR_FILEACCESS_001, getFilepath()));
				LOG.info(ex.getMessage(), ex);
				throw ex;
			}

			// ファイルから１行データを読み込む
			String nextLine = getNextLine();

			// １レコードのデータをフォーマットして、FlatFileRecordを返却する
			return nextLine != null ? parseLine(nextLine) : null;

		} catch (IOException e) {
			DataFileIOException ex = new DataFileIOException(MESSAGE.getErrorMessage(
					ErrorCodeConstant.ACCESSOR_FILEACCESS_FLAT_DELIMITED_002,
					LogMessageConstant.CB3_ACCESSOR_FILEACCESS_004), e);
			LOG.info(ex.getMessage(), ex);
			throw ex;
		}
	}

	/**
	 * 可変長ファイルの設定ファイルを読み込みます。
	 *
	 * @param key String 設定ファイル名
	 * @throws ConfigFileNotFoundException 設定ファイルが存在しない場合に発生します。
	 * @throws ConfigFileDefinitionException 設定ファイルの間違いにより発生します。
	 * @throws ConfigFileIOException 設定ファイル読み込み時のIOエラーにより発生します。
	 * @throws ConfigFileParseException 設定ファイルのXMLファイル解析が失敗した時に発生します。
	 */
	public void initialize(String key) throws ConfigFileNotFoundException,
		ConfigFileDefinitionException, ConfigFileIOException, ConfigFileParseException {

		// 可変長ファイル設定ファイルを読み込む
		config = (DelimitedLineRecordReaderConfig) ConfigurationLoader.load(
				getRuleFilepath(), key);

		// 設定ファイルパスの設定
		this.configFilepath = key;

		// separatorのデフォルト値を設定
		if (config.getSeparator() == null || config.getSeparator().length() == 0) {
			config.setSeparator(RecordFieldConstants.SEPARATOR_DEFAULT);
		}
		// encodeのデフォルト値を設定
		if (config.getEncode() == null || config.getEncode().length() == 0) {
			config.setEncode(RecordFieldConstants.SYSTEM_ENCODE);
		}

		// ファイルパスを設定
		super.setFilepath(config.getFilepath());
		// エンコードを設定
		super.setEncode(config.getEncode());
	}

	/**
	 * ファイルから１行データを読み込みます。
	 *
	 * @return 読み込んだデータ文字列。
	 * @throws IOException レコードの読み込みに失敗した場合に発生します。
	 */
	private String getNextLine() throws IOException {

		// スキップ行数を取る
		int skipLines = config.getSkipLines();

		if (row == 0 && skipLines > 0) {
		// 一回目読み込み and スキップ行数が設定された場合
			for (int i = 0; i < skipLines; i++) {
				// スキップ
				super.getBufferedReader().readLine();
				// 行をカウント
				row ++;
			}
		}
		// １行データを読み込む
		String nextLine = super.getBufferedReader().readLine();
		// 行をカウント
		row++;

		return nextLine;
	}

	/**
	 * １行データ文字列をFlatFileRecordに変換します。
	 *
	 * @param nextLine １行データの文字列
	 * @return 変換後のFlatFileRecord型レコード
	 * @throws IOException レコードの読み込み時のIOエラーにより発生します。
	 * @throws DataFormatException レコード項目のフォーマットに失敗した場合に発生します。
	 */
	private FlatFileRecord parseLine(String nextLine) throws IOException, DataFormatException {

		if (nextLine == null || nextLine.length() == 0) {
			return null;
		}

		// フィールドリスト
		List<String> tokensOnThisLine = new ArrayList<String>();

		if (config.getQuotechar() != null && config.getQuotechar().length() > 0) {
		// クォート文字が設定された場合

			StringBuffer fieldValue = new StringBuffer();

			// セパレータ
			char separator = config.getSeparator().charAt(0);
			// クォート
			char quotechar = config.getQuotechar().charAt(0);

			// クォート文字フラグ：
			// 一つ目のクォート文字があった場合inQuotes = true
			boolean inQuotes = false;

			do {
				if (inQuotes) {
					// 一つ目のクォート文字があった、二つ目のクォート文字が次の行になる場合
					// ※１レコードが複数行になる場合
					fieldValue.append(System.getProperty("line.separator"));
					nextLine = getNextLine();
					if (nextLine == null) {
						break;
					}
				}
				// 1行データの文字列より１文字ずつ取得、判定して、フィールド値リストを作成する
				for (int i = 0; i < nextLine.length(); i++) {
					// 一文字を取得
					char charVal = nextLine.charAt(i);
					if (charVal == quotechar) {
					// クォート文字の場合
						if (inQuotes && nextLine.length() > (i+1) && nextLine.charAt(i+1) == quotechar) {
							// 一つ目のクォート文字がある and 次の文字もクォート文字の場合(例：[,"ab""ccc",])、
							// クォート文字を１個フィールドの値に設定する
							fieldValue.append(nextLine.charAt(i+1));
							i++;
						} else {
							if (i > 2 && nextLine.charAt(i-1) != separator
									&& nextLine.length() > (i+1) && nextLine.charAt(i+1) != separator) {
								// クォート文字前の文字と後の文字がseparatorじゃない場合(例：[,"ab"def",])、
								// クォート文字を１個フィールドの値に設定する
								fieldValue.append(charVal);
							} else {
								// 一つ目のクォート文字の場合：inQuotes = true
								// 二つ目のクォート文字の場合：inQuotes = false
								inQuotes = !inQuotes;
							}
						}
					} else if (charVal == separator && !inQuotes) {
						// セパレータ文字 and inQuotes＝falseの場合、
						// 一つのフィールド値の取得が完了、リストに追加

						// フィールド項目がリストに追加する。
						tokensOnThisLine.add(fieldValue.toString());
						fieldValue = new StringBuffer();
					} else {
						// １個フィールドの値を設定する。
						fieldValue.append(charVal);
					}
				}
			} while (inQuotes);

			// 最後の値をリストに追加する
			tokensOnThisLine.add(fieldValue.toString());

		} else {
		// クォート文字が設定されない場合
			// セパレータ
			String separator = config.getSeparator();

			// 文字列のトークンを作成する
			StringTokenizer tokenizer = new StringTokenizer(nextLine, separator);
			// 分割したデータをリストにセットする
			while (tokenizer.hasMoreTokens()) {
				tokensOnThisLine.add(tokenizer.nextToken());
			}
		}

		// １レコードを作成する
		FlatFileRecord record = new FlatFileRecordImpl(makeMap(tokensOnThisLine));

		return record;
	}

	/**
	 * フィールド情報とフィールド値リストよりレコードのMap情報を作成します。
	 *
	 * @param dataList フィールド値リスト
	 * @return 作成したマップ情報
	 * @throws DataFormatException レコード項目のフォーマットに失敗した場合に発生します。
	 */
	private LinkedHashMap<String,String> makeMap(List<String> dataList) throws DataFormatException {
		// レコードマップにインスタンスする
		LinkedHashMap<String, String> recordMap = new LinkedHashMap<String, String>();
		// フィールド定義情報
		DelimitedLineRecordFieldConfig field = null;
		// フィールド数
		int fieldCnt = config.getFieldList().size();

		// 設定ファイルのフィールド数とデータファイルのフィールド数のチェック
		if ( config.getFieldList().size() != dataList.size()){
			DataFormatException e = new DataFormatException(
					MESSAGE.getErrorMessage(
							ErrorCodeConstant.ACCESSOR_FILEACCESS_FLAT_DELIMITED_003,
							LogMessageConstant.CB3_ACCESSOR_FILEACCESS_010,
							configFilepath,
							fieldCnt,
							config.getFilepath(),
							dataList.size()));
			LOG.info(e.getMessage(), e);
			throw e;
		}

		// レコードのフィールドを１つずつフォーマットする
		for (int i = 0; i < fieldCnt; i++){
			// フィールド定義情報を取得
			field = config.getFieldList().get(i);

			// フィールド値
			String data = dataList.get(i);

			if (field.getPatternClass() != null && field.getPatternClass().length() > 0) {
			// パターンクラスが設定された場合
				try {
					// ユーザー定義クラスをインスタンスする
					RecordFieldReaderFormatable fieldFormat = (RecordFieldReaderFormatable) Class.forName(field.getPatternClass()).newInstance();

					// パディングした文字を取り除く
					data = fieldFormat.trim(field.getAlign(), field.getPadding(), data);
					// フォーマットする
					data = fieldFormat.format(field.getPattern(), field.getType(), data);

				} catch (DataFormatException e) {
					throw e;
				} catch (Exception e) {
					DataFormatException ex = new DataFormatException(MESSAGE
							.getErrorMessage(
									ErrorCodeConstant.ACCESSOR_FILEACCESS_FLAT_DELIMITED_004,
									LogMessageConstant.CB3_ACCESSOR_FILEACCESS_009, field
											.getPatternClass()));
					LOG.info(ex.getMessage(), ex);
					throw ex;
				}
			} else {
			// パターンクラスが設定されない場合
				// RecordFieldReaderFormatをインスタンスする
				RecordFieldReaderFormat fieldFormat = new RecordFieldReaderFormat(configFilepath, field.getName());

				// パディングした文字を取り除く
				data = fieldFormat.trim(field.getAlign(), field.getPadding(), data);

				// フォーマットする
				data = fieldFormat.format(field.getPattern(), field.getType(), data);
			}
			recordMap.put(field.getName(), data);
		}

		return recordMap;
	}

	/**
	 * ルールファイルパスを返します。<br>
	 * @return ルールファイルパス
	 */
	public String getRuleFilepath() {
		if (ruleFilepath == null) {
			ruleFilepath = RecordFieldConstants.DELIMITED_LINE_FILE_READER_RULE_FILE_NAME;
		}
		return ruleFilepath;
	}

	/**
	 * ルールファイルパスを設定します。<br>
	 * ルールファイルパスを設定しない場合、およびnullを設定した場合はデフォルトのパス
	 * ({@link RecordFieldConstants#DELIMITED_LINE_FILE_READER_RULE_FILE_NAME})が使用されます。
	 * @param path ルールファイルパス
	 */
	public void setRuleFilepath(String path) {
		ruleFilepath = path;
	}
}
