/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.hayabusa.taglib;

import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.db.DBTableModel;
import org.opengion.fukurou.util.ErrorMessage;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.ToString;						// 6.1.1.0 (2015/01/17)
import org.opengion.fukurou.util.ArraySet;						// 6.4.3.4 (2016/03/11)

import static org.opengion.fukurou.util.StringUtil.nval ;
import static org.opengion.fukurou.system.HybsConst.BR;			// 6.1.0.0 (2014/12/26) refactoring

import java.util.Locale ;
import java.util.Set ;
import java.util.TreeSet ;
import java.util.Comparator ;
import java.io.File ;
import java.io.Serializable;
import java.io.IOException ;									// 7.0.5.0 (2019/09/13)

/**
 * ファイル検索リストを元に、action に基づいた処理を行うタグです。
 * command="ENTRY" 時のみ処理を行います。
 *
 * fileQuery などで検索したファイル一覧のDBTableModel を元に、ファイルの
 * コピー(COPY)、移動(MOVE,MODIFY)、削除(DELETE)などの処理を行います。
 * 処理を行うオリジナルファイルは、PARENT,NAME というカラムでなければなりません。
 * このカラム名は、fileQuery の検索時には、必ず作成されるカラムです。
 * また、各アクションに対応するターゲットファイルは、TO_PARENT,TO_NAME という
 * カラムで指定するか、targetDir 属性を利用してフォルダを指定します。
 * TO_PARENT(先フォルダ)と、TO_NAME(先ファイル名)は、処理に応じて、必要なカラムが
 * あれば、自動的に処理します。
 * つまり、TO_PARENT のみの場合は、ファイル名はオリジナルのまま、フォルダのみ変更します。
 * 逆に、TO_NAME の場合は、フォルダはそのままで、ファイル名のみ指定します。
 * 両方同時に指定することも可能です。
 * targetDir 属性で指定する場合は、TO_PARENT のみに同じ値を設定した場合と同じになります。
 * この属性を指定すると、TO_PARENT は無視されます。(TO_NAME は有効です。)
 * COPY、MOVE(,MODIFY) の場合は、指定のフォルダに一括処理可能です。
 * COPY、MOVE(,MODIFY) などの処理で、ターゲットフォルダが存在しないときに、作成するか、エラーにするかは
 * createDir属性 で指定できます。初期値は、(true:作成する) です。
 * これは、COPY先やMOVE(,MODIFY)先が存在している前提のシステムで、不要な箇所に間違ってフォルダを
 * 自動作成されると困る場合に、(false:作成しない) とすれば、間違いに気づく確率が上がります。
 *
 * ※ このタグは、Transaction タグの対象ではありません。
 *
 * @og.formSample
 * ●body：なし
 * ●形式：
 *      ・&lt;og:fileUpdate
 *          action        = "COPY|MOVE|MODIFY|DELETE" アクション属性(必須)
 *          command       = "[ENTRY]"                 ENTRY 時のみ実行します(初期値:ENTRY)
 *          targetDir     = "[指定フォルダ]"          ターゲットとなるフォルダ
 *          createDir     = "[true/false]"            ターゲットとなるフォルダがなければ作成する(true)かどうか(初期値:true)
 *          tableId       = [HybsSystem.TBL_MDL_KEY]  DBTableModel を取り出すキー
 *          outMessage    = "[true/false]"            検索結果のメッセージを表示する(true)かどうかを指定(初期値:true)
 *          displayMsg    = "MSG0040";                処理結果を表示します(初期値:｢　件登録しました。｣)
 *          selectedAll   = "[false/true]"            データを全件選択済みとして処理する(true)かどうか指定(初期値:false)
 *          keepTimeStamp = "[false/true]"            COPY,親違いMOVE(,MODIFY)の時にオリジナルのタイムスタンプを使用するかどうか(初期値:false)
 *      /&gt;
 *
 *    [action属性(必須)]
 *      COPY   オリジナルファイルを、ターゲット(TO_PARENT,TO_NAMEで指定)にコピーします。
 *      MOVE   オリジナルファイルを、ターゲットに移動(COPY+DELETE)/名称変更(RENAME)します。
 *      MODIFY (MOVE と同じ。エンジンの command を利用するための簡易action)
 *      DELETE オリジナルファイルを削除します(ターゲット(TO_PARENT,TO_NAME)は、関係しません)。
 *
 * ●Tag定義：
 *   &lt;og:fileUpdate
 *       action           ○【TAG】アクション[COPY|MOVE|MODIFY|DELETE]をセットします(必須)。
 *       command            【TAG】コマンド[ENTRY]をセットします(初期値:ENTRY)
 *       targetDir          【TAG】ターゲットとなるフォルダを指定します
 *       createDir          【TAG】ターゲットとなるフォルダがなければ、作成するかどうかを指定します(初期値:true)
 *       tableId            【TAG】(通常は使いません)結果のDBTableModelを、sessionに登録するときのキーを指定します
 *       scope              【TAG】キャッシュする場合のスコープ[request/page/session/application]を指定します(初期値:session)
 *       outMessage         【TAG】検索結果のメッセージを表示する/しない[true/false]を指定します(初期値:true)
 *       displayMsg         【TAG】処理結果を画面上に表示するメッセージリソースIDを指定します(初期値:MSG0040[　件登録しました])
 *       selectedAll        【TAG】データを全件選択済みとして処理するかどうか[true/false]を指定します(初期値:false)
 *       keepTimeStamp      【TAG】オリジナルのタイムスタンプを利用するかどうかを指定します(初期値:false)
 *       inPath             【TAG】入力共通パスを指定します(PARENTフォルダの共通部分、COPY/MOVE時にtargetDirと置換されます) 6.8.0.0 (2017/06/02)。
 *       useTimeView        【TAG】処理時間を表示する TimeView を表示するかどうかを指定します
 *										(初期値:VIEW_USE_TIMEBAR[={@og.value SystemData#VIEW_USE_TIMEBAR}])。
 *       useSLabel          【TAG】7.0.7.0 (2019/12/13) ｴﾗｰﾒｯｾｰｼﾞにSLABELを利用するかどうか[true/false]を指定します(初期値:false)
 *       caseKey            【TAG】このタグ自体を利用するかどうかの条件キーを指定します(初期値:null)
 *       caseVal            【TAG】このタグ自体を利用するかどうかの条件値を指定します(初期値:null)
 *       caseNN             【TAG】指定の値が、null/ゼロ文字列 でない場合(Not Null=NN)は、このタグは使用されます(初期値:判定しない)
 *       caseNull           【TAG】指定の値が、null/ゼロ文字列 の場合は、このタグは使用されます(初期値:判定しない)
 *       caseIf             【TAG】指定の値が、true/TRUE文字列の場合は、このタグは使用されます(初期値:判定しない)
 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
 *   /&gt;
 *
 * ●使用例
 *       ・&lt;og:fileUpdate command="{&#064;command}" action="COPY" /&gt;
 *             TO_PARENT または、 TO_NAME(両方指定も可)による行単位 COPY 処理
 *             fileQuery の useUpdateClm="true" を設定し、検索結果に、TO_PARENT、 TO_NAMEカラムを追加します。
 *             TO_PARENT または、 TO_NAME は、columnSet などで値をセットしておきます。
 *
 *       ・&lt;og:fileUpdate command="{&#064;command}" action="MODIFY" targetDir="AAA_DIR"  /&gt;
 *             fileQuery の検索結果を、AAA_DIR フォルダに移動します。
 *             ファイル名は、そのままオリジナルの値が使用されます。
 *
 * @og.rev 5.3.4.0 (2011/04/01) 新規追加
 * @og.group ファイル出力
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class FileUpdateTag extends CommonTagSupport {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "7.0.7.0 (2019/12/13)" ;
	private static final long serialVersionUID = 707020191213L ;

	/** command 引数に渡す事の出来る コマンド  登録{@value} */
	public static final String CMD_ENTRY  = "ENTRY" ;
	/** command 引数に渡す事の出来る コマンド リスト  */
	// 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
	private static final Set<String> COMMAND_SET = new ArraySet<>( CMD_ENTRY );

	/** エラーメッセージID {@value} */
	private static final String ERR_MSG_ID	= HybsSystem.ERR_MSG_KEY;		// 6.4.1.1 (2016/01/16) errMsgId → ERR_MSG_ID  refactoring

	/** action 引数に渡す事の出来る アクションコマンド  COPY {@value} */
	public static final String ACT_COPY		= "COPY" ;
	/** action 引数に渡す事の出来る アクションコマンド  MOVE {@value} */
	public static final String ACT_MOVE		= "MOVE" ;
	/** action 引数に渡す事の出来る アクションコマンド  MODIFY {@value} */
	public static final String ACT_MODIFY		= "MODIFY" ;
	/** action 引数に渡す事の出来る アクションコマンド  DELETE {@value} */
	public static final String ACT_DELETE	= "DELETE" ;
	// 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
	private static final Set<String> ACTION_SET = new ArraySet<>( ACT_COPY , ACT_MOVE , ACT_MODIFY , ACT_DELETE );

	private String	action		;
	private String	targetDir	;			// ターゲットとなるフォルダ
	private boolean	createDir	= true;		// ターゲットとなるフォルダがなければ、作成するかどうか(true:作成する)

	private String	inPath		;			// 6.8.0.0 (2017/06/02) 入力共通パスを指定します。

	private String	tableId		= HybsSystem.TBL_MDL_KEY;
	private String	command		= CMD_ENTRY;
	private boolean	outMessage	= true;
	private String	displayMsg	= "MSG0040";		// 　件登録しました。
	private boolean selectedAll ;
	private boolean keepTimeStamp;					// オリジナルのタイムスタンプを利用する場合、true

	private transient DBTableModel	table		;
	private transient ErrorMessage	errMessage	;
	private int		executeCount	= -1;			// 処理件数
	private int		errCode	 		= ErrorMessage.OK;
	private boolean	useTimeView		= HybsSystem.sysBool( "VIEW_USE_TIMEBAR" );		// 6.3.6.0 (2015/08/16)
	private boolean	useSLabel		;				// 7.0.7.0 (2019/12/13) ｴﾗｰﾒｯｾｰｼﾞにSLABELを利用するかどうか[true/false]を指定します(初期値:false)

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
	 */
	public FileUpdateTag() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
	 *
	 * @og.rev 6.4.4.1 (2016/03/18) 意味のない、StringBuilderだったので、廃止します。
	 * @og.rev 6.9.9.0 (2018/08/20) ｢ERR0041:検索処理中に割り込みの検索要求がありました｣エラーを、標準のErrorMessageに追加するようにします。
	 * @og.rev 7.0.7.0 (2019/12/13) useSLabel 属性を追加。
	 *
	 * @return	後続処理の指示
	 */
	@Override
	public int doEndTag() {
		debugPrint();
		// 5.2.2.0 (2010/11/01) caseKey 、caseVal 属性対応
		if( !useTag() ) { return EVAL_PAGE ; }

		final long dyStart = System.currentTimeMillis();

		table = (DBTableModel)getObject( tableId );

		String label  = "";				// 4.0.0 (2005/11/30) 検索しなかった場合。
		if( table != null && table.getRowCount() > 0 && check( command, COMMAND_SET ) ) {
			startQueryTransaction( tableId );

			execute();	// 実際の処理を実行します。

			setRequestAttribute( "DB.COUNT"   , String.valueOf( executeCount ) );
			setRequestAttribute( "DB.ERR_CODE", String.valueOf( errCode ) );

			// 6.9.9.0 (2018/08/20) ｢ERR0041:検索処理中に割り込みの検索要求がありました｣エラーを、標準のErrorMessageに追加するようにします。
			if( ! commitTableObject( tableId, table ) ) {
				if( errMessage == null ) { errMessage = new ErrorMessage( "FileUpdateTag Query Error!" ); }
				// ERR0041:検索処理中に割り込みの検索要求がありました。処理されません。
				errMessage.addMessage( 0,ErrorMessage.NG,"ERR0041" );	
				errCode = ErrorMessage.NG;
			}

//			final String err = TaglibUtil.makeHTMLErrorTable( errMessage,getResource() );
			final String err = TaglibUtil.makeHTMLErrorTable( errMessage,getResource(),useSLabel );		// 7.0.7.0 (2019/12/13)
			if( err != null && err.length() > 0 ) {
				label = err ;		// 6.4.4.1 (2016/03/18)
				setSessionAttribute( ERR_MSG_ID,errMessage );
			}

//			// 6.9.9.0 (2018/08/20) ｢ERR0041:検索処理中に割り込みの検索要求がありました｣エラーを、標準のErrorMessageに追加するようにします。
//			if( table != null && ! commitTableObject( tableId, table ) ) {
//				jspPrint( "FileUpdateTag Query処理が割り込まれました。DBTableModel は登録しません。" );
//				return SKIP_PAGE ;
//			}
		}

		jspPrint( label );

		// 実行件数の表示
		// 4.0.0 (2005/11/30) 出力順の変更。一番最初に出力します。
		if( displayMsg != null && displayMsg.length() > 0 ) {
			final String status = executeCount + getResource().getLabel( displayMsg ) ;
			jspPrint( status + BR );
		}

		if( useTimeView ) {		// 6.3.6.0 (2015/08/16)
			// 3.5.4.7 (2004/02/06)
			final long dyTime = System.currentTimeMillis()-dyStart;
			jspPrint( "<div id=\"queryTime\" value=\"" + (dyTime) + "\"></div>" );	// 3.5.6.3 (2004/07/12)
		}
		return EVAL_PAGE ;
	}

	/**
	 * タグリブオブジェクトをリリースします。
	 * キャッシュされて再利用されるので、フィールドの初期設定を行います。
	 *
	 * @og.rev 6.8.0.0 (2017/06/02) 入力共通パス(inPath)を指定します。
	 * @og.rev 7.0.7.0 (2019/12/13) useSLabel 属性を追加。
	 */
	@Override
	protected void release2() {
		super.release2();
		tableId		= HybsSystem.TBL_MDL_KEY;
		command		= CMD_ENTRY;
		action		= null;
		targetDir	= null;		// ターゲットとなるフォルダ
		createDir	= true;		// ターゲットとなるフォルダがなければ、作成するかどうか(true:作成する)
		outMessage	= true;
		displayMsg	= "MSG0040";	// 　件登録しました。
		selectedAll = false;
		keepTimeStamp = false;		// オリジナルのタイムスタンプを利用する場合、true
		table		= null;
		errMessage	= null;
		executeCount= -1;		// 処理件数
		errCode	 	= ErrorMessage.OK;
		useTimeView	= HybsSystem.sysBool( "VIEW_USE_TIMEBAR" );	// 6.3.6.0 (2015/08/16)
		inPath		= null;		// 6.8.0.0 (2017/06/02) 入力共通パスを指定します。
		useSLabel	= false;	// 7.0.7.0 (2019/12/13) ｴﾗｰﾒｯｾｰｼﾞにSLABELを利用するかどうか[true/false]を指定します(初期値:false)
	}

	/**
	 * 処理を実行します。
	 *
	 * @og.rev 6.8.0.0 (2017/06/02) 入力共通パス(inPath)を指定します。
	 */
	private void execute() {
		final int[] rowNo = getParameterRows();
		if( rowNo.length > 0 ) {

			final FromToFiles fromToFiles = new FromToFiles( table , targetDir , createDir , inPath );	// 6.8.0.0 (2017/06/02) 入力共通パス(inPath)を指定

			if( ACT_COPY.equalsIgnoreCase( action ) ) {
				actionCOPY( rowNo,fromToFiles );
			}
			// ACT_MODIFY は、エンジンの command で使うため、便利
			else if( ACT_MOVE.equalsIgnoreCase( action ) || ACT_MODIFY.equalsIgnoreCase( action ) ) {
				actionMOVE( rowNo,fromToFiles );
			}
			else if( ACT_DELETE.equalsIgnoreCase( action ) ) {
				actionDELETE( rowNo,fromToFiles );
			}
		}
	}

	/**
	 * COPY アクションを実行します。
	 *
	 * @og.rev 5.6.5.2 (2013/06/21) From側がファイルの場合のみ処理します。
	 *
	 * @param	rowNo		処理を実施する行番号
	 * @param	fromToFiles	FromFile,ToFileをまとめた補助クラス
	 * @throws	HybsSystemException	処理中に何らかのエラーが発生した場合
	 */
	private void actionCOPY( final int[] rowNo , final FromToFiles fromToFiles ) {
		File fromFile = null ;
		File toFile   = null ;

		executeCount = 0 ;	// 開始前に初期化しておく。
		final int rowCount = rowNo.length ;
		for( int i=0; i<rowCount; i++ ) {
			final File[] files = fromToFiles.makeFromToFile( rowNo[i] );	// FromFile,ToFile
			fromFile = files[0];
			toFile   = files[1];

			// 5.6.5.2 (2013/06/21) From側がファイルの場合のみ処理します。
			if( fromFile.isFile() && !FileUtil.copy( fromFile,toFile,keepTimeStamp ) ) {
				final String errMsg = "アクション=[" + action + "]中にエラーが発生しました。" + CR
									+ "From=[" + fromFile + "],To=[" + toFile + "]" + CR;
				// 6.0.2.5 (2014/10/31) refactoring
				if( errMessage == null ) { errMessage = new ErrorMessage( "FileUpdateTag Error" ); }
				errMessage.addMessage( rowNo[i]+1,ErrorMessage.NG,"actionCOPY",errMsg );
			}
			executeCount++ ;
		}
	}

	/**
	 * MOVE アクションを実行します。
	 *
	 * @og.rev 5.5.2.4 (2012/05/16) メソッドの戻り値の設定
	 * @og.rev 5.6.5.2 (2013/06/21) From側がファイルの場合のみ処理します。
	 *
	 * @param	rowNo		処理を実施する行番号
	 * @param	fromToFiles	FromFile,ToFileをまとめた補助クラス
	 * @throws	HybsSystemException	処理中に何らかのエラーが発生した場合
	 */
	private void actionMOVE( final int[] rowNo , final FromToFiles fromToFiles ) {
		File fromFile = null ;
		File toFile   = null ;

		executeCount = 0 ;	// 開始前に初期化しておく。
		final int rowCount = rowNo.length ;
		for( int i=0; i<rowCount; i++ ) {
			final File[] files = fromToFiles.makeFromToFile( rowNo[i] );	// FromFile,ToFile
			fromFile = files[0];
			toFile   = files[1];

			if( fromToFiles.lastParentEquals() ) {	// FromDirとToDirが同じなので、RENAMEできる。
				if( !fromFile.renameTo( toFile ) ) {
					final String errMsg = "アクション=[" + action + "]中にエラーが発生しました。" + CR
										+ "同一親フォルダのため、RENAME処理を行っています。" + CR
										+ "From=[" + fromFile + "],To=[" + toFile + "]" + CR;
					// 6.0.2.5 (2014/10/31) refactoring
					if( errMessage == null ) { errMessage = new ErrorMessage( "FileUpdateTag Error" ); }
					errMessage.addMessage( rowNo[i]+1,ErrorMessage.NG,"actionMOVE",errMsg );
				}
			}
			// 5.6.5.2 (2013/06/21) From側がファイルの場合のみ処理します。
			else if( fromFile.isFile() ) {			// FromDirとToDirが異なるので、COPY ＋ DELETE する。
				if( !FileUtil.copy( fromFile,toFile,keepTimeStamp ) ) {
					final String errMsg = "アクション=[" + action + "]中にエラーが発生しました。" + CR
										+ "移動前のCOPY処理を行っていました。" + CR
										+ "From=[" + fromFile + "],To=[" + toFile + "]" + CR;
					// 6.0.2.5 (2014/10/31) refactoring
					if( errMessage == null ) { errMessage = new ErrorMessage( "FileUpdateTag Error" ); }
					errMessage.addMessage( rowNo[i]+1,ErrorMessage.NG,"actionMOVE",errMsg );
				}

				if( !fromFile.delete() ) {
					String errMsg = "アクション=[" + action + "]中にエラーが発生しました。" + CR
										+ "移動後のオリジナルファイルの削除処理を行っていました。" + CR
										+ "From=[" + fromFile + "],To=[" + toFile + "]" + CR;
					// 5.5.2.4 (2012/05/16) メソッドの戻り値の設定
					if( !toFile.delete() ) {
						errMsg = errMsg + "toFile も削除に失敗しました。" + CR;
					}

					// 6.0.2.5 (2014/10/31) refactoring
					if( errMessage == null ) { errMessage = new ErrorMessage( "FileUpdateTag Error" ); }
					errMessage.addMessage( rowNo[i]+1,ErrorMessage.NG,"actionMOVE",errMsg );
				}
			}
			executeCount++ ;
		}
	}

	/**
	 * DELETE アクションを実行します。
	 *
	 * この処理では、リストにフォルダが含まれている場合も削除します。
	 * 通常、フォルダの削除は、その要素（内部）にファイル等が存在しない場合のみ
	 * 行いますが、検索リストから削除する順番によっては、フォルダもファイルも
	 * 削除対象になる場合があります。そこで、まず。ファイルだけ削除し、フォルダは、
	 * あとで削除するように処理を行います。
	 *
	 * @og.rev 5.6.5.2 (2013/06/21) フォルダも削除対象にします。
	 *
	 * @param	rowNo		処理を実施する行番号
	 * @param	fromToFiles	FromFile,ToFileをまとめた補助クラス
	 * @throws	HybsSystemException	処理中に何らかのエラーが発生した場合
	 */
	private void actionDELETE( final int[] rowNo , final FromToFiles fromToFiles ) {
		File fromFile = null;

		// 5.6.5.2 (2013/06/21) フォルダを削除する為の、退避
		final Set<File> dirSet = new TreeSet<>( new FileNameLengthComparator() );		// ファイルの文字数順に並べたSet

		executeCount = 0 ;	// 開始前に初期化しておく。
		final int rowCount = rowNo.length ;
		for( int i=0; i<rowCount; i++ ) {
			fromFile = fromToFiles.makeFromOnly( rowNo[i] );	// FromFile

			// 5.6.5.2 (2013/06/21) まず、ファイルを削除します。
			if( fromFile.isFile() ) {
				if( !fromFile.delete() ) {
					final String errMsg = "アクション=[" + action + "]中にエラーが発生しました。" + CR
										+ "From=[" + fromFile + "]" + CR;
					// 6.0.2.5 (2014/10/31) refactoring
					if( errMessage == null ) { errMessage = new ErrorMessage( "FileUpdateTag Error" ); }
					errMessage.addMessage( rowNo[i]+1,ErrorMessage.NG,"actionDELETE",errMsg );
				}
			}
			else {
				// 5.6.5.2 (2013/06/21) フォルダの場合は、アドレスの桁数をキーにソートしておきます。
				dirSet.add( fromFile );
			}
			executeCount++ ;
		}

		// 5.6.5.2 (2013/06/21) フォルダの削除は、アドレスの桁数の大きい順（階層の深い順）に削除します。
		for( final File file : dirSet ) {
			if( !file.delete() ) {
				final String errMsg = "アクション=[" + action + "]中にエラーが発生しました。" + CR
									+ "From(Dir)=[" + file + "]" + CR;
				// 6.0.2.5 (2014/10/31) refactoring
				if( errMessage == null ) { errMessage = new ErrorMessage( "FileUpdateTag Error" ); }
				errMessage.addMessage(-1,ErrorMessage.NG,"actionDELETE",errMsg );
			}
		}
	}

	/**
	 * ファイルの名称の長さ順（長い順）に比較する、Comparator インターフェースの実装クラス
	 *
	 * ここでの大小比較は、ファイル名の文字数が、大きい方が、小さいとみなされます。
	 * つまり階層が深いので、先に処理する必要があるという事を意味します。
	 * 処理としては、f1 != null &amp;&amp; f2 != null で、len1 = f1.getAbsolutePath().length() と len2 = f2.getAbsolutePath().length() を比較し
	 * len1 &gt; len2 ⇒ 負 , len1 &lt; len2 ⇒ 正 , len1 == len2 ⇒ 0 を返します。
	 * 具体的には、return ( len2 - len1 ); です。
	 * 
	 * 注: このコンパレータは equals と一貫性のない順序付けを課します。
	 * 
	 * @og.rev 5.6.5.2 (2013/06/21) 新規追加
	 * 
	 */
	private static final class FileNameLengthComparator implements Comparator<File> , Serializable {
		private static final long serialVersionUID = 565220130621L ;		// 5.6.5.2 (2013/06/21)
		/**
		 * 順序付けのために ２つの引数を比較します。
		 *
		 * ここでの大小比較は、ファイル名の文字数が、大きい方が、小さいとみなされます。
		 * 具体的には、return ( len2 - len1 ); です。
		 * 
		 * @param	f1 比較対象の１番目のオブジェクト
		 * @param	f2 比較対象の２番目のオブジェクト
		 * @return	最初の引数が２番目の引数より小さい場合は負の整数、両方が等しい場合は 0、最初の引数が ２番目の引数より大きい場合は正の整数
		 */
		public int compare( final File f1 , final File f2 ) {
			if( f1 == null || f2 == null ) {
				final String errMsg = "引数のFileにnullが含まれています。file1=[" + f1 + "] , file2=[" + f2 + "]" ;
				throw new IllegalArgumentException( errMsg );
			}

			final int len1 = f1.getAbsolutePath().length();
			final int len2 = f2.getAbsolutePath().length();

			return len2 - len1 ;
		}
	}

	/**
	 * 表示データの HybsSystem.ROW_SEL_KEY を元に、選ばれた 行を処理の対象とします。
	 *
	 * @return	選択行の配列
	 * @og.rtnNotNull
	 */
	@Override
	protected int[] getParameterRows() {
		final int[] rowNo ;
		if( selectedAll ) {
			final int rowCnt = table.getRowCount();
			rowNo = new int[ rowCnt ];
			for( int i=0; i<rowCnt; i++ ) {
				rowNo[i] = i;
			}
		} else {
			rowNo = super.getParameterRows();
		}
		return rowNo ;
	}

	/**
	 * 【TAG】アクション[COPY|MOVE|MODIFY|DELETE]をセットします。
	 *
	 * @og.tag
	 * アクションは、ファイルをコピー(COPY)したり、移動(MOVE,MODIFY)したり、削除(DELETE)する
	 * などの操作を指定する必須属性です。
	 *
	 * <table class="plain">
	 *   <caption>action属性(必須)のキーワード</caption>
	 *   <tr><th>action</th><th>名称</th><th>機能</th></tr>
	 *   <tr><td>COPY  </td><td>コピー</td><td>オリジナルファイルを、ターゲット(TO_PARENT,TO_NAMEで指定)にコピーします。</td></tr>
	 *   <tr><td>MOVE  </td><td>移動  </td><td>オリジナルファイルを、ターゲットに移動(COPY+DELETE)/名称変更(RENAME)します。</td></tr>
	 *   <tr><td>MODIFY</td><td>移動  </td><td>(MOVE と同じ。エンジンの command を利用するための簡易action)</td></tr>
	 *   <tr><td>DELETE</td><td>削除  </td><td>オリジナルファイルを、削除します。(フォルダ、ファイルに関わらず)</td></tr>
	 * </table>
	 *
	 * @og.rev 6.3.4.0 (2015/08/01) Arrays.toString から String.join に置き換え。
	 * @og.rev 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
	 *
	 * @param	act アクション (public static final 宣言されている文字列)
	 * @see		<a href="../../../../constant-values.html#org.opengion.hayabusa.taglib.FileUpdateTag.ACT_COPY">アクション定数</a>
	 */
	public void setAction( final String act ) {
		action = nval( getRequestParameter( act ),action );

		if( action != null && !check( action, ACTION_SET ) ) {
			final String errMsg = "指定のアクションは実行できません。アクションエラー"	+ CR
							+ "action=[" + action + "] "								+ CR
							+ "actionList=" + String.join( ", " , ACTION_SET ) ;
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 【TAG】ターゲットとなるフォルダを指定します(初期値:null)。
	 *
	 * @og.tag
	 * targetDir 属性を利用する場合は、引数のファイル、またはフォルダが指定されたことに
	 * なります。COPY、MOVE(,MODIFY) の場合は、targetDir 属性にフォルダを指定することで一括処理可能です。
	 * 指定先のフォルダが存在しない場合は、createDir属性の値により処理が異なります。
	 * createDir="true"(初期値)で、ターゲットフォルダが存在しない場合は、自動作成します。
	 *
	 * @param  dir ターゲットとなるフォルダ
	 * @see		#setCreateDir( String )
	 */
	public void setTargetDir( final String dir ) {
		targetDir = nval( getRequestParameter( dir ),targetDir );
	}

	/**
	 * 【TAG】ターゲットとなるフォルダがなければ、作成するかどうかを指定します(初期値:true)。
	 *
	 * @og.tag
	 * COPY,MOVE(,MODIFY) などの処理で、ターゲットフォルダが存在しないときに、作成するか、エラーにするかを
	 * createDir属性 で指定できます。
	 * これは、COPY先やMOVE(,MODIFY)先が存在している前提のシステムで、不要な箇所に間違ってフォルダを
	 * 自動作成されると困る場合に、false:作成しない とすれば、間違いに気づく確率が上がります。
	 * 初期値は true:作成する です。
	 *
	 * @param	flag	フォルダ作成可否 [true:作成する/false:作成しない]
	 */
	public void setCreateDir( final String flag ) {
		createDir = nval( getRequestParameter( flag ),createDir );
	}

	/**
	 * 【TAG】(通常は使いません)結果のDBTableModelを、sessionに登録するときのキーを指定します
	 *		(初期値:HybsSystem#TBL_MDL_KEY[={@og.value HybsSystem#TBL_MDL_KEY}])。
	 *
	 * @og.tag
	 * 検索結果より、DBTableModelオブジェクトを作成します。これを、下流のviewタグ等に
	 * 渡す場合に、通常は、session を利用します。その場合の登録キーです。
	 * query タグを同時に実行して、結果を求める場合、同一メモリに配置される為、
	 * この tableId 属性を利用して、メモリ空間を分けます。
	 *		(初期値:HybsSystem#TBL_MDL_KEY[={@og.value HybsSystem#TBL_MDL_KEY}])。
	 *
	 * @param	id テーブルID (sessionに登録する時のID)
	 */
	public void setTableId( final String id ) {
		tableId = nval( getRequestParameter( id ),tableId );
	}

	/**
	 * 【TAG】コマンド (ENTRY)をセットします(初期値:ENTRY)。
	 *
	 * @og.tag
	 * このタグは、command="ENTRY" でのみ実行されます。
	 * コマンドは,HTMLから(get/post)指定されますので,CMD_xxx で設定される
	 * フィールド定数値のいづれかを、指定できます。
	 * 初期値は、ENTRY なので、何も指定しなければ、実行されます。
	 *
	 * @param	cmd コマンド (public static final 宣言されている文字列)
	 * @see		<a href="../../../../constant-values.html#org.opengion.hayabusa.taglib.FileUpdateTag.CMD_ENTRY">コマンド定数</a>
	 */
	public void setCommand( final String cmd ) {
		final String cmd2 = getRequestParameter( cmd );
		if( cmd2 != null && cmd2.length() >= 0 ) { command = cmd2.toUpperCase(Locale.JAPAN); }
	}

	/**
	 * 【TAG】検索結果のメッセージを表示する/しない[true/false]を指定します(初期値:true)。
	 *
	 * @og.tag
	 * 初期値は、表示する：true です。
	 *
	 * @param	flag  メッセージ表示可否 [true:表示する/それ以外:含めない]
	 */
	public void setOutMessage( final String flag ) {
		outMessage = nval( getRequestParameter( flag ),outMessage );
	}

	/**
	 * 【TAG】処理結果を画面上に表示するメッセージリソースIDを指定します(初期値:MSG0040[　件登録しました])。
	 *
	 * @og.tag
	 * ここでは、検索結果の件数や登録された件数をまず出力し、
	 * その次に、ここで指定したメッセージをリソースから取得して表示します。
	 * 表示させたくない場合は, displayMsg = "" をセットしてください。
	 * displayMsg の初期値は、MSG0040[　件登録しました]です。
	 *
	 * @param	id 処理結果表示メッセージID
	 */
	public void setDisplayMsg( final String id ) {
		final String ids = getRequestParameter( id );
		if( ids != null ) { displayMsg = ids; }
	}

	/**
	 * 【TAG】データを全件選択済みとして処理するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * 全てのデータを選択済みデータとして扱って処理します。
	 * 全件処理する場合に、(true/false)を指定します。
	 * 初期値は false です。
	 *
	 * @param  all 全件選択済み指定 [true:全件選択済み/false:通常]
	 */
	public void setSelectedAll( final String all ) {
		selectedAll = nval( getRequestParameter( all ),selectedAll );
	}

	/**
	 * 【TAG】オリジナルのタイムスタンプを利用するかどうかを指定します(初期値:false)。
	 *
	 * @og.tag
	 * COPYや親違いMOVE(,MODIFY)の時に、オリジナルのタイムスタンプをそのままコピー先のファイルにも
	 * 適用するかどうかを指定します。
	 * タイムスタンプを初期化されたくない場合に、true に設定します。
	 * 初期値は 利用しない:false です。
	 *
	 * @param  flag タイムスタンプ利用 [true:する/false:しない]
	 */
	public void setKeepTimeStamp( final String flag ) {
		keepTimeStamp = nval( getRequestParameter( flag ),keepTimeStamp );
	}

	/**
	 * 【TAG】処理時間を表示する TimeView を表示するかどうか[true:する/false:しない]を指定します
	 *		(初期値:VIEW_USE_TIMEBAR[={@og.value SystemData#VIEW_USE_TIMEBAR}])。
	 *
	 * @og.tag
	 * true に設定すると、処理時間を表示するバーイメージが表示されます。
	 * これは、DB検索、APサーバー処理、画面表示の各処理時間をバーイメージで
	 * 表示させる機能です。処理時間の目安になります。
	 * (初期値:VIEW_USE_TIMEBAR[={@og.value SystemData#VIEW_USE_TIMEBAR}])。
	 *
	 * @og.rev 6.3.6.0 (2015/08/16) useTimeView の初期値を、VIEW_USE_TIMEBAR にする。
	 *
	 * @param	flag	処理時間を表示 [true:する/false:しない]
	 */
	public void setUseTimeView( final String flag ) {
		useTimeView = nval( getRequestParameter( flag ),useTimeView );
	}

	/**
	 * 【TAG】入力共通パスを指定します。
	 *
	 * @og.tag
	 * 通常、fileQueryタグ等で、検索した結果は、PARENT,NAME というカラムにセットされます。
	 * この、fileQueryのfrom属性に、検索を開始するディレクトリを指定し、multi="true"で、
	 * 多段階展開 した場合、この、from属性に指定したパスを、inPath にセットすることで、
	 * 以下の階層フォルダそのままに、targetDir にCOPY または、MOVE することが出来ます。
	 * 逆に、指定しない場合は、フォルダ階層無しで、COPY,MOVE されます。
	 *
	 * @og.rev 6.8.0.0 (2017/06/02) 入力共通パスを指定します。
	 * @og.rev 7.0.5.0 (2019/09/13) inPath のﾊﾟｽは、正規パス名から作成。
	 *
	 * @param	path	入力共通パス
	 */
	public void setInPath( final String path ) {
		inPath = nval( getRequestParameter( path ),inPath );

		// 7.0.5.0 (2019/09/13) inPath のﾊﾟｽは、正規パス名から作成。
		if( inPath != null ) {
			try {
				inPath = new File(inPath).getCanonicalPath();
			}
			catch( final IOException ex ) {
				final String errMsg = "inPathの正式なファイル名の取得に失敗しました。[" + inPath + "]"
							+ CR + ex.getMessage();
				throw new HybsSystemException( errMsg,ex );
			}
		}
	}

	/**
	 * 【TAG】ｴﾗｰﾒｯｾｰｼﾞにSLABELを利用するかどうか[true/false]を指定します(初期値:false)。
	 *
	 * @og.tag
	 * 通常のｴﾗｰﾒｯｾｰｼﾞは、ﾗﾍﾞﾙ（長）が使われますが、これをﾗﾍﾞﾙ（短）を使いたい場合に、true にセットします。
	 * ここでのラベル（短）は、ﾀｸﾞ修飾なしの、ﾗﾍﾞﾙ（短）です。
	 * 標準はfalse:利用しない=ﾗﾍﾞﾙ（長）です。
	 * true/false以外を指定した場合はfalse扱いとします。
	 *
	 * ﾗﾍﾞﾙﾘｿｰｽの概要説明があれば表示しますが、useSLabel="true" 時は、概要説明を表示しません。
	 *
	 * @og.rev 7.0.7.0 (2019/12/13) 新規追加
	 *
	 * @param prm SLABEL利用 [true:利用する/false:利用しない]
	 */
	public void setUseSLabel( final String prm ) {
		useSLabel = nval( getRequestParameter( prm ),useSLabel );
	}

	/**
	 * DBTableModel から、FromFile,ToFile を作成するための処理をまとめた補助クラスです。
	 *
	 * ここでは、オリジナルファイルやターゲットファイルを作成するための処理のみを集めています。
	 * メソッドにすると、ローカル変数を多く管理するか、多数の引数渡しを繰り返すことになるため、
	 * このローカルクラスに処理と、値を格納します。
	 *
	 */
	private static final class FromToFiles {
		private final DBTableModel	table ;

		private final int PARENT	;
		private final int NAME		;
		private final int TO_PARENT	;
		private final int TO_NAME	;

		private final File		toDir	;
		private final boolean	createDir;	// ターゲットとなるフォルダがなければ、作成するかどうか(true:作成する)
		private final int	inPathCnt	;	// 6.8.0.0 (2017/06/02) 入力共通パスの文字数

		private boolean equalParent	;		// 最後に実行された処理で、親フォルダが同一の場合は、true

		/**
		 *  引数指定のコンストラクター
		 *
		 * 必要なパラメータを渡して、オブジェクトを構築します。
		 * 入力共通パス(inPath)は、COPY/MOVE時にtargetDirと置換されます。
		 *
		 * @og.rev 6.8.0.0 (2017/06/02) 入力共通パス(inPath)を指定します。
		 *
		 * @param	table     一覧が格納されているDBTableModel
		 * @param	targetDir	ターゲットとなるフォルダ
		 * @param	createDir	フォルダ作成可否 [true:作成する/false:作成しない]
		 * @param	inPath		入力共通パス
		 */
		public FromToFiles( final DBTableModel table , final String targetDir , final boolean createDir , final String inPath ) {
			this.table		= table;
			this.createDir	= createDir ;
			toDir			= mkDirs( targetDir,createDir );			// targetDir が指定されていない場合は、null
			// 7.0.5.0 (2019/09/13) inPath のﾊﾟｽの文字数も、正規パス名から作成。
			inPathCnt		= inPath == null ? 0 : inPath.length() ;	// 6.8.0.0 (2017/06/02) 入力共通パスの文字数

			// "PARENT","NAME","TO_PARENT","TO_NAME" のカラム名のDBTableModelのカラム番号。存在しない場合は、-1
			PARENT		= table.getColumnNo( "PARENT"	, false );
			NAME		= table.getColumnNo( "NAME"		, false );
			TO_PARENT	= table.getColumnNo( "TO_PARENT", false );
			TO_NAME		= table.getColumnNo( "TO_NAME"	, false );
		}

		/**
		 * 行番号より、対応するオリジナルファイル(FromFile)を返します。
		 *
		 * ここでは、TO_PARENT や TO_NAME は、判定する必要がないため、makeFromToFile( int ) の
		 * 一部のみで処理が終了できます。
		 * １．FromDir は、PARENT 列の値から作成する。
		 * ２．FromFileは、FromDir ＋ NAME列の値から作成する。
		 *
		 * 配列返しの関係で、メソッド(および内部処理)を分けています。
		 *
		 * @param	rowNo	カラムNo
		 * @return	オリジナルファイル(FromFile)
		 * @og.rtnNotNull
		 * @see		#makeFromToFile( int )
		 */
		public File makeFromOnly( final int rowNo ) {
			final String[] value = table.getValues( rowNo );
			final File fromDir  = mkDirs( value[PARENT],createDir );

			return new File( fromDir, value[NAME] ) ;
		}

		/**
		 * 行番号より、対応するオリジナルファイル(FromFile)とターゲットファイル(ToFile)を配列に格納して返します。
		 *
		 * ここでは、TO_PARENT や TO_NAME は、存在するかどうか不明なので、以下の手順で作成します。
		 * １．FromDir は、PARENT 列の値から作成する。
		 * ２．FromFileは、FromDir ＋ NAME列の値から作成する。
		 * ３．toDir は、
		 *       Ａ．targetDir が有れば、それを使う。
		 *       Ｂ．なければ、TO_PARENT 列の値から作成する。
		 *       Ｃ．TO_PARENT 列がないか、値が未設定の場合は、FromDir をそのまま使う。
		 * ４．toFile は、
		 *       Ａ．toDir ＋ TO_NAME 列の値から作成する。
		 *       Ｂ．TO_NAME 列がないか、値が未設定の場合は、toDir ＋ NAME列の値から作成する。
		 * 返り値は、new File[] { formFile , toFile }; とする。
		 *
		 * @og.rev 6.8.0.0 (2017/06/02) 入力共通パス(inPath)を指定します。
		 *
		 * @param	rowNo	カラムNo
		 * @return	ファイル配列(0:オリジナルファイル 1:ターゲットファイル)
		 * @og.rtnNotNull
		 */
		public File[] makeFromToFile( final int rowNo ) {
			final String[] value = table.getValues( rowNo );
			final File fromDir  = mkDirs( value[PARENT],createDir );
			final File formFile = new File( fromDir, value[NAME] );
			File tempToDir = toDir;

			equalParent = false;	// 最後に実行された処理で、親フォルダが同一かどうかのフラグをリセットする。
			if( tempToDir == null ) {
				if( TO_PARENT >= 0 && nval( value[TO_PARENT],null ) != null ) {
					tempToDir = mkDirs( value[TO_PARENT],createDir );
				}
				else {
					tempToDir = fromDir;
					equalParent = true;		// 最後に実行された処理で、親フォルダが同一の場合は、true
				}
			}
			// 6.8.0.0 (2017/06/02) toDirが指定され、かつ、入力共通パスの文字数(inPathCnt)が指定された場合。
			else if( inPathCnt > 0 ) {
				if( TO_PARENT >= 0 && nval( value[TO_PARENT],null ) != null ) {
					final String prntVal = toDir.getAbsolutePath() + value[TO_PARENT].substring( inPathCnt );
					tempToDir = mkDirs( prntVal,createDir );
				}
				else {
					final String prntVal = toDir.getAbsolutePath() + value[PARENT].substring( inPathCnt );
					tempToDir = mkDirs( prntVal,createDir );
				}
			}

			File toFile = null;
			if( TO_NAME >= 0 && nval(value[TO_NAME],null) != null  ) {
				toFile = new File( tempToDir, value[TO_NAME] );
			}
			else {
				toFile = new File( tempToDir, value[NAME] );
			}

			return new File[] { formFile , toFile };
		}

		/**
		 * 最後に実行された処理で、親フォルダが同一かどうかを返します(同一の場合は、true)。
		 *
		 * makeFromToFile( int ) が処理されたときの、FromDir と toDir が同一であれば、true を、
		 * 異なる場合は、false を返します。
		 * ここでの結果は、厳密な同一判定ではなく、処理的に、同一かどうかを判定しています。
		 * つまり、toDir に FromDir をセットする(３．Ｃのケース)場合に、true を内部変数にセットします。
		 * この判定値は、ファイルの移動処理で、異なる親フォルダの場合は、COPY ＆ DELETE しなければ
		 * なりませんが、同一親フォルダの場合は、RENAME で済む という処理負荷の軽減が目的です。
		 * よって、結果的に、PARENT と TO_PARENT が同じとか、PARENT と targetDir が同じでも
		 * ここでのフラグは、false が返されます。
		 *
		 * @return	最後に実行された処理で、親フォルダが同一の場合は、true
		 * @see		#makeFromToFile( int )
		 */
		public boolean lastParentEquals() {
			return equalParent ;
		}

	//	/**
	//	 *  カラム名配列(String[])より、対応するカラムNo配列(int[])を作成します。
	//	 *
	//	 * ここでは、TO_PARENT や TO_NAME は、存在するかどうか不明なので、
	//	 * EXCEPTION にせず、配列番号に、-1 を返すようにしています。
	//	 *
	//	 * @param	table     一覧が格納されているDBTableModel
	//	 * @param	nameArray	カラム名配列
	//	 * @return	カラムNo配列(カラム名が存在しない場合は、-1)
	//	 */
	//	private int[] getTableColumnNo( final DBTableModel table ,final String[] nameArray ) {
	//		int[] clmNo = new int[ nameArray.length ];
	//		for( int i=0; i<clmNo.length; i++ ) {
	//			clmNo[i] = table.getColumnNo( nameArray[i] , false );	// カラム名が存在しない場合は、-1 を返す。
	//		}
	//		return clmNo;
	//	}

		/**
		 * フォルダを作成します。
		 *
		 * フォルダが存在しない場合は、途中階層をすべて作成します。
		 *
		 * @og.rev 7.0.5.0 (2019/09/13) target のﾊﾟｽも、正規パス名から作成。
		 *
		 * @param	fname	フォルダ名
		 * @param	createDir	フォルダ作成可否 [true:作成する/false:作成しない]
		 * @return	フォルダを表すファイルオブジェクト。引数が null の場合は、null を返します。
		 * @throws	HybsSystemException		ファイルか、存在しない場合に、createDir=false か、mkdirs() が false の場合
		 */
		private File mkDirs( final String fname , final boolean createDir ) {
			File target = null;
			if( fname != null ) {
				// 7.0.5.0 (2019/09/13) target のﾊﾟｽも、正規パス名から作成。
//				target = new File( fname );
				try {
					target = new File( fname ).getCanonicalFile();
					if( target.exists() ) {			// 存在する
						if( target.isFile() ) {
							final String errMsg = "ターゲットに、ファイル名は指定できません。" + CR
												+ "ターゲット=[" + fname + "]"  + CR;
							throw new HybsSystemException( errMsg );
						}
					}
					else {							// 存在しない
						// 存在しないのに、作成しない
						if( !createDir ) {
							final String errMsg = "ターゲットが存在しません。 " + CR
												+ "ターゲット=[" + fname + "]"  + CR;
							throw new HybsSystemException( errMsg );
						}
						// 作成できない
						if( !target.mkdirs() ) {
							final String errMsg = "ターゲットを自動作成使用としましたが、作成できませんでした。" + CR
												+ "ターゲット=[" + fname + "]"  + CR;
							throw new HybsSystemException( errMsg );
						}
					}
				}
				catch( final IOException ex ) {
					final String errMsg = "File#getCanonicalFile() で、正式パス名を求めることができませんでした。" + CR
										+ ex.getMessage()
										+ "ターゲット=[" + fname + "]"  + CR;
					throw new HybsSystemException( errMsg,ex );
				}
			}
			return target;
		}
	}

	/**
	 * このオブジェクトの文字列表現を返します。
	 * 基本的にデバッグ目的に使用します。
	 *
	 * @return このクラスの文字列表現
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		return ToString.title( this.getClass().getName() )
				.println( "VERSION"			,VERSION		)
				.println( "action"			,action			)
				.println( "command"			,command		)
				.println( "targetDir" 		,targetDir		)
				.println( "createDir" 		,createDir		)
				.println( "tableId"			,tableId		)
				.println( "outMessage"		,outMessage 	)
				.println( "displayMsg"		,displayMsg 	)
				.println( "selectedAll"		,selectedAll	)
				.println( "keepTimeStamp"	,keepTimeStamp	)
				.fixForm().toString() ;
	}
}
