/*
 * 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.fukurou.security;

import java.util.Map;
// import java.util.WeakHashMap;
// import java.util.HashMap;
// import java.util.concurrent.ConcurrentMap;						// 6.4.3.3 (2016/03/04)
// import java.util.concurrent.ConcurrentHashMap;					// 6.4.3.1 (2016/02/12) refactoring
import java.util.Collections;										// 6.4.3.1 (2016/02/12) refactoring
import java.util.LinkedHashMap;										// 6.4.3.3 (2016/03/04)

/**
 * URLHashMap は、セキュリティ強化の為の Hybs独自の暗号化クラスです。
 *
 * このクラスは、暗号化キーを受け取り、それに基づいて暗号化/復号化を行います。
 * ここでの暗号化は、秘密キー方式でバイト文字列に変換されたものを、１６進アスキー文字に
 * 直して、扱っています。よって、暗号化/復号化共に、文字列として扱うことが可能です。
 *
 * @og.rev 5.2.2.0 (2010/11/01) 新規追加
 * @og.rev 6.4.3.3 (2016/03/04) キャッシュを、世代交代のWeakHashMapから、LinkedHashMap(固定容量) に変更します。
 * @og.group ライセンス管理
 *
 * @version  5.2.2.0 (2010/11/01)
 * @author   Kazuhiko Hasegawa
 * @since    JDK1.6,
 */
public final class URLHashMap {
//	private static final int SIZE = 6000;
	private static final int SIZE = 10000;		// 6.4.3.3 (2016/03/04) 固定容量なので、増やしておきます。
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
//	private static final Map<String,String> URLMAP1 = new HashMap<>( SIZE*4/3 );		// 現役世代1	// 6.4.1.1 (2016/01/16) urlMap1 → URLMAP1 refactoring
//	private static final ConcurrentMap<String,String> URLMAP1 = new ConcurrentHashMap<>( SIZE*4/3 );		// 現役世代1	// 6.4.1.1 (2016/01/16) urlMap1 → URLMAP1 refactoring
//	private static       Map<String,String> urlMap2 = new HashMap<>( SIZE*4/3 );		// 現役世代2
//	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
//	private static final Map<String,String> URLMAP2 = new HashMap<>( SIZE*4/3 );		// 現役世代2	// 6.4.1.1 (2016/01/16) urlMap2 → URLMAP2 refactoring
//	private static final ConcurrentMap<String,String> URLMAP2 = new ConcurrentHashMap<>( SIZE*4/3 );		// 現役世代2	// 6.4.1.1 (2016/01/16) urlMap2 → URLMAP2 refactoring
//	/** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。  */
//	private static final Map<String,String> URLMAP3 = new WeakHashMap<>( SIZE*4/3 );	// 旧世代		// 6.4.1.1 (2016/01/16) urlMap3 → URLMAP3 refactoring
//	private static final Map<String,String> URLMAP3 = Collections.synchronizedMap( new WeakHashMap<>( SIZE*4/3 ) );	// 旧世代		// 6.4.1.1 (2016/01/16) urlMap3 → URLMAP3 refactoring
//	private static final Object LOCK = new Object();													// 6.4.1.1 (2016/01/16) lock    → LOCK refactoring

	/**
	 * LinkedHashMap は、反復順序を持つMapインタフェースで、データ件数を固定に設定します。
	 * ここでは、マップに新しいマッピングが追加されると、自動的に古いマッピングを削除するポリシーを
	 * 適用するために、removeEldestEntry(Map.Entry)メソッドをオーバーライドしています。 
	 * 古いマッピングの定義は、コンストラクタの、順序付けモードで指定でき、アクセス順ならtrue、挿入順ならfalse です。
	 * ここでは、固定の個数分だけ、キャッシュし、あふれた古い分は、順次キャッシュから追い出されていきます。
	 * Collections.synchronizedMap で、同期処理を行います。
	 */
	private static final Map<String,String> URLMAP = Collections.synchronizedMap( new LinkedHashMap<String,String>( SIZE*2,0.75f,true ) {		// アクセス順
		private static final long serialVersionUID = 643320160304L ;
		/**
		 * このマップが一番古いエントリを削除するはずの場合にtrueを返します。
		 *
		 * @og.rev 6.4.3.3 (2016/03/04) キャッシュを、世代交代のWeakHashMapから、LinkedHashMap(固定容量) に変更します。
		 *
		 * @param	eldest	もっとも以前にマップに挿入されたエントリ
		 *
		 * @return	もっとも古いエントリをマップから削除すべき場合はtrue。そのエントリを保持すべき場合はfalse
		 */
		@Override
		protected boolean removeEldestEntry( final Map.Entry<String,String> eldest ) {
			return size() > SIZE;
		}
	} );

	/**
	 * デフォルトコンストラクター
	 * これは、すべてstatic メソッドで構成されているクラスなので、直接インスタンスの生成を禁止します。
	 *
	 * @og.rev 5.3.0.0 (2010/12/01) 新規追加
	 */
	private URLHashMap() {
		// 直接インスタンスの生成を禁止
	}

	/**
	 * URL をハッシュ化/暗号化して返します。
	 * 共通処理を簡易化するための、便利メソッドです。
	 * ここでのハッシュ化/暗号化は、URLのパラメータ(?以降の部分)のみ行います。
	 * また、ここで渡す文字列は、URLを含む文字列なので、href のキーワードを
	 * 手がかりに、? を探して、その部分のみ、変換します。
	 * href を含まない場合は、それ自体が URL と判断して、? を探します。
	 * ? が存在しないケースは、url をそのまま返します。
	 *
	 * URLのアドレスが、"/" から始まる場合は、自身のアドレスと判断し、ハッシュ処理を行います。
	 * そうでない場合(http:// や ../ の場合)は、暗号化処理を行います。
	 * 第２引数は、ハッシュ化/暗号化されたパラメータを指定するキーになります。(標準は、h_r)
	 * 処理を選択します。
	 * href を含まず、'?' だけを含む場合は、'?' 以降を暗号化します。
	 *
	 * ハッシュの場合、このキーを元に、設定値を内部キャッシュ(Map)に設定します。
	 * Map は、３世代持っており、内部キャッシュ(SIZE=6000)を超えると、２世代目に移行し、
	 * さらに、超えると、３世代目に移行します。３世代目は、WeakHashMap を利用している
	 * ため、場合によってはデータがなくなり、アクセス不能になっている可能性があります。
	 *
	 * @param	url 	オリジナルのURL、または、URLを含む文字列
	 * @param	hkey	パラメータのキーとして使用する文字
	 * @param	extOnly	外部URL対応 [true:外部URLの場合のみ処理を実行/false:全実行]
	 *
	 * @return	変換されたURL を含む文字列
	 */
	public static String makeUrlChange( final String url , final String hkey , final boolean extOnly ) {
		if( url == null || hkey == null ) { return null; }
		// 外部のみ(extOnly=true)で、かつ、内部URLの形式の場合、引数そのままを返します。
		if( extOnly && url.indexOf( "href=\"/" ) >= 0 ) { return url; }

		String rtnUrl = url;

		final int idx1 = url.indexOf( "href=\"" );
		final int idx2 = url.indexOf( '?', idx1 );		// href･･･ がない時(idx1 == -1)でも正常に動作する。
		if( idx2 >= 0 ) {
			final String url1 = url.substring( 0,idx2+1 );		// 最初から '?' まで(?を含む)
			final int idx3 = url.indexOf( '"',idx2+1 );			// 6.0.2.5 (2014/10/31) refactoring
			if( idx3 >= 0 ) {
				String url2 = url.substring( idx2+1,idx3 );		// 純粋なパラメータ部分
				final String url3 = url.substring( idx3 );			// ダブルクオート(含む)以降
				// href="/ かどうか。文字列の有効長があるかどうかを先にチェックしている。
				if( idx1 >= 0 && url.length() > idx1+7 && url.charAt( idx1+7 ) == '/' ) {
					url2 = makeHashKey( url2 );
				}
				else {
					url2 = makeEncrypt( url2 );
				}
				rtnUrl = url1 + hkey + "=" + url2 + url3 ;
			}
			// '?' はあるが、最後のダブルクオートがない場合は、'?' から後ろすべてを暗号化する。
			else {
				final String url2 = url.substring( idx2+1 );		// ?以降の部分
				rtnUrl = url1 + hkey + "=" + makeEncrypt( url2 ) ;
			}
		}

		return rtnUrl;
	}

	/**
	 * 引数の設定値をハッシュ化したときの値(＝ハッシュキー)を作成します。
	 * 内部的に、このハッシュキーを元に、設定値を内部キャッシュ(Map)に設定します。
//	 * このMapは、WeakHashMap を利用しているため、場合によってはデータがなくなり、
//	 * アクセス不能になっている可能性があります。
	 *
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 * @og.rev 6.4.3.3 (2016/03/04) キャッシュを、世代交代のWeakHashMapから、LinkedHashMap(固定容量) に変更します。
	 *
	 * @param	val	オリジナルの設定値
	 *
	 * @return	ハッシュキー
	 * @og.rtnNotNull
	 */
	public static String makeHashKey( final String val ) {
		if( val == null ) { return ""; }

		// 必殺技：最後に "X" を付けることで、ハッシュ処理されたことを示す。
		final String key = HybsCryptography.getMD5( val ) + "X";
//		synchronized( LOCK ) {
//			URLMAP1.put( key,val );
//		}
		URLMAP.put( key,val );			// 6.4.3.3 (2016/03/04)
		return key;
	}

	/**
	 * 引数の設定値を暗号化したときの値を作成します。
	 * 暗号化なので、ハッシュ化の時の用に、内部キャッシュ(Map)に設定しません。
	 * 処理時間等を考慮すると、外部URLの暗号化や、長期にわたるURL(一旦、システムの
	 * 外部にURLを渡す：例えば、メールでリンク先を送信するなど)の作成に使用します。
	 *
	 * @param	val	オリジナルの設定値
	 *
	 * @return	暗号化キー
	 * @og.rtnNotNull
	 */
	public static String makeEncrypt( final String val ) {
		if( val == null ) { return ""; }

		// 必殺技：最後に "Y" を付けることで、暗号化処理されたことを示す。
		final HybsCryptography crypt = new HybsCryptography();
		return crypt.encrypt( val ) + "Y";
	}

	/**
	 * 指定のキーに対応した設定値を取り出します。
	 * ここでは、ハッシュ化 または 暗号化されているキーに対して、
	 * ハッシュキーであれば、関連付けられた設定値を取り出し、暗号化キーで
	 * あれば、復号化処理を行います。
	 *
	 * @og.rev 6.4.1.1 (2016/01/16) URLMAP2 を、static finalにするため、代入せずに、clear() + putAll(Map) で処理します。
	 * @og.rev 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。
	 * @og.rev 6.4.3.3 (2016/03/04) キャッシュを、世代交代のWeakHashMapから、LinkedHashMap(固定容量) に変更します。
	 *
	 * @param	key	ハッシュ化/暗号化されたキー
	 *
	 * @return	オリジナルの設定値
	 */
	public static String getValue( final String key ) {
		if( key == null || key.isEmpty() ) { return null; }

		String rtn = null;

		final char ch = key.charAt( key.length()-1 );
		if( ch == 'X' ) {
			rtn = URLMAP.get( key );
////			synchronized( LOCK ) {
//				// URLMAP1 → URLMAP2 → URLMAP3 と順番に探す。
//				// ※ getOrDefault は、defaultValue の処理は必ず実行されます。
//				// compute や merge は、ラムダ式で、実行判断されますが、put されるため、if判定しています。
//				// rtn = URLMAP1.getOrDefault( key , URLMAP2.getOrDefault( key , URLMAP3.get( key ) ) );
//
//				rtn = URLMAP1.get( key );
//				if( rtn == null ) { rtn = URLMAP2.get( key ); }
//				if( rtn == null ) { rtn = URLMAP3.get( key ); }
//
//				// ハッシュを作成するより取得する方が頻度が少ない。
//				if( URLMAP1.size() > SIZE ) {
//					URLMAP3.putAll( URLMAP2 );
//					// 6.4.1.1 (2016/01/16) URLMAP2 を、static finalにする。  ※ 既存のソースがそうなっていない理由は不明。単に処理速度の問題？
////					URLMAP2 = new HashMap<>( URLMAP1 );
//					URLMAP2.clear();						// 6.4.1.1 (2016/01/16)
//					URLMAP2.putAll( URLMAP1 );				// 6.4.1.1 (2016/01/16)
//					URLMAP1.clear();
//				}
////			}
		}
		else if( ch == 'Y' ) {
			final HybsCryptography crypt = new HybsCryptography();
			rtn = crypt.decrypt( key.substring( 0,key.length()-1 ) );
		}

		return rtn ;
	}
}
