/*
 * $Id: URLPool.java,v 1.6 2006/02/19 04:58:13 akabane Exp $
 * Copyright (c) 2006 LOGICAL-PARADOX.ORG
 */
package org.logical_paradox.rss.http;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Set;
import java.util.Vector;


/**
 * URLPool
 * 未探索のURLを格納しておくオブジェクト
 * @author satoshi akabane
 * @version $Revision: 1.6 $
 */
public class URLPool {
	/** ドメイン名を管理するテーブル */
	private Hashtable<String, HashSet<String>> domain = null;

	/**
	 * コンストラクタ．
	 */
	public URLPool() {
		domain = new Hashtable<String, HashSet<String>>();
	}

	/**
	 * 未処理URLを追加する．
	 * @param url 追加するURL
	 * @throws MalformedURLException URLが不正である
	 */
	public void add(String url) throws MalformedURLException {
		synchronized(domain) {
			// URLからドメイン名の部分を抽出する
			String dn = getDomainName(url);
			if(dn == null || dn.trim().length() == 0) {
				throw new MalformedURLException();
			}

			// URLを正規化する
			url = normalize(url);

			// 該当するドメイン名のURLセットを取得する
			HashSet<String> s = domain.get(dn);
			if(s == null) {
				s = new HashSet<String>();
				domain.put(dn, s);
			}
	
			// 未処理URLを追加する
			// HashSetなので重複は上書きされる．従って判定は必要ない
			s.add(url);

			domain.notifyAll();
		}
	}

	/**
	 * URLを正規化する．
	 * 具体的には，'#'以降をカットする．
	 * @param url URL
	 * @return 正規化されたURL
	 */
	public String normalize(String url) {
		int pos = url.indexOf('#');
		return pos < 0 ? url : url.substring(0, pos);
	}
	/**
	 * 未処理URLをまとめて追加する．
	 * @param urls 登録するURLの配列
	 * @throws MalformedURLException URLが不正である
	 */
	public void add(String[] urls) throws MalformedURLException {
		synchronized(domain) {
			for(String url : urls) {
				if(urls != null) {
					add(url);
				}
			}
	
			domain.notifyAll();
		}
	}

	/**
	 * 未処理URLをまとめて追加する．
	 * @param i イテレータ
	 * @throws MalformedURLException URLが不正である
	 */
	public void add(Iterator<String> i) throws MalformedURLException {
		synchronized(domain) {
			while(i.hasNext()) {
				add(i.next());
			}
			domain.notifyAll();
		}
	}

	/**
	 * 指定範囲のドメイン名を返す．
	 * @param idx インデックス
	 * @param len 長さ
	 * @return ドメイン名
	 */
	public String[] domains(int idx, int len) {
		String[] d = domains();
		Vector<String> p = new Vector<String>();

		// 指定範囲のドメイン名だけフェッチ
		for(int i = 0; i < len && i < d.length; i++) {
			p.addElement(d[i]);
		}

		// 文字列の配列にして返す
		return (String[])p.toArray(new String[0]);
	}

	/**
	 * ドメイン名一覧を返す．
	 * @return ドメイン名一覧
	 */
	public String[] domains() {
		Set<String> keys = null;
		synchronized(domain) {
			keys = domain.keySet();
		}

		if(keys == null) {
			return null;
		}
		return (String[])keys.toArray(new String[0]);
	}

	/**
	 * 指定ドメインのURLを返す．
	 * 指定ドメインのURLが存在しない場合はnullを返す
	 * @param dn ドメイン名
	 * @return URL一覧
	 */
	public String[] urls(String dn) {
		HashSet<String> set = domain.get(dn);
		if(set == null) {
			return null;
		}

		return (String[])set.toArray(new String[0]);
	}

	/**
	 * 指定ドメインのイテレータを返す．
	 * 指定ドメインのURLが存在しない場合はnullを返す]
	 * @param dn ドメイン名
	 * @return  イテレータ
	 */
	public Iterator<String> getURLIterator(String dn) {
		HashSet<String> set = domain.get(dn);
		if(set == null) {
			return null;
		}

		return set.iterator();
	}

	/**
	 * URLからドメイン名の部分だけを取得する．
	 * この場合，ポート番号やプロトコルは関係ないものとする<br>
	 * ただしサブドメインは関係があるものとする(別のサーバーである可能性があるため)<br>
	 * @param url URL
	 * @return ドメイン名
	 * @throws MalformedURLException URLが不正である
	 */
	protected String getDomainName(String url) throws MalformedURLException {
		URL u = new URL(url);
		String siteName = u.getHost();

		return siteName;
	}

	/**
	 * 指定URLをプールから削除する．
	 * @param url 削除対象URL
	 * @throws MalformedURLException URLが不正である
	 */
	public void remove(String url) throws MalformedURLException {
		synchronized(domain) {
			String dn = getDomainName(url);
			if(dn == null || dn.trim().length() == 0) {
				throw new MalformedURLException();
			}

			HashSet set = domain.get(dn);
			if(set == null) {
				return;
			}

			// 削除する
			set.remove(url);

			if(set.size() == 0) {
				// 未処理URLを1つも保有しないドメインになってしまった場合は
				// そのドメインを削除する
				domain.remove(dn);
			}
			domain.notifyAll();
		}
	}

	/**
	 * 指定ドメインを削除する．
	 * @param domainname 削除対象ドメイン名
	 */
	public void removeDomain(String domainname) {
		synchronized(domain) {
			HashSet domainSet = domain.remove(domainname);
			if(domainSet != null) {
				domainSet.clear();
			}

			domain.notifyAll();
		}
	}

	/**
	 * このプールのサイズを返す．
	 * @return サイズ
	 */
	public int size() {
		int remain = 0;

		synchronized(domain) {
			Enumeration e = domain.elements();
			while(e.hasMoreElements()) {
				HashSet s = (HashSet)e.nextElement();
				remain += s.size();
			}

			domain.notifyAll();
		}

		return remain;
	}

	/**
	 * このプールにデータが格納されているかどうかを返す
	 * @param true: 何か格納されている / false: 何も格納されていない
	 */
	public boolean isEmpty() {
		boolean rc = false;
		synchronized(domain) {
			rc = domain.isEmpty();
		}

		return rc;
	}

	/**
	 * このオブジェクトが空の間，スレッドの待ち合わせをする．
	 */
	public void waitUntilEmpty() {
		synchronized(domain) {
			while(domain.isEmpty() == true) {
				try {
					domain.wait();
				} catch(InterruptedException ie) {
					// 待ち合わせが強制的に終了した．別にそれならそれで結構
				}
			}
		}
	}
}
