/*
 * Copyright (c) 2003, Morpho Project.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 * 3. Neither the name of the Morpho Project nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */
package jp.morpho.webapp;

import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Vector;
import java.util.logging.Logger;

import javax.servlet.ServletContext;

import jp.morpho.config.Configuration;
import jp.morpho.lang.LifeCycle;
import jp.morpho.lang.LifeCycleException;
import jp.morpho.webapp.impl.PluginContextImpl;

/**
 * <P>
 * FrameworkContext クラスはプラグインを格納します。
 * プラグイン は add メソッドを使用して FrameworkContext に格納できます。
 * </P>
 * <P>
 * Web アプリケーション単位で見た場合、「サーブレットコンテキスト」 1 つに、1 つの FrameworkContext が対応します。
 * </P>
 * @author Kenichi Fukuda
 */
public class FrameworkContext
	extends Vector
	implements LifeCycle, Serializable
{

	/** ロガー */
	private Logger log = Logger.getLogger("jp.morpho");

	/** サーブレットコンテキストにバインドされる属性の名前 */
	private static final String ATTRIBUTE = "jp.morpho.webapp.FrameworkContext";

	/** フレームワークの配備記述子のリソースパス */
	private static final String CONFIG_PATH = "/WEB-INF/morpho-config.xml";

	/** プラグインのマップ */
	private HashMap map = new HashMap();

	/** FrameworkContext がスタートしている場合は true */
	private boolean running = false;

	/**
	 * <P>
	 * FrameworkContext を作成します。
	 * </P>
	 */
	protected FrameworkContext()
	{
		super();
	}

	/**
	 * <P>
	 * FrameworkContext を作成または取得します。
	 * すでに FrameworkContext が作成されている場合は、それが返されます。
	 * そうでない場合は、新しい FrameworkContext が作成されます。
	 * </P>
	 * <P>
	 * 新しい FrameworkContext が作成される場合、そのフレームワークコンフィグはサーブレットコンテキストの属性 jp.morpho.webapp.FrameworkContext にバインドされます。
	 * すでにサーブレットコンテキストの属性 jp.morpho.webapp.FrameworkContext に FrameworkContext がバインドされている場合は、それが返されます。
	 * </P>
	 * @param sc この FrameworkContext を格納するサーブレットコンテキスト
	 * @return FrameworkContext オブジェクト
	 */
	public static synchronized FrameworkContext getInstance(ServletContext sc) throws FrameworkException
	{
		try
		{
			//サーブレットコンテキストから FrameworkContext を取得します
			FrameworkContext fc =
				(FrameworkContext)sc.getAttribute(ATTRIBUTE);

			//まだ FrameworkContext が作成されていない場合は、
			//新しい FrameworkContext が作成されます。
			if (fc == null)
			{
				//FrameworkContext を作成します
				fc = new FrameworkContext();

				//フレームワークの配備記述子の URL を取得します
				URL configURL = sc.getResource(CONFIG_PATH);

				//フレームワークを設定します
				InputStream in = configURL.openStream();
				Configuration config = new Configuration();
				config.configure(fc, in);
				in.close();

				//サーブレットコンテキストへバインドします
				sc.setAttribute(ATTRIBUTE, fc);
			}

			return fc;
		}
		catch (Exception e)
		{
			throw new FrameworkException(e);
		}
	}

	/**
	 * <P>
	 * 指定されたオブジェクトがプラグインであるかどうかを判定します。
	 * 指定されたオブジェクトが jp.morpho.webapp.Plugin を実装しているかどうかを調べます。
	 * </P>
	 * @param obj プラグインであるかどうかを調べるオブジェクト
	 * @exception IllegalArgumentException 指定されたオブジェクトがプラグインでない場合
	 */
	private synchronized void checkInstance(Object obj)
	{
		if (!(obj instanceof Plugin))
		{
			throw new IllegalArgumentException("Object is not a Plugin: " + obj);
		}
	}

	/**
	 * <P>
	 * すでにプラグインが追加されているかどうかを判定します。
	 * プラグインのマップが指定のキーのマッピングを保持する場合に 例外 IllegalArgumentException をスローします。
	 * </P>
	 * @param key マップにあるかどうかが判定されるキー
	 * @exception IllegalArgumentException プラグインのマップが指定のキーのマッピングを保持している場合
	 */
	private synchronized void checkMapping(String key)
	{
		if (map.containsKey(key))
		{
			throw new IllegalArgumentException("Plugin Key already exists: " + key);
		}
	}

	/**
	 * <P>
	 * FrameworkContext 内の指定された位置に指定のプラグインを挿入します。
	 * その位置とそれ以降にプラグインがあればそれらを右に移動させ、各プラグインのインデックスに 1 を加えます。
	 * </P>
	 * @param index 指定のプラグインが挿入されるインデックス
	 * @param element 挿入されるプラグイン
	 * @exception IndexOutOfBoundsException インデックスが範囲外の場合 (index < 0 || index > size())
	 * @exception IllegalArgumentException 指定されたオブジェクトがプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see java.util.List#add(int, Object)
	 */
	public synchronized void add(int index, Object element)
	{
		checkInstance(element);

		String key = ((Plugin)element).getPluginKey();
		checkMapping(key);

		map.put(key, element);
		super.add(index, element);
	}

	/**
	 * <P>
	 * FrameworkContext の末尾に指定のプラグインを追加します。
	 * </P>
	 * @param element FrameworkContext に追加されるプラグイン
	 * @return true (Collection.add の汎用規約どおり)
	 * @exception IllegalArgumentException 指定されたオブジェクトがプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see java.util.Collection#add(Object)
	 */
	public synchronized boolean add(Object element)
	{
		checkInstance(element);

		String key = ((Plugin)element).getPluginKey();
		checkMapping(key);

		map.put(key, element);
		try
		{
			PluginContextImpl ctx = new PluginContextImpl();
			ctx.setFrameworkContext(this);
			((Plugin)element).setPluginContext(ctx);
		}
		catch (PluginException e)
		{
			log.warning(e.getMessage());
		}
		return super.add(element);
	}

	/**
	 * <P>
	 * 指定された Collection 内のすべてのプラグインをこの FrameworkContext の末尾に、その Collection の Iterator によって返される順序で追加します。
	 * このオペレーションの動作は、指定された Collection がオペレーションの進行時に変更されると定義されません。
	 * これは、その Collection が FrameworkContext である場合にこの呼び出しの動作が定義されず、FrameworkContext が空でないことを示します。
	 * </P>
	 * @param c FrameworkContext に挿入されるプラグイン
	 * @return この呼び出しの結果、この FrameworkContext が変更された場合は true
	 * @exception IndexOutOfBoundsException インデックスが範囲外の場合 (index < 0 || index > size())
	 * @exception NullPointerException 指定されたコレクションが null である場合
	 * @exception IllegalArgumentException 指定されたコレクションの要素がプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see java.util.Collection#addAll(Collection)
	 */
	public synchronized boolean addAll(Collection c)
	{
		for (Iterator i = c.iterator(); i.hasNext();)
		{
			Object o = i.next();
			checkInstance(o);

			String key = ((Plugin)o).getPluginKey();
			checkMapping(key);

			map.put(key, o);
		}

		return super.addAll(c);
	}

	/**
	 * <P>
	 * 指定された Collection 内のすべてのプラグインを、FrameworkContext の指定された位置に挿入します。
	 * その位置とそれ以降にプラグインがある場合は、それらを右に移動して、各プラグインのインデックスに 1 を加えます。
	 * 新しい各プラグインは、指定された Collection の反復子によって返される順序で FrameworkContext 内に格納されます。
	 * </P>
	 * @param index 指定されたコレクションから最初のプラグインを挿入する位置のインデックス
	 * @param c FrameworkContext に挿入されるプラグイン
	 * @return この呼び出しの結果、この FrameworkContext が変更された場合は true
	 * @exception IndexOutOfBoundsException インデックスが範囲外の場合 (index < 0 || index > size())
	 * @exception NullPointerException 指定されたコレクションが null である場合
	 * @exception IllegalArgumentException 指定されたコレクションの要素がプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see java.util.List#addAll(int, Collection)
	 */
	public synchronized boolean addAll(int index, Collection c)
	{
		for (Iterator i = c.iterator(); i.hasNext();)
		{
			Object o = i.next();
			checkInstance(o);

			String key = ((Plugin)o).getPluginKey();
			checkMapping(key);

			map.put(key, o);
		}

		return super.addAll(index, c);
	}

	/**
	 * <P>
	 * 指定のプラグインを FrameworkContext の最後に追加し、サイズを 1 増やします。
	 * サイズが FrameworkContext の容量より大きくなるときは容量が増やされます。
	 * </P>
	 * <P>
	 * このメソッドは機能的に add(Object) メソッドと同じです。これは、List インタフェースの一部です。
	 * </P>
	 * @param element 追加されるプラグイン
	 * @exception IllegalArgumentException 指定されたオブジェクトがプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see jp.morpho.webapp.FrameworkContext#add(Object)
	 */
	public synchronized void addElement(Object element)
	{
		checkInstance(element);

		String key = ((Plugin)element).getPluginKey();
		checkMapping(key);

		map.put(key, element);
		super.addElement(element);
	}

	/**
	 * <P>
	 * FrameworkContext の指定された index に、指定されたプラグインを挿入します。
	 * 指定された index に等しいか、それより大きいインデックスのプラグインはすべて 1 つ後方のインデックスにずれます。
	 * </P>
	 * <P>
	 * インデックスは、0 に等しいか、それより大きい値でなければなりません。
	 * また、FrameworkContext の現在のサイズに等しいか、それより小さい値でなければなりません。
	 * インデックスが FrameworkContext の現在のサイズに等しい場合、新しいプラグインは FrameworkContext の末尾に追加されます。
	 * </P>
	 * <P>
	 * このメソッドは機能的に add(Object, int) メソッドと同じです。
	 * これは、List インタフェースの一部です。
	 * 配列の使い方により適した形にするために add メソッドがパラメータの順序を逆にすることに注意してください。
	 * </P>
	 * @param element 挿入されるプラグイン
	 * @param index 新しいプラグインを挿入する位置
	 * @exception ArrayIndexOutOfBoundsException - インデックスが範囲外の場合 (index < 0 || index >= size())
	 * @exception IllegalArgumentException 指定されたオブジェクトがプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see java.util.Vector#insertElementAt(Object, int)
	 */
	public synchronized void insertElementAt(Object element, int index)
	{
		checkInstance(element);

		String key = ((Plugin)element).getPluginKey();
		checkMapping(key);

		map.put(key, element);
		super.insertElementAt(element, index);
	}

	/**
	 * <P>
	 * FrameworkContext 内の指定の位置にあるプラグインを削除します。
	 * それ以降にプラグインがあればそれらを左に移動し、各プラグインのインデックスから 1 を減らします。
	 * FrameworkContext から削除されたプラグインを返します。
	 * </P>
	 * @param index 削除されるプラグインのインデックス
	 * @exception ArrayIndexOutOfBoundsException - インデックスが範囲外の場合 (index < 0 || index >= size())
	 * @see java.util.List#remove(int)
	 */
	public synchronized Object remove(int index)
	{
		Object o = super.remove(index);
		if (o != null)
		{
			String key = ((Plugin)o).getPluginKey();
			map.remove(key);
		}
		return o;
	}

	/**
	 * <P>
	 * FrameworkContext 内で最初に検出された指定のプラグインを削除します。
	 * FrameworkContext がプラグインを保持していない場合は、変更されません。
	 * つまり、(element == null ? get(i) == null : element.equals(get(i))) であるもっとも小さいインデックス i を持つプラグインがあれば削除します。
	 * </P>
	 * @param element FrameworkContext から削除されるプラグイン (そのプラグインがある場合)
	 * @return 指定されたプラグインが FrameworkContext 内にあった場合は true
	 * @see java.util.Collection#remove(Object)
	 */
	public synchronized boolean remove(Object element)
	{
		boolean ret = super.remove(element);
		if (ret)
		{
			String key = ((Plugin)element).getPluginKey();
			map.remove(key);
		}

		return ret;
	}

	/**
	 * <P>
	 * FrameworkContext から、指定された Collection 内に保持されているすべてのプラグインを削除します。
	 * </P>
	 * @param c FrameworkContext から削除されるプラグインのコレクション
	 * @return FrameworkContext が呼び出しの結果として変更された場合は true
	 * @exception NullPointerException 指定されたコレクションが null である場合
	 * @see java.util.Collection#removeAll(Collection)
	 */
	public synchronized boolean removeAll(Collection c)
	{
		boolean retAll = false;
		for (Iterator i = c.iterator(); i.hasNext();)
		{
			Object o = i.next();
			boolean ret = this.remove(o);
			retAll = (ret) ? ret : retAll;
		}
		return retAll;
	}

	/**
	 * <P>
	 * FrameworkContext からすべてのプラグインを削除し、サイズを 0 に設定します。
	 * </P>
	 * <P>
	 * このメソッドは機能的に clear メソッドと同じです。
	 * これは、List インタフェースの一部です。
	 * </P>
	 * @see java.util.Vector#removeAllElements()
	 */
	public synchronized void removeAllElements()
	{
		map.clear();
		super.removeAllElements();
	}

	/**
	 * <P>
	 * 最初に検出された (最小インデックスの) 引数のプラグインを FrameworkContext から削除します。
	 * プラグインが検出されると、そのプラグインのインデックスと同じかそれより大きいインデックスのプラグインはすべて 1 つ前方のインデックスに詰められます。
	 * </P>
	 * <P>
	 * このメソッドは機能的に remove(Object) メソッドと同じです。
	 * これは、List インタフェースの一部です。
	 * </P>
	 * @param element 削除されるプラグイン
	 * @return 引数が FrameworkContext のプラグインである場合は true、そうでない場合は false
	 * @see java.util.Vector#removeElement(Object)
	 */
	public synchronized boolean removeElement(Object element)
	{
		boolean ret = super.removeElement(element);
		if (ret)
		{
			String key = ((Plugin)element).getPluginKey();
			map.remove(key);
		}

		return ret;
	}

	/**
	 * <P>
	 * 指定されたインデックスのプラグインを削除します。
	 * 指定された index に等しいか、それより大きいインデックスのプラグインは、すべて 1 つ前方のインデックスに詰められます。
	 * FrameworkContext のサイズは 1 だけ減らされます。
	 * </P>
	 * <P>
	 * インデックスは、0 に等しいか、それより大きい値でなければなりません。
	 * また、FrameworkContext の現在のサイズより小さい値でなければなりません。
	 * </P>
	 * <P>
	 * このメソッドは機能的に remove メソッドと同じです。
	 * これは、List インタフェースの一部です。
	 * remove メソッドが指定の位置に格納されていた古い値を返すことに注意してください。
	 * </P>
	 * @param index 削除されるプラグインのインデックス
	 * @exception ArrayIndexOutOfBoundsException - インデックスが範囲外の場合 (index < 0 || index >= size())
	 * @see java.util.Vector#removeElementAt(int)
	 */
	public synchronized void removeElementAt(int index)
	{
		String key = ((Plugin)get(index)).getPluginKey();
		map.remove(key);
		super.removeElementAt(index);
	}

	/**
	 * <P>
	 * 指定された Collection 内に保持されている、FrameworkContext 内のプラグインだけを保持します。
	 * つまり、FrameworkContext から、指定された Collection 内に保持されていないすべてのプラグインを削除します。
	 * </P>
	 * @param c FrameworkContext に保持されているプラグインのコレクション (ほかのすべてのプラグインは削除)
	 * @return FrameworkContext が呼び出しの結果として変更された場合は true
	 * @exception NullPointerException 指定されたコレクションが null である場合
	 * @see java.util.Collection#retainAll(Collection)
	 */
	public synchronized boolean retainAll(Collection c)
	{
		boolean ret = super.retainAll(c);
		Object[] keys = map.keySet().toArray();
		for (int i = 0; i < keys.length; i++)
		{
			String key = (String)keys[i];
			if (!(this.contains(map.get(key))))
			{
				map.remove(key);
			}
		}

		return ret;
	}

	/**
	 * <P>
	 * FrameworkContext 内の指定された位置にあるプラグインを、指定のプラグインで置き換えます。
	 * </P>
	 * @param index 置換されるプラグインのインデックス
	 * @param element 指定された位置に格納されるプラグイン
	 * @return 指定された位置に以前あったプラグイン
	 * @exception ArrayIndexOutOfBoundsException インデックスが範囲外の場合 (index < 0 || index >= size())
	 * @exception IllegalArgumentException 指定されたオブジェクトがプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see java.util.List#set(int, Object)
	 */
	public synchronized Object set(int index, Object element)
	{
		checkInstance(element);

		String key = ((Plugin)get(index)).getPluginKey();
		map.remove(key);

		key = ((Plugin)element).getPluginKey();
		map.put(key, element);
		return super.set(index, element);
	}

	/**
	 * <P>
	 * FrameworkContext の指定された index のプラグインに、指定されたプラグインを設定します。
	 * そのインデックスにあったプラグインは破棄されます。
	 * </P>
	 * <P>
	 * インデックスは、0 に等しいか、それより大きい値でなければなりません。
	 * また、FrameworkContext の現在のサイズより小さい値でなければなりません。
	 * </P>
	 * <P>
	 * このメソッドは機能的に set メソッドと同じです。
	 * これは、List インタフェースの一部です。
	 * 配列の使い方により近づけるために、set メソッドがパラメータの順序を逆にすることに注意してください。
	 * また、set メソッドが指定の位置に格納されていた古い値を返すことにも注意してください。
	 * </P>
	 * @param element プラグインに設定されるオブジェクト
	 * @param index インデックス
	 * @exception ArrayIndexOutOfBoundsException インデックスが範囲外の場合 (index < 0 || index >= size())
	 * @exception IllegalArgumentException 指定されたオブジェクトがプラグインでない場合、または同じ名前のプラグインが FrameworkContext にあった場合
	 * @see java.util.Vector#setElementAt(Object, int)
	 */
	public synchronized void setElementAt(Object element, int index)
	{
		checkInstance(element);

		String key = ((Plugin)get(index)).getPluginKey();
		map.remove(key);

		key = ((Plugin)element).getPluginKey();
		map.put(key, element);
		super.setElementAt(element, index);
	}

	/**
	 * <P>
	 * 指定されたキーのプラグインを返します。
	 * FrameworkContext がこのキーのプラグインを保持していない場合は null を返します。
	 * </P>
	 * @param key プラグインのキー
	 * @return 指定されたキーのプラグイン。指定されたキーのプラグインがない場合は null
	 */
	public Object get(String key)
	{
		return map.get(key);
	}

	/**
	 * <P>
	 * すべてのプラグインを FrameworkContext から削除します。
	 * この呼び出しが返ると、それが例外をスローしないかぎり、FrameworkContext は空になります。
	 * </P>
	 * @see java.util.Collection#clear()
	 */
	public synchronized void clear()
	{
		map.clear();
		super.clear();
	}

	/**
	 * <P>
	 * FrameworkContext がスタートしている場合に true を返します。
	 * </P>
	 * @return FrameworkContext がスタートしている場合は true
	 * @see jp.morpho.lang.LifeCycle#isRunning()
	 */
	public synchronized boolean isRunning()
	{
		return running;
	}

	/**
	 * <P>
	 * FrameworkContext をスタートします。
	 * </P>
	 * @exception LifeCycleException FrameworkContext のスタート処理中に問題が起こった場合
	 * @see jp.morpho.lang.LifeCycle#start()
	 */
	public synchronized void start() throws LifeCycleException
	{
		if (isRunning())
		{
			throw new LifeCycleException("Already started");
		}
		running = true;

		Plugin[] plugins = new Plugin[this.size()];
		plugins = (Plugin[])this.toArray(plugins);
		for (int i = 0; i < plugins.length; i++)
		{
			try
			{
				plugins[i].pluginActivate();
			}
			catch (PluginException e)
			{
				//続行
			}
		}
	}

	/**
	 * <P>
	 * FrameworkContext をストップします。
	 * </P>
	 * @exception LifeCycleException FrameworkContext のストップ処理中に問題が起こった場合
	 * @see jp.morpho.lang.LifeCycle#stop()
	 */
	public synchronized void stop() throws LifeCycleException
	{
		running = false;
		Plugin[] plugins = new Plugin[this.size()];
		plugins = (Plugin[])this.toArray(plugins);
		while (this.size() > 0)
		{
			try
			{
				plugins[this.size() - 1].pluginRemove();
			}
			catch (PluginException e)
			{
				//続行
			}
			this.remove(this.size() - 1);
		}
	}

}
