java.util.Stack

目的
タグの閉じ忘れを検出する。

関連クラス

今回のソース
//////////////////// Main.java ////////////////////

import java.io.*;

class Main
{
	public static void main(String args[])
	{
		StringBuffer str_buf = new StringBuffer();
		String buf;
		try
		{
			BufferedReader reader = new BufferedReader(new FileReader(args[0]));
			while((buf = reader.readLine()) != null)
			{
				str_buf.append(buf);
			}
		}
		catch(IOException io_ex)
		{
			io_ex.printStackTrace();
		}

		TagChecker checker = new TagChecker(str_buf.toString());
		UnclosedTagInfo info[] = checker.analyze();

		for(int i=0; i<info.length; i++)
		{
			System.out.println(info[i].toString());
		}
	}
}

////////////////////////////////////////////////////////////

//////////////////// TagChecker.java ////////////////////

import java.io.*;
import java.util.Stack;
import java.util.Vector;

class TagChecker
{
	// 許容タグリストファイル名
	final String tag_list_file = "checklist.txt";

	String text;			// チェックすべきテキスト
	private Vector list;		// 閉じ忘れタグリスト
	private Vector check_tag_list;	// チェックタグリスト
	private Stack stack;		// タグチェック用スタック

	// コンストラクタ
	public TagChecker(String text)
	{
		this.text = text;

		list = new Vector();
		check_tag_list = new Vector();
		stack = new Stack();

		// 許容タグリストの取得
		try
		{
			// 入力ストリームの作成
			FileInputStream file_in = new FileInputStream(tag_list_file);
			BufferedReader reader = new BufferedReader(new InputStreamReader(file_in));

			// ファイルの読み込み
			String buffer;
			while((buffer = reader.readLine()) != null)
			{
				check_tag_list.add(buffer);
			}

			// ストリームを閉じる
			reader.close();
			file_in.close();
		}
		catch(IOException ioe)
		{
			System.err.println(ioe);
		}
	}

	// タグの閉じ忘れチェックルーチン
	public UnclosedTagInfo[] analyze()
	{
		try
		{
			// 入力ストリームの作成
			StringReader string_reader = new StringReader(text);
			BufferedReader reader = new BufferedReader(string_reader);

			String buffer;
			int current_position = 0;	// 始点(0)からの行頭の相対位置

			// 一行ずつ読み込んで解析する
			while((buffer = reader.readLine()) != null)
			{
				analyzeOneLine(buffer,current_position);
				current_position += buffer.length() + 1;
			}

			// 残っている閉じ忘れタグを閉じる
			while(stack.empty() == false)
			{
				int position = text.length();
				String unclosed_tag = (String)stack.pop();

				list.addElement(new UnclosedTagInfo(position,unclosed_tag));
			}

			// ストリームを閉じる
			reader.close();
			string_reader.close();
		}
		catch(IOException ioe)
		{
			System.err.println(ioe);
		}

		// エラーリストを返却する
		UnclosedTagInfo return_list[] = new UnclosedTagInfo[list.size()];
		list.copyInto(return_list);
		return(return_list);
	}

	// タグの閉じ忘れチェックルーチン(一行)
	protected void analyzeOneLine(String line,int current_position)
	{
		int leftIndex = 0,rightIndex,spaceIndex;

		// '<'がある場合
		while((leftIndex = line.indexOf('<',leftIndex)) != -1)
		{
			// '>'がある場合
			if((rightIndex = line.indexOf('>',leftIndex + 1)) != -1)
			{
				// タグの名前の取得(属性がある場合は無視する)
				String tag;
				if((spaceIndex = line.substring(leftIndex + 1,rightIndex).indexOf(' ')) != -1)
				{
					tag = line.substring(leftIndex + 1,leftIndex + spaceIndex + 1);
				}
				else
				{
					tag = line.substring(leftIndex + 1,rightIndex);
				}

				// '/'が先頭にある場合
				if(!stack.empty() && tag.charAt(0) == '/')
				{
					// 許容タグリストにあれば
					if(check_tag_list.contains(tag.substring(1,tag.length())))
					{
						// 閉じ忘れが無くなるまで、閉じ忘れリストに追加
						while(!stack.empty() && tag.substring(1,tag.length()).compareToIgnoreCase((String)stack.peek()) != 0)
						{
							int position = current_position + leftIndex;
							String unclosed_tag = (String)stack.pop();

							list.addElement(new UnclosedTagInfo(position,unclosed_tag));
						}

						// 一致したタグをスタックから取り除く
						// スタックが空 -> 閉じタグのみ存在するケース
						if(!stack.empty())
						{
							stack.pop();
						}
					}
				}
				// 単なるタグの場合、スタックにプッシュする
				else
				{
					for(int i=0;i<check_tag_list.size();i++)
					{
						// 許容タグリストにあれば
						if(check_tag_list.contains(tag.toLowerCase()))
						{
							stack.push(tag.toLowerCase());
							break;
						}
					}
				}

				// タグの次の文字から解析開始
				leftIndex = rightIndex + 1;
			}
			else
			{
				break;
			}
		}
	}
}

////////////////////////////////////////////////////////////

//////////////////// UnclosedTagInfo.java ////////////////////

// タグ閉じ忘れ位置とタグ名の情報
class UnclosedTagInfo
{
	int position;		// 挿入位置
	String unclosed_tag;	// タグ名

	// コンストラクタ
	public UnclosedTagInfo(int position,String unclosed_tag)
	{
		this.position = position;
		this.unclosed_tag = unclosed_tag;
	}

	// 挿入位置の取得
	public int getPosition()
	{
		return(position);
	}

	// タグ名の取得
	public String getTagName()
	{
		return(unclosed_tag);
	}

	// java.lang.Object.toString()のオーバーライド
	public String toString()
	{
		return(unclosed_tag + " : " + position);
	}
}

////////////////////////////////////////////////////////////
Source is here. (ZIP Format,2624Byte,Shift-JIS)

コンパイル&実行
javac Main.java
java Main <File Name>

説明
(概略)

スタックを使った、HTMLタグのオートクローズは、
うちの掲示板でやっているのですが、
今回は、それをJavaに移植してみました。

動作原理は、タグを順番にスタックにプッシュして、
閉じタグが出てきた場合に、一番最後にプッシュしたタグと比べて、
同じかどうかを調べ、ネストしていないかどうかを調べます。

例えば、<html><head></html>というのは、
まず、"html"と"head"が、順番にスタックにプッシュされます。
次に、"/html"という閉じタグが来ているので、
一番最後にプッシュしたタグ(head)と比べます。
ところが、/htmlとheadは違うので、/headを追加するといった感じです。

う〜ん。分かりづらいかも...

(サンプルプログラムの説明)

許容タグリスト
これは、閉じる必要があるタグかどうかを示すリストです。
つまり、<br>のように、単独で意味をなすタグは、
クローズする必要が無いということです。
StringReader string_reader = new StringReader(text);
文字列を一行づつに分解して、
一行づつ解析するルーチンに渡すために、
StringReaderクラスを使っています。

残っている閉じ忘れタグを閉じる
閉じ忘れを検出出来る場所は、
閉じタグがある場所か、又は文字列の最後です。
この場合は、最後の場所で検出を行っています。
例えば、<html><head></head>のような場合、
htmlは閉じられていないので、最後に付加する必要があるということです。
タグの名前の取得(属性がある場合は無視する)
タグには属性が含まれている場合があります。
例えば、<a href = "xxxx">のようなケースです。
この場合、最初に現われるスペースを検知して、
そこより前の部分をタグ名とする処置をしています。