added org.eclipse.epf.web.search project. This is the web search core component for epf
diff --git a/org.eclipse.epf.web.search/.classpath b/org.eclipse.epf.web.search/.classpath
new file mode 100644
index 0000000..0a604b9
--- /dev/null
+++ b/org.eclipse.epf.web.search/.classpath
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+	<classpathentry kind="lib" path="lib/icu4j_3_4.jar"/>
+	<classpathentry kind="lib" path="lib/lucene-core-1.9.1.jar"/>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/org.eclipse.epf.web.search/.packaging b/org.eclipse.epf.web.search/.packaging
new file mode 100644
index 0000000..a336b0b
--- /dev/null
+++ b/org.eclipse.epf.web.search/.packaging
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>

+<configurations/>

diff --git a/org.eclipse.epf.web.search/.project b/org.eclipse.epf.web.search/.project
new file mode 100644
index 0000000..b642b1f
--- /dev/null
+++ b/org.eclipse.epf.web.search/.project
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>org.eclipse.epf.web.search</name>
+	<comment></comment>
+	<projects>
+		<project>Lucene 1.9.1</project>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/org.eclipse.epf.web.search/epf-web-search.jardesc b/org.eclipse.epf.web.search/epf-web-search.jardesc
new file mode 100644
index 0000000..30ed911
--- /dev/null
+++ b/org.eclipse.epf.web.search/epf-web-search.jardesc
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="WINDOWS-1252"?>
+<jardesc>
+<jar path="D:/EPF Workspaces/epf-head/org.eclipse.epf.publishing/docroot/WEB-INF/lib/epf-web-search.jar"/>
+<options buildIfNeeded="true" compress="true" descriptionLocation="/org.eclipse.epf.web.search/epf-web-search.jardesc" exportErrors="true" exportWarnings="true" includeDirectoryEntries="false" overwrite="false" saveDescription="true" storeRefactorings="false" useSourceFolders="false"/>
+<storedRefactorings deprecationInfo="true" structuralOnly="false"/>
+<selectedProjects/>
+<manifest generateManifest="true" manifestLocation="" manifestVersion="1.0" reuseManifest="false" saveManifest="false" usesManifest="true">
+<sealing sealJar="false">
+<packagesToSeal/>
+<packagesToUnSeal/>
+</sealing>
+</manifest>
+<selectedElements exportClassFiles="true" exportJavaFiles="false" exportOutputFolder="false">
+<javaElement handleIdentifier="=org.eclipse.epf.web.search/src"/>
+</selectedElements>
+</jardesc>
diff --git a/org.eclipse.epf.web.search/lib/icu4j_3_4.jar b/org.eclipse.epf.web.search/lib/icu4j_3_4.jar
new file mode 100644
index 0000000..d35e3c3
--- /dev/null
+++ b/org.eclipse.epf.web.search/lib/icu4j_3_4.jar
Binary files differ
diff --git a/org.eclipse.epf.web.search/lib/lucene-core-1.9.1.jar b/org.eclipse.epf.web.search/lib/lucene-core-1.9.1.jar
new file mode 100644
index 0000000..f143e9d
--- /dev/null
+++ b/org.eclipse.epf.web.search/lib/lucene-core-1.9.1.jar
Binary files differ
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/DefaultSearchService.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/DefaultSearchService.java
new file mode 100644
index 0000000..dc6d4a5
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/DefaultSearchService.java
@@ -0,0 +1,5 @@
+package org.eclipse.epf.web.search;
+
+public class DefaultSearchService {
+
+}
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/IndexLoader.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/IndexLoader.java
new file mode 100644
index 0000000..78ed928
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/IndexLoader.java
@@ -0,0 +1,309 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2007 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.StringTokenizer;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.eclipse.epf.web.search.utils.UNCUtil;
+
+
+public class IndexLoader {
+	public static final String VERSION_FILE_NAME = "version.txt";
+	public static final String VERSION_DELIMITER = "*";
+	public static final String DOWNLOAD_ERROR = "Unable to download index.jar file";
+
+	private static String userIndexFolderName = null;
+	private String userIndexPath = null;
+	private static Map cache = new HashMap();
+
+	protected String m_docBase;
+	protected String m_indexFolder;
+	protected String m_indexFile;
+	protected long tally;
+	protected int fileSize;
+
+	public String downloadIndex() {
+		byte bytes[] = new byte[16384];
+
+		String product = "";
+		File versionFile = null;
+		File indexArchive = null;
+
+		try {
+			// TBD replace 100 with something to do with archive size or
+			// something
+			StringBuffer base = new StringBuffer(m_docBase);
+			base.append("/search/index/index.jar");
+
+			URL url = new URL(base.toString());
+			// handle UNC
+			url = UNCUtil.handleURLForUNC(url);
+
+			// File folder = new File(m_indexFolder);
+			// folder = folder.getParentFile();
+			//
+			// if (folder.exists())
+			// {
+			// folder.delete();
+			// }
+
+			indexArchive = new File(m_indexFile);
+			indexArchive.getParentFile().mkdirs();
+
+			StringBuffer newIndexFolderName = new StringBuffer(m_indexFolder);
+			newIndexFolderName.append(userIndexFolderName);
+
+			// let's get the attrbutes of the file we want to download.
+			versionFile = new File(newIndexFolderName.toString()
+					+ File.separator + VERSION_FILE_NAME);
+			FileReader readVersion = new FileReader(versionFile);
+			BufferedReader bRead = new BufferedReader(readVersion);
+			String versionString = bRead.readLine();
+			bRead.close();
+			readVersion.close();
+			StringTokenizer tok = new StringTokenizer(versionString,
+					VERSION_DELIMITER);
+			product = tok.nextToken();
+			tok.nextToken();
+			fileSize = Integer.parseInt(tok.nextToken());
+
+			BufferedInputStream inputS = new BufferedInputStream(url
+					.openStream());
+			FileOutputStream writeArchive = new FileOutputStream(indexArchive);
+
+			// System.out.println("docBase: " + m_docBase);
+
+			tally = 0;
+			int streamNotAvailable = 0;
+
+			// System.out.println("Downloading index file: " + url.toString());
+			// System.out.println("fileSize: " + fileSize);
+
+			while (tally < fileSize) {
+				int num = inputS.read(bytes);
+
+				// inputS.available() is not implemented for some imput stream,
+				// such as strem from https url. The value will always be 0
+				if (/* inputS.available() */num > 0) {
+					streamNotAvailable = 0;
+					writeArchive.write(bytes, 0, num);
+					tally = tally + num;
+				} else {
+					try {
+						// this will cause invalid thread exeception
+						// since the thread may not own the object's monitor
+						// use synchronized key to force the thread to own the
+						// object's monitor
+						synchronized (this) {
+							wait(50);
+						}
+
+						if (streamNotAvailable > 100) {
+							System.out.println("time out: " + DOWNLOAD_ERROR);
+							throw new Exception(DOWNLOAD_ERROR);
+						}
+						streamNotAvailable++;
+					} catch (Exception e1) {
+						System.out.println("Download index failed");
+						// display.setText(display.getText() + "\nDownload index
+						// failed");
+						e1.printStackTrace();
+						break;
+					}
+				}
+			}
+
+			inputS.close();
+			writeArchive.close();
+			tally = 0;
+
+			ZipInputStream zStream = new ZipInputStream(new FileInputStream(
+					m_indexFile));
+			ZipEntry zEntry = zStream.getNextEntry();
+			while (zEntry != null) {
+				File file = new File(newIndexFolderName.toString(), zEntry
+						.getName());
+
+				if (zEntry.isDirectory()) {
+					file.mkdir();
+				} else {
+					file.getParentFile().mkdirs();
+					FileOutputStream outs = new FileOutputStream(file);
+					while (zStream.available() > 0) {
+						int num = zStream.read(bytes);
+						if (num > 0) {
+							outs.write(bytes, 0, num);
+						}
+					}
+					outs.close();
+					tally++;
+				}
+
+				zEntry = zStream.getNextEntry();
+			}
+			zStream.close();
+
+			System.out
+					.println("Completed writing the index files successfully");
+		} catch (Exception e1) {
+			System.out.println(e1.getMessage());
+			product = null;
+			versionFile.delete();
+
+			e1.printStackTrace();
+		} finally {
+			if (indexArchive.exists())
+				indexArchive.delete();
+
+		}
+
+		return product;
+	}
+
+	/**
+	 * Constructor for IndexLoader
+	 */
+	public IndexLoader(String documentBase, String indexFolder, String indexFile) {
+		m_docBase = documentBase;
+		m_indexFolder = indexFolder;
+		m_indexFile = indexFile;
+	}
+
+	/**
+	 * Determines whether the index version is out of date.
+	 */
+	public String checkIndexVersion() throws Exception {
+		String siteFolderName = null;
+
+		try {
+			URL url = new URL(m_docBase
+					+ "/search/index/version.txt"); //$NON-NLS-1$
+			// handle UNC
+			url = UNCUtil.handleURLForUNC(url);
+
+			BufferedInputStream inputS = new BufferedInputStream(url
+					.openStream());
+			InputStreamReader iStreamReader = new InputStreamReader(inputS);
+			BufferedReader read = new BufferedReader(iStreamReader);
+			String version = read.readLine();
+			int idx = (version != null) ? version.indexOf(VERSION_DELIMITER)
+					: -1;
+			if (idx > -1) {
+				String localFolderName = version.substring(idx + 1);
+				userIndexFolderName = removeChar(localFolderName, '*');
+			} else {
+				throw new FileNotFoundException();
+			}
+			read.close();
+		
+			if (userIndexFolderName == null) {
+				throw new FileNotFoundException();
+			}
+
+			userIndexPath = m_indexFolder + userIndexFolderName;
+
+			String userStringVersion = ""; //$NON-NLS-1$
+			File userVersion = new File(userIndexPath + File.separator
+					+ "version.txt"); //$NON-NLS-1$
+			if (userVersion.exists()) {
+				FileReader fRead = new FileReader(userVersion);
+				read = new BufferedReader(fRead);
+				userStringVersion = read.readLine();
+				read.close();
+			}
+			if (version.equals(userStringVersion)) {
+				siteFolderName = userStringVersion.substring(0,
+						userStringVersion.indexOf("*")); //$NON-NLS-1$
+			} else {
+				userVersion.delete();
+				userVersion.getParentFile().delete();
+				userVersion.getParentFile().mkdirs();
+				FileWriter fWrite = new FileWriter(userVersion);
+				BufferedWriter write = new BufferedWriter(fWrite);
+				write.write(version + "\n"); //$NON-NLS-1$
+				write.close();
+			}
+		} catch (FileNotFoundException e) {
+			// try {
+			// IndexBuilder indexBuilder = new IndexBuilder(pathDocumentBase);
+			// indexBuilder.createIndex();
+			// siteFolderName = checkIndexVersion(pathDocumentBase,
+			// indexLocation);
+			// return siteFolderName;
+			// } catch (Exception e2) {
+			// e2.printStackTrace();
+			// }
+		} catch (Exception e1) {
+			e1.printStackTrace();
+		}
+
+		return siteFolderName;
+	}
+	
+	public static String getSiteFolderName(String documentBase) throws Exception {
+		String siteFolderName = (String) cache.get(documentBase);
+		
+		if (siteFolderName != null)
+			return siteFolderName;		
+
+		try {
+			URL url = new URL(documentBase + "/search/index/version.txt"); //$NON-NLS-1$
+			// handle UNC
+			url = UNCUtil.handleURLForUNC(url);
+
+			BufferedInputStream inputS = new BufferedInputStream(url
+					.openStream());
+			InputStreamReader iStreamReader = new InputStreamReader(inputS);
+			BufferedReader read = new BufferedReader(iStreamReader);
+			String version = read.readLine();
+			int idx = (version != null) ? version.indexOf(VERSION_DELIMITER)
+					: -1;
+			if (idx > -1) {
+				siteFolderName = version.substring(0, idx);	
+				cache.put(documentBase, siteFolderName);
+			} else {
+				throw new FileNotFoundException();
+			}
+			read.close();
+		
+			if (siteFolderName == null) {
+				throw new FileNotFoundException();
+			}			
+		} catch (FileNotFoundException fnfe) {
+			fnfe.printStackTrace();
+		} catch (Exception e1) {
+			e1.printStackTrace();
+		}
+
+		return siteFolderName;
+	}
+	
+	public static String removeChar(String s, char c) {
+		String r = "";
+		for (int i = 0; i < s.length(); i++) {
+			if (s.charAt(i) != c)
+				r += s.charAt(i);
+		}
+		return r;
+	}
+
+	public String getUserIndexPath() {
+		return userIndexPath;
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/IndexSearch.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/IndexSearch.java
new file mode 100644
index 0000000..a894a84
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/IndexSearch.java
@@ -0,0 +1,145 @@
+package org.eclipse.epf.web.search;
+
+import java.util.StringTokenizer;
+
+import org.apache.lucene.document.Document;
+import org.apache.lucene.queryParser.ParseException;
+import org.apache.lucene.queryParser.QueryParser;
+import org.apache.lucene.search.Hits;
+import org.apache.lucene.search.IndexSearcher;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.Searcher;
+import org.apache.lucene.search.Sort;
+import org.apache.lucene.search.SortField;
+import org.apache.lucene.store.RAMDirectory;
+import org.eclipse.epf.web.search.analysis.ChineseAnalyzer;
+import org.eclipse.epf.web.search.analysis.TextAnalyzer;
+
+import com.ibm.icu.text.DecimalFormat;
+
+public class IndexSearch {
+
+	private static Searcher searcher;
+
+	public static Hits search(String indexLocation, String inputQuery,
+		String searchField) {
+		
+		Hits hits = null;
+		Query q = null;
+		String queryString = null;
+		DecimalFormat scoreFormatter = new DecimalFormat("0.######");
+
+		if (indexLocation == null || inputQuery == null || searchField == null
+			|| searchField.length() == 0 || inputQuery.length() == 0)
+			return null;
+
+		try {
+			queryString = detectHyphenated(inputQuery);
+
+			if (queryString == null || queryString.length() == 0)
+				return null;
+
+			if (searcher == null) {
+				searcher = new IndexSearcher(new RAMDirectory(indexLocation));
+//				searcher = new IndexSearcher(FSDirectory.getDirectory(indexLocation,
+//					false));
+			}		
+						
+			if ( searcher == null ) {
+				return hits;
+			}
+			
+//			Sort sort = new Sort(new SortField[]{					
+//					new SortField("title"),
+//					new SortField("summary")
+//					});
+			
+			// per user's request, the sorting should be based on the hit rate, 
+			// so don't pass in any sorting criteria
+			Sort sort = null;
+			
+			try {
+				
+				q = QueryParser.parse(queryString, searchField,
+						new TextAnalyzer());
+				if ( q != null ) {
+					hits = searcher.search(q, (Sort)sort);
+				}
+			} catch (Throwable th ) {
+				
+			}
+			
+			// note: icu4j failed with jre 1.5 in server mode.
+			// in such a case, try the old one
+			q = QueryParser.parse(queryString, searchField,
+					new ChineseAnalyzer());
+			if ( q != null ) {
+				hits = searcher.search(q, (Sort)sort);
+			}
+			
+			return hits;
+			
+
+		} catch (ParseException pe) {
+			pe.printStackTrace();
+		} catch (java.io.IOException io) {
+			io.printStackTrace();
+		} catch (Throwable p1) {
+			p1.printStackTrace();
+		}
+
+		return hits;
+	}
+
+	/*
+	 * detect hyphenated words and if hyphenate, put quotes around so that they are
+	 * considered one word for example,"use-case" as opposed to use-case, which Lucene
+	 * interprets as "use" NOT "case" which is not what we want. algorithm: first tokenize
+	 * by space to isolate words next, if words have hyphens, put quotes at the beginning
+	 * and end append to buffer
+	 */
+	public static String detectHyphenated(String queryString) {		
+		StringTokenizer spaceTokenizer = new StringTokenizer(queryString);
+		StringBuffer wordBuffer = new StringBuffer();
+				
+		while (spaceTokenizer.hasMoreTokens()) {
+			// isolate into words
+			String word = spaceTokenizer.nextToken();
+			if ((word.indexOf('-') != -1 ) && !word.startsWith("\"")) //$NON-NLS-1$
+			{
+				// hyphen found, and word is not already an exact phrase,
+				// so add quotes to beginning and end of word
+				wordBuffer.append("\""); //$NON-NLS-1$
+				wordBuffer.append(word);
+				wordBuffer.append("\""); //$NON-NLS-1$
+				//System.out.println("if: " + wordBuffer.toString());
+			} else {
+				//System.out.println("else: " + queryString);
+				return queryString;  //RATLC00251031
+				//wordBuffer.append(word);				
+			}
+			if (spaceTokenizer.hasMoreTokens()) {
+				wordBuffer.append(" "); //$NON-NLS-1$
+			}
+		}
+		
+		
+		return wordBuffer.toString();
+	}
+
+	/**
+	 * @param args
+	 */
+//	public static void main(String[] args) {
+//		if (args.length > 0) {
+//			// String rupPath = System.getProperty( "user.home" ) + java.io.File.separator
+//			// + "rup";
+//			String rupPath = args[0];
+//			SortedHits sortedHits = IndexSearch.search(rupPath + "\\index", "Analyst",
+//				"contents");
+//			System.out
+//				.println("There are " + sortedHits.length() + " hits for 'analyst'");
+//		}
+//
+//	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/SEARCH.properties b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/SEARCH.properties
new file mode 100644
index 0000000..3bf392a
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/SEARCH.properties
@@ -0,0 +1,9 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2003, 2007 IBM Corporation. All Rights Reserved.
+#-------------------------------------------------------------------------------
+# NLS_MESSAGEFORMAT_VAR
+nextActionText=Next
+previousActionText=Previous
+# note: these words should be all lowercase
+SearchFailed=Search request failed
+
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/SearchResources.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/SearchResources.java
new file mode 100644
index 0000000..58f4d3c
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/SearchResources.java
@@ -0,0 +1,23 @@
+package org.eclipse.epf.web.search;
+
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+public class SearchResources {
+	private static final String BUNDLE_NAME = "org.eclipse.epf.web.search.SEARCH"; //$NON-NLS-1$
+
+	private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle
+			.getBundle(BUNDLE_NAME);
+
+	private SearchResources() {
+	}
+
+	public static String getString(String key) {
+		// TODO Auto-generated method stub
+		try {
+			return RESOURCE_BUNDLE.getString(key);
+		} catch (MissingResourceException e) {
+			return '[' + key + ']';
+		}
+	}
+}
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseAnalyzer.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseAnalyzer.java
new file mode 100644
index 0000000..735eebd
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseAnalyzer.java
@@ -0,0 +1,29 @@
+package org.eclipse.epf.web.search.analysis;
+
+import java.io.Reader;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+
+
+public class ChineseAnalyzer extends Analyzer {
+
+	/**
+	 * Default constructor.
+	 */
+	public ChineseAnalyzer()
+	{
+		super();
+	}
+	
+    /**
+    * Creates a TokenStream which tokenizes all the text in the provided Reader.
+    *
+    * @return  A TokenStream build from a ChineseTokenizer filtered with ChineseFilter.
+    */
+    public final TokenStream tokenStream(String fieldName, Reader reader) {
+        TokenStream result = new ChineseTokenizer(reader);
+        result = new ChineseFilter(result);
+        return result;
+    }
+}
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseFilter.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseFilter.java
new file mode 100644
index 0000000..3cf281f
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseFilter.java
@@ -0,0 +1,120 @@
+//------------------------------------------------------------------------------
+// Copyright (c) 2004, 200 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.analysis;
+
+import java.util.Hashtable;
+import java.util.ResourceBundle;
+import java.util.StringTokenizer;
+
+import org.apache.lucene.analysis.StopAnalyzer;
+import org.apache.lucene.analysis.StopFilter;
+import org.apache.lucene.analysis.Token;
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+
+
+public class ChineseFilter extends TokenFilter {
+
+	private Hashtable _stopTable;
+
+    public ChineseFilter(TokenStream in) 
+    {
+    	super(in);
+    	
+        input = in;
+
+        if (_stopTable == null) {
+			loadStopWords();
+		}
+    }
+
+    public final Token next() throws java.io.IOException {
+
+        for (Token token = input.next(); token != null; token = input.next()) {
+            String text = token.termText();
+
+            if (_stopTable.get(text) == null) {
+                switch (Character.getType(text.charAt(0))) {
+
+				// June 3, 2003 - fchong added support for digits for Rational
+				case Character.DECIMAL_DIGIT_NUMBER:
+				case Character.LETTER_NUMBER:
+                case Character.LOWERCASE_LETTER:
+                case Character.UPPERCASE_LETTER:
+
+                    // English word/token should larger than 1 character.
+                    if (text.length()>1) {
+                        return token;
+                    }
+                    break;
+                case Character.OTHER_LETTER:
+
+                    // One Chinese character as one Chinese word.
+                    // Chinese word extraction to be added later here.
+
+                    return token;
+                }
+
+            }
+
+        }
+        return null;
+    }
+    
+    /**
+	 * Loads the stop words defined in the StopWords.properties file.
+	 */
+	private void loadStopWords() {
+		String[] words = null;
+		try {
+			ResourceBundle bundle = ResourceBundle.getBundle(ChineseFilter.class
+					.getPackage().getName()
+					+ ".StopWords"); //$NON-NLS-1$				
+			String property = bundle.getString("Search.stopWords"); //$NON-NLS-1$
+			words = split(property, " ,", -1); //$NON-NLS-1$		
+		} catch (Exception e) {
+			words = StopAnalyzer.ENGLISH_STOP_WORDS;
+		}
+		_stopTable = StopFilter.makeStopTable(words);
+	}
+	
+	/**
+	 * Splits a string into an array of string tokens.
+	 * 
+	 * @param str
+	 *            A string.
+	 * @param sep
+	 *            A string containing the string separators.
+	 * @param count
+	 *            The desired number of string tokens.
+	 * @return An array of string tokens.
+	 */
+	public static String[] split(String str, String sep, int count) {
+		if (str == null || count == 0 || count < -1) {
+			return null;
+		}
+
+		StringTokenizer tokenizer = new StringTokenizer(str, sep,
+				count == -1 ? false : true);
+
+		if (count == -1) {
+			count = tokenizer.countTokens();
+		}
+
+		String[] result = new String[count];
+		int i = 0;
+		while (tokenizer.hasMoreTokens()) {
+			String t = tokenizer.nextToken();
+			if (i < count) {
+				if ((t.length() == 1) && (sep.indexOf(t) != -1)) {
+					continue;
+				}
+				result[i++] = t;
+			} else {
+				result[count - 1] += t;
+			}
+		}
+		return result;
+	}
+}
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseTokenizer.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseTokenizer.java
new file mode 100644
index 0000000..181ebfb
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/ChineseTokenizer.java
@@ -0,0 +1,87 @@
+package org.eclipse.epf.web.search.analysis;
+
+import java.io.Reader;
+
+import org.apache.lucene.analysis.Token;
+import org.apache.lucene.analysis.Tokenizer;
+
+public class ChineseTokenizer extends Tokenizer {
+
+	public ChineseTokenizer(Reader in) {
+        input = in;
+    }
+
+    private int offset = 0, bufferIndex=0, dataLen=0;
+    private final static int MAX_WORD_LEN = 255;
+    private final static int IO_BUFFER_SIZE = 1024;
+    private final char[] buffer = new char[MAX_WORD_LEN];
+    private final char[] ioBuffer = new char[IO_BUFFER_SIZE];
+
+
+    private int length;
+    private int start;
+
+
+    private final void push(char c) {
+
+        if (length == 0) start = offset-1;            // start of token
+        buffer[length++] = Character.toLowerCase(c);  // buffer it
+
+    }
+
+    private final Token flush() {
+
+        if (length>0) {
+            //System.out.println(new String(buffer, 0, length));
+            return new Token(new String(buffer, 0, length), start, start+length);
+        }
+        else
+            return null;
+    }
+
+    public final Token next() throws java.io.IOException {
+
+        length = 0;
+        start = offset;
+
+
+        while (true) {
+
+            final char c;
+            offset++;
+
+            if (bufferIndex >= dataLen) {
+                dataLen = input.read(ioBuffer);
+                bufferIndex = 0;
+            };
+
+            if (dataLen == -1) return flush();
+            else
+                c = (char) ioBuffer[bufferIndex++];
+
+
+            switch(Character.getType(c)) {
+
+            case Character.DECIMAL_DIGIT_NUMBER:
+            case Character.LOWERCASE_LETTER:
+            case Character.UPPERCASE_LETTER:
+                push(c);
+                if (length == MAX_WORD_LEN) return flush();
+                break;
+
+            case Character.OTHER_LETTER:
+                if (length>0) {
+                    bufferIndex--;
+                    return flush();
+                }
+                push(c);
+                return flush();
+
+            default:
+                if (length>0) return flush();
+                break;
+            }
+        }
+
+    }
+}
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/StopWords.properties b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/StopWords.properties
new file mode 100644
index 0000000..5ca97c3
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/StopWords.properties
@@ -0,0 +1,13 @@
+#-------------------------------------------------------------------------------
+# Copyright (c) 2003, 2007 IBM Corporation. All Rights Reserved.
+#-------------------------------------------------------------------------------
+# NLS_MESSAGEFORMAT_VAR
+
+# Search Stop Words
+Search.stopWords=\
+a, about, an, and, are, as, at, be, but, by, for, from, how, \
+i, if, in, into, is, it, no, not, of, on, or, s, such, \
+t, that, the, their, then, there, these, they, this, to, \
+was, what, when, where, who, will, with
+
+
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextAnalyzer.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextAnalyzer.java
new file mode 100644
index 0000000..b3db689
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextAnalyzer.java
@@ -0,0 +1,34 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2005 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.analysis;
+
+import java.io.Reader;
+
+import org.apache.lucene.analysis.Analyzer;
+import org.apache.lucene.analysis.TokenStream;
+
+/**
+* A Text Analyzer that handles Unicode 4.1 characters.
+* 
+* @author Kelvin Low
+* @since 1.0
+*/
+public class TextAnalyzer extends Analyzer {
+
+	/**
+	 * Creates a new instance.
+	 */
+	public TextAnalyzer() {
+		super();
+	}
+
+	/**
+	 * @see org.apache.lucene.analysis.Analyzer#tokenStream(String, Reader)
+	 */
+	public final TokenStream tokenStream(String fieldName, Reader reader) {
+		TokenStream result = new TextTokenizer(reader);
+		return new TextFilter(result);
+	}
+
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextFilter.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextFilter.java
new file mode 100644
index 0000000..7d67b2d
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextFilter.java
@@ -0,0 +1,68 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2007 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.analysis;
+
+import java.io.IOException;
+import java.util.ResourceBundle;
+import java.util.Set;
+
+import org.apache.lucene.analysis.StopAnalyzer;
+import org.apache.lucene.analysis.StopFilter;
+import org.apache.lucene.analysis.Token;
+import org.apache.lucene.analysis.TokenFilter;
+import org.apache.lucene.analysis.TokenStream;
+import org.eclipse.epf.web.search.utils.StrUtil;
+
+
+
+/**
+* A Text Filter that handles Unicode 4.1 characters.
+* 
+* @author Kelvin Low
+* @since 1.0
+*/
+public final class TextFilter extends TokenFilter {
+
+	private static Set stopWords = null;
+
+	/**
+	 * Creates a new instance.
+	 */
+	public TextFilter(TokenStream in) {
+		super(in);
+		if (stopWords == null) {
+			loadStopWords();
+		}
+	}
+
+	/**
+	 * @see org.apache.lucene.analysis.TokenStream#next()
+	 */
+	public final Token next() throws IOException {
+		for (Token token = input.next(); token != null; token = input.next()) {
+			String tokenText = token.termText();
+			if (!stopWords.contains(tokenText)) {
+				return token;
+			}
+		}
+		return null;
+	}
+
+	/**
+	 * Loads the stop words defined in the StopWords.properties file.
+	 */
+	private void loadStopWords() {
+		String[] words = null;
+		try {
+			ResourceBundle bundle = ResourceBundle.getBundle(TextFilter.class
+					.getPackage().getName()
+					+ ".StopWords"); //$NON-NLS-1$				
+			String property = bundle.getString("Search.stopWords"); //$NON-NLS-1$
+			words = StrUtil.split(property, " ,"); //$NON-NLS-1$		
+		} catch (Exception e) {
+			words = StopAnalyzer.ENGLISH_STOP_WORDS;
+		}
+		stopWords = StopFilter.makeStopSet(words);
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextTokenizer.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextTokenizer.java
new file mode 100644
index 0000000..3899f99
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/analysis/TextTokenizer.java
@@ -0,0 +1,76 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2005 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.analysis;
+
+import java.io.IOException;
+import java.io.Reader;
+
+import org.apache.lucene.analysis.Token;
+import org.apache.lucene.analysis.Tokenizer;
+
+import com.ibm.icu.text.BreakIterator;
+
+/**
+* A Text Tokenizer that segments text into words using ICU4J.
+* 
+* @author Kelvin Low
+* @since 1.0
+*/
+public final class TextTokenizer extends Tokenizer {
+
+	private final static int BUFFER_SIZE = 4096;
+
+	private String text;
+
+	private BreakIterator iterator;
+
+	/**
+	 * Creates a new instance.
+	 * 
+	 * @param reader
+	 *            The text source.
+	 */
+	public TextTokenizer(Reader reader) {
+		super(reader);
+		StringBuffer textBuffer = new StringBuffer(BUFFER_SIZE);
+		char[] buffer = new char[BUFFER_SIZE];
+		int charsRead;
+		try {
+			while ((charsRead = reader.read(buffer, 0, BUFFER_SIZE)) > 0) {
+				textBuffer.append(buffer, 0, charsRead);
+			}
+			text = textBuffer.toString();
+			iterator = BreakIterator.getWordInstance();
+			iterator.setText(text);
+		} catch (IOException e) {
+			iterator = null;
+		}
+	}
+
+	/**
+	 * @see org.apache.lucene.analysis.TokenStream#next()
+	 */
+	public final Token next() throws IOException {
+		if (iterator != null) {
+			while (true) {
+				int start = iterator.current();
+				int end = iterator.next();
+				if (end != BreakIterator.DONE) {
+					String tokenText = text.substring(start, end).toLowerCase();
+					if (!tokenText.equals(" ")) { //$NON-NLS-1$
+						if (tokenText.endsWith("'s")) { //$NON-NLS-1$
+							tokenText = tokenText.substring(0, tokenText
+									.length() - 2);
+						}
+						return new Token(tokenText, 0, tokenText.length());
+					}
+				} else {
+					return null;
+				}
+			}
+		}
+		return null;
+	}
+
+}
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/FileUtil.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/FileUtil.java
new file mode 100644
index 0000000..04c1bd1
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/FileUtil.java
@@ -0,0 +1,409 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2006 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.utils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.URL;
+
+import com.ibm.icu.util.ULocale;
+
+/**
+* Implements a utility class for managing directories and files.
+* 
+* @author Kelvin Low
+* @since 6.0
+*/
+public class FileUtil {
+
+	/**
+	 * Platform-specific line separator.
+	 */
+	public static final String LINE_SEP = System.getProperty("line.separator"); //$NON-NLS-1$
+
+	/**
+	 * Private constructor to prevent this class from being instantiated. All
+	 * methods in this class should be static.
+	 */
+	private FileUtil() {
+	}
+
+	/**
+	 * Returns the absolute path for the given file or directory.
+	 * 
+	 * @param file
+	 *            The given file or directory.
+	 * @return The absolute path of the given file or directory.
+	 */
+	public static String getAbsolutePath(File file) {
+		return file.getAbsolutePath().replace('\\', '/');
+	}
+
+	/**
+	 * Returns the absolute path for the given file or directory.
+	 * 
+	 * @param file
+	 *            The given file or directory name.
+	 * @return The absolute path of the given file or directory.
+	 */
+	public static String getAbsolutePath(String file) {
+		return getAbsolutePath(new File(file));
+	}
+
+	/**
+	 * Returns the absolute path for the given URL.
+	 * 
+	 * @param url
+	 *            The given URL.
+	 * @return The absolute path of the given URL.
+	 */
+	public static String getAbsolutePath(URL url) {
+		String pathName = url.getFile().substring(1);
+		String result = NetUtil.decodeUrl(pathName, null);
+		return result;
+	}
+
+	/**
+	 * Returns the parent directory of the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @return The name of the parent directory.
+	 */
+	public static String getParentDirectory(String path) {
+		return (new File(path)).getParent();
+	}
+
+	/**
+	 * Returns the file name and extension from the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @return The file name and the file extension.
+	 */
+	public static String getFileName(String path) {
+		return getFileName(path, true);
+	}
+
+	/**
+	 * Returns the file name from the given path, with or without the file
+	 * extension.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @param withExtension
+	 *            If true, include the file extension in the result.
+	 * @return The file name with or without the file extension.
+	 */
+	public static String getFileName(String path, boolean withExtension) {
+		String normalizedPath = path.replace('\\', '/');
+
+		int prefixLength = 0;
+		if (normalizedPath.startsWith(NetUtil.URI_FILE_PREFIX)) {
+			prefixLength = NetUtil.URI_FILE_PREFIX_SIZE;
+		} else if (normalizedPath.startsWith(NetUtil.URI_HTTP_PREFIX)) {
+			prefixLength = NetUtil.URI_HTTP_PREFIX_SIZE;
+		}
+
+		String fileName;
+		int index = normalizedPath.lastIndexOf("/"); //$NON-NLS-1$
+		if (index < prefixLength) {
+			fileName = normalizedPath.substring(prefixLength);
+		} else {
+			fileName = path.substring(index + 1);
+		}
+
+		if (withExtension) {
+			return fileName;
+		}
+
+		index = fileName.indexOf("."); //$NON-NLS-1$
+		return (index > 0) ? fileName.substring(0, index) : fileName;
+	}
+
+	/**
+	 * Appends the platform specific path separator to the end of the given
+	 * path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @return The path name appended with the platform specific path separator.
+	 */
+	public static String appendSeparator(String path) {
+		return appendSeparator(path, File.separator);
+	}
+
+	/**
+	 * Appends the given path separator to the end of the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @param separator
+	 *            The path separator.
+	 * @return The path name appended with the given separator.
+	 */
+	public static String appendSeparator(String path, String separator) {
+		return path.endsWith(separator) ? path : path + separator;
+	}
+
+	/**
+	 * Removes the ending path separator from the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @return The path name minus the platform specific path separator.
+	 */
+	public static String removeSeparator(String path) {
+		return path.endsWith(File.separator) ? path.substring(0,
+				path.length() - 1) : path;
+	}
+
+	/**
+	 * Removes the ending path separator from the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @return The path name minus the path separator "\\" or "/".
+	 */
+	public static String removeAllSeparator(String path) {
+		return path.endsWith("/") || path.endsWith("\\") ? path.substring(0, path.length() - 1) : path; //$NON-NLS-1$ //$NON-NLS-2$
+	}
+
+	/**
+	 * Removes the ending path separator from the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @param separator
+	 *            The path separator.
+	 * @return The path name minus the separator.
+	 */
+	public static String removeSeparator(String path, String separator) {
+		return path.endsWith(separator) ? path.substring(0, path.length() - 1)
+				: path;
+	}
+
+	/**
+	 * Replaces the file name with another in the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @param oldFileName
+	 *            The old file name.
+	 * @param newFileName
+	 *            The new file name.
+	 * @return The new path name with the new file name.
+	 */
+	public static String replaceFileName(String path, String oldFileName,
+			String newFileName) {
+		int index = path.lastIndexOf(oldFileName);
+		return path.substring(0, index) + newFileName;
+	}
+
+	/**
+	 * Replaces the file extension with another in the given path.
+	 * 
+	 * @param path
+	 *            The path name.
+	 * @param oldFileExt
+	 *            The old file extension.
+	 * @param newFileExt
+	 *            The new file extension.
+	 * @return The new path with the new file extension.
+	 */
+	public static String replaceExtension(String path, String oldExt,
+			String newExt) {
+		int index = path.lastIndexOf(oldExt);
+		return path.substring(0, index) + newExt;
+	}
+
+	/**
+	 * Returns the locale-specific path of a base path.
+	 * 
+	 * @param path
+	 *            The base path name.
+	 * @param localeStr
+	 *            The locale string.
+	 * @return The locale-specific path.
+	 */
+	public static String getLocalePath(String path, String localeStr) {
+		if (StrUtil.isBlank(localeStr)) {
+			return path;
+		}
+		String fileName = getFileName(path);
+		return replaceFileName(path, fileName, localeStr + "/" + fileName); //$NON-NLS-1$
+	}
+
+	/**
+	 * Returns the locale-specific path of a base path.
+	 * 
+	 * @param path
+	 *            The base path name.
+	 * @param locale
+	 *            The locale object.
+	 * @return The locale-specific path.
+	 */
+	public static String getLocalePath(String path, ULocale locale) {
+		return locale == null ? path : getLocalePath(path, locale.toString());
+	}
+
+	/**
+	 * Write the given text to a file.
+	 * 
+	 * @param fileName
+	 *            The target file name.
+	 * @param text
+	 *            The text to write.
+	 * @return true if the given text is written successfully to file.
+	 */
+	public static boolean writeFile(String filename, String text) {
+		FileWriter writer = null;
+		try {
+			writer = new FileWriter(filename);
+			writer.write(text);
+			writer.flush();
+		} catch (IOException e) {
+		} finally {
+			if (writer != null) {
+				try {
+					writer.close();
+					return true;
+				} catch (Exception e) {
+				}
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Write the given text to a file.
+	 * 
+	 * @param fileName
+	 *            The target file name.
+	 * @param text
+	 *            The text to write.
+	 * @return true if the given text is written successfully to file.
+	 */
+	public static boolean writeUTF8File(String filename, String text) {
+		OutputStreamWriter writer = null;
+		FileOutputStream fileOut = null;
+		try {
+			fileOut = new FileOutputStream(filename);
+			writer = new OutputStreamWriter(fileOut, "UTF-8"); //$NON-NLS-1$
+			writer.write(text);
+			writer.flush();
+			fileOut.flush();
+		} catch (IOException e) {
+		} finally {
+			if (writer != null) {
+				try {
+					writer.close();
+					return true;
+				} catch (Exception e) {
+				}
+			}
+			if (fileOut != null) {
+				try {
+					fileOut.close();
+					return true;
+				} catch (Exception e) {
+				}
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Write the content of the given URI to the given output stream.
+	 * 
+	 * @param uri
+	 *            The source URI.
+	 * @param output
+	 *            The output stream.
+	 */
+	public static void writeFile(String uri, OutputStream output)
+			throws IOException {
+		if (uri == null) {
+			return;
+		}
+
+		InputStream input = null;
+		try {
+			input = NetUtil.getInputStream(uri);
+			int bytesRead;
+			byte[] buf = new byte[4096];
+			while ((bytesRead = input.read(buf, 0, 4096)) > 0) {
+				output.write(buf, 0, bytesRead);
+			}
+			output.flush();
+		} finally {
+			if (input != null) {
+				try {
+					input.close();
+				} catch (Exception e) {
+				}
+			}
+		}
+	}
+
+	/**
+	 * Write the content of the given URI to the given PrintWriter.
+	 * 
+	 * @param uri
+	 *            The source URI.
+	 * @param writer
+	 *            The PrintWriter obejct.
+	 */
+	public static void writeFile(String uri, PrintWriter pw) throws IOException {
+		if (uri == null) {
+			return;
+		}
+
+		InputStreamReader input = null;
+		try {
+			input = new InputStreamReader(NetUtil.getInputStream(uri));
+			int charsRead;
+			char[] buf = new char[4096];
+			while ((charsRead = input.read(buf, 0, 4096)) > 0) {
+				pw.write(buf, 0, charsRead);
+			}
+			pw.flush();
+		} finally {
+			if (input != null) {
+				try {
+					input.close();
+				} catch (Exception e) {
+				}
+			}
+		}
+	}
+
+	/**
+	 * Delete all sub-directories and files in the given directory.
+	 * 
+	 * @param dir
+	 *            The directory containing the sub-directories and files.
+	 */
+	public static void delete(File dir) {
+		File[] files = dir.listFiles();
+		if (files != null) {
+			for (int i = 0; i < files.length; i++) {
+				File file = files[i];
+				if (file.isFile()) {
+					file.delete();
+				} else {
+					delete(file);
+				}
+			}
+		}
+		dir.delete();
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/NetUtil.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/NetUtil.java
new file mode 100644
index 0000000..f03ceaf
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/NetUtil.java
@@ -0,0 +1,321 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2006 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.utils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+/**
+* Implements a utility class for managing URLs and URIs.
+* 
+* @author Kelvin Low
+* @since 6.0
+*/
+public class NetUtil {
+
+	/**
+	 * HTTP URI prefix.
+	 */
+	public final static String URI_HTTP_PREFIX = "http://"; //$NON-NLS-1$
+
+	/**
+	 * File URI prefix.
+	 */
+	public final static String URI_FILE_PREFIX = "file:/"; //$NON-NLS-1$
+
+	/**
+	 * HTTP URI prefix size.
+	 */
+	public final static int URI_HTTP_PREFIX_SIZE = URI_HTTP_PREFIX.length();
+
+	/**
+	 * File URI prefix size.
+	 */
+	public final static int URI_FILE_PREFIX_SIZE = URI_FILE_PREFIX.length();
+
+	/**
+	 * A table of hex values.
+	 */
+	private final static String[] HEX_VALUES = {
+			"%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%08", "%09", "%0A", "%0B", "%0C", "%0D", "%0E", "%0F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%18", "%19", "%1A", "%1B", "%1C", "%1D", "%1E", "%1F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%28", "%29", "%2A", "%2B", "%2C", "%2D", "%2E", "%2F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%38", "%39", "%3A", "%3B", "%3C", "%3D", "%3E", "%3F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%48", "%49", "%4A", "%4B", "%4C", "%4D", "%4E", "%4F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%58", "%59", "%5A", "%5B", "%5C", "%5D", "%5E", "%5F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%68", "%69", "%6A", "%6B", "%6C", "%6D", "%6E", "%6F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%78", "%79", "%7A", "%7B", "%7C", "%7D", "%7E", "%7F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%88", "%89", "%8A", "%8B", "%8C", "%8D", "%8E", "%8F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%98", "%99", "%9A", "%9B", "%9C", "%9D", "%9E", "%9F", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%A0", "%A1", "%A2", "%A3", "%A4", "%A5", "%A6", "%A7", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%A8", "%A9", "%AA", "%AB", "%AC", "%AD", "%AE", "%AF", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%B0", "%B1", "%B2", "%B3", "%B4", "%B5", "%B6", "%B7", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%B8", "%B9", "%BA", "%BB", "%BC", "%BD", "%BE", "%BF", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%C0", "%C1", "%C2", "%C3", "%C4", "%C5", "%C6", "%C7", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%C8", "%C9", "%CA", "%CB", "%CC", "%CD", "%CE", "%CF", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%D0", "%D1", "%D2", "%D3", "%D4", "%D5", "%D6", "%D7", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%D8", "%D9", "%DA", "%DB", "%DC", "%DD", "%DE", "%DF", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%E0", "%E1", "%E2", "%E3", "%E4", "%E5", "%E6", "%E7", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%E8", "%E9", "%EA", "%EB", "%EC", "%ED", "%EE", "%EF", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%F0", "%F1", "%F2", "%F3", "%F4", "%F5", "%F6", "%F7", //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+			"%F8", "%F9", "%FA", "%FB", "%FC", "%FD", "%FE", "%FF" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ //$NON-NLS-6$ //$NON-NLS-7$ //$NON-NLS-8$
+	};
+
+	/**
+	 * Private constructor to prevent this class from being instantiated. All
+	 * methods in this class should be static.
+	 */
+	private NetUtil() {
+	}
+
+	/**
+	 * Returns the URI for the given file.
+	 * 
+	 * @param file
+	 *            The input file.
+	 * @return The URI for the given file.
+	 * @throws MalformedURLException
+	 *             if an error occur while constructing the URI for the given
+	 *             file.
+	 */
+	public static String getUri(File file) throws MalformedURLException {
+		String url = file.toURL().toExternalForm();
+		StringBuffer strBuf = new StringBuffer();
+		int urlLength = url.length();
+		for (int i = 0; i < urlLength; i++) {
+			char ch = url.charAt(i);
+			switch (ch) {
+			case ' ':
+				strBuf.append("%20"); //$NON-NLS-1$
+				break;
+			default:
+				strBuf.append(ch);
+				break;
+			}
+		}
+		return strBuf.toString();
+	}
+
+	/**
+	 * Resolves the given URI using the given the base URI.
+	 * 
+	 * @param uri
+	 *            The URI to resolve.
+	 * @param baseUri
+	 *            The base URI.
+	 * @return A fully formed URI.
+	 */
+	public static String resolveUri(String uri, String baseUri) {
+		if (uri == null) {
+			return null;
+		}
+
+		if (uri.startsWith("../")) //$NON-NLS-1$
+		{
+			if (baseUri.endsWith("/")) //$NON-NLS-1$
+			{
+				baseUri = baseUri.substring(0, baseUri.length() - 1);
+			}
+			while (uri.startsWith("../")) //$NON-NLS-1$
+			{
+				uri = uri.substring(3);
+				int index = baseUri.lastIndexOf('/');
+				if (index > 0) {
+					baseUri = baseUri.substring(0, index);
+				}
+			}
+			uri = "/" + uri; //$NON-NLS-1$
+		}
+
+		if (uri.startsWith("/")) //$NON-NLS-1$
+		{
+			return baseUri.endsWith("/") //$NON-NLS-1$
+			? baseUri + uri.substring(1)
+					: baseUri + uri;
+		}
+
+		if (uri.startsWith(URI_HTTP_PREFIX) || uri.startsWith(URI_FILE_PREFIX)) {
+			return uri;
+		}
+
+		return baseUri.endsWith("/") ? baseUri + uri : baseUri + '/' + uri; //$NON-NLS-1$
+	}
+
+	/**
+	 * Returns the input stream for the given URI.
+	 * 
+	 * @param uri
+	 *            The source URI.
+	 * @return The input stream for the given URI.
+	 * @throw MalformedURLException if a given XML document URI is invalid.
+	 * @throw IOException if an I/O error occur while accessing the URI.
+	 */
+	public static InputStream getInputStream(String uri)
+			throws MalformedURLException, IOException {
+		if (uri == null) {
+			return null;
+		}
+
+		if (uri.startsWith(URI_HTTP_PREFIX)) {
+			URL url = new URL(uri);
+			return url.openStream();
+		} else if (uri.startsWith(URI_FILE_PREFIX)) {
+			uri = uri.substring(URI_FILE_PREFIX_SIZE);
+		}
+
+		return new FileInputStream(NetUtil.decodeUrl(uri, null));
+	}
+
+	/**
+	 * Returns the Java string represention (encoded in UTF-16) of the given URL
+	 * (encoded in the given encoding and ASCII-escaped).
+	 * 
+	 * @param url
+	 *            The URL to decode.
+	 * @param encoding
+	 *            The encoding of the URL.
+	 * @return The Java UTF-16 string respresentation.
+	 * @throws IllegalArgumentException
+	 *             if the given URL contain improperly escaped characters.
+	 */
+	public static String decodeUrl(String url, String encoding) {
+		int len = url.length();
+
+		if (url == null || len == 0) {
+			return url;
+		}
+
+		// Unescape the url.
+		StringBuffer strBuf = new StringBuffer();
+		for (int i = 0; i < len; i++) {
+			char ch = url.charAt(i);
+			switch (ch) {
+			case '+':
+				strBuf.append(' ');
+				break;
+			case '%':
+				try {
+					strBuf.append((char) Integer.parseInt(url.substring(i + 1,
+							i + 3), 16));
+				} catch (NumberFormatException e) {
+					throw new IllegalArgumentException();
+				}
+				i += 2;
+				break;
+			default:
+				strBuf.append(ch);
+				break;
+			}
+		}
+
+		// Convert the un-escaped byte values to Java UTF-16 string.
+		String result = strBuf.toString();
+		if (encoding != null) {
+			try {
+				byte[] bytes = result.getBytes("8859_1"); //$NON-NLS-1$
+				result = new String(bytes, encoding);
+			} catch (UnsupportedEncodingException e) {
+			}
+		}
+
+		return result;
+	}
+
+	/**
+	 * Returns the ASCII-escaped representation (encoded in the specified
+	 * encoding) of the given URL (encoded in UTF-16).
+	 * 
+	 * @param url
+	 *            The URL to encode.
+	 * @param encoding
+	 *            The encoding of the URL.
+	 * @return the ASCII-escaped respresentation.
+	 * @throws IllegalArgumentException
+	 *             if the given URL contain improperly escaped characters.
+	 * @throws UnsupportedEncodingException
+	 *             if the given coding is unsupport.
+	 */
+	public static String encodeUrl(String url, String encoding)
+			throws UnsupportedEncodingException {
+		int len = url.length();
+
+		if (url == null || len == 0) {
+			return url;
+		}
+
+		StringBuffer result = new StringBuffer();
+
+		byte[] bytes = url.getBytes(encoding);
+		for (int i = 0; i < bytes.length; i++) {
+			char ch = (char) bytes[i];
+
+			if (ch >= 'a' && ch <= 'z') {
+				result.append(ch);
+			} else if (ch >= 'A' && ch <= 'Z') {
+				result.append(ch);
+			} else if (ch >= '0' && ch <= '9') {
+				result.append(ch);
+			} else {
+				switch (ch) {
+				case '-':
+				case '_':
+				case '.':
+				case '!':
+				case '~':
+				case '*':
+				case '\'':
+				case '(':
+				case ')':
+					result.append(ch);
+					break;
+				default:
+					result.append(HEX_VALUES[ch & 0xFF]);
+					break;
+				}
+			}
+		}
+
+		return result.toString();
+	}
+
+	/**
+	 * Returns the ASCII-escaped representation of the given file URL.
+	 * 
+	 * @param fileURL
+	 *            The file URL to encode.
+	 * @return the ASCII-escaped respresentation.
+	 */
+	public static String encodeFileUrl(String fileURL) {
+		String url = fileURL;
+		StringBuffer strBuf = new StringBuffer();
+		int urlLength = url.length();
+		for (int i = 0; i < urlLength; i++) {
+			char ch = url.charAt(i);
+			switch (ch) {
+			case ' ':
+				strBuf.append("%20"); //$NON-NLS-1$
+				break;
+			default:
+				strBuf.append(ch);
+				break;
+			}
+		}
+		return strBuf.toString();
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/StrUtil.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/StrUtil.java
new file mode 100644
index 0000000..08d4608
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/StrUtil.java
@@ -0,0 +1,482 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2006 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.utils;
+
+import java.util.List;
+import com.ibm.icu.util.StringTokenizer;
+
+/**
+ * Implements a utility class for managing strings.
+ * 
+ * @author Kelvin Low
+ * @since 6.0
+ */
+public class StrUtil {
+
+	// Array of hex digits.
+	private static final char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5',
+			'6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
+
+	/**
+	 * Private constructor to prevent this class from being instantiated. All
+	 * methods in this class should be static.
+	 */
+	private StrUtil() {
+	}
+
+	/**
+	 * Is the given string a null string?
+	 * <p>
+	 * A null string is defined as one that has a empty reference (i.e. = null)
+	 * or has zero length.
+	 * 
+	 * @param str
+	 *            The test string.
+	 * @return true if the the given string is a null string.
+	 */
+	public static boolean isNull(String str) {
+		return str == null || str.length() == 0;
+	}
+
+	/**
+	 * Is the given string a blank string?
+	 * <p>
+	 * A blank string is defined as one that has a empty reference (i.e. = null)
+	 * or has zero length after the leading and trailing space characters are
+	 * trimmed.
+	 * 
+	 * @param str
+	 *            The test string.
+	 * @return true if the the given string is a blank string.
+	 */
+	public static boolean isBlank(String str) {
+		return str == null || str.trim().length() == 0;
+	}
+
+	/**
+	 * Is the given string an ASCII string?
+	 * 
+	 * @param str
+	 *            The test string.
+	 * @return true if the the given string is an ASCII string.
+	 */
+	public static boolean isAscii(String str) {
+		int len = (str == null) ? 0 : str.length();
+		for (int i = 0; i < len; i++) {
+			if (str.charAt(i) > 0x007E) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	/**
+	 * Removes the leading and trailing space characters (' ') from the given
+	 * string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @return A string with no leading and trailing space characters.
+	 */
+	public static String trim(String str) {
+		return str == null ? null : str.trim();
+	}
+
+	/**
+	 * Removes white space characters ('\t', '\n', ' ') from the given string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @return A string with white space characters removed.
+	 */
+	public static String removeWhiteSpaceChars(String str) {
+		int len = (str == null) ? 0 : str.length();
+		for (int i = 0; i < len; i++) {
+			switch (str.charAt(i)) {
+			case '\t':
+			case '\n':
+			case ' ':
+				break;
+
+			default:
+				return str;
+			}
+		}
+		return ""; //$NON-NLS-1$
+	}
+
+	/**
+	 * Removes end-of-line characters from the given string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @return A string with no end-of-line characters.
+	 */
+	public static String removeEndOfLineChars(String str) {
+		if (isNull(str)) {
+			return str;
+		}
+
+		String lineSep = FileUtil.LINE_SEP;
+		int lineSepSize = FileUtil.LINE_SEP.length();
+
+		while (str.endsWith(lineSep)) {
+			str = str.substring(0, str.length() - lineSepSize);
+		}
+		return str;
+	}
+
+	/**
+	 * Removes the given leading and trailing characters from the given string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param chars
+	 *            The characters to removed.
+	 * @return A string with stripped off the given leading and trailing
+	 *         characters.
+	 */
+	public static String removeChars(String str, String chars) {
+		if (isNull(str)) {
+			return str;
+		}
+
+		int sizeOfChars = chars.length();
+
+		while (str.startsWith(chars)) {
+			str = str.substring(sizeOfChars);
+		}
+		while (str.endsWith(chars)) {
+			str = str.substring(0, str.length() - sizeOfChars);
+		}
+
+		return str;
+	}
+
+	/**
+	 * Splits the given strings into an array of tokens based on the given
+	 * separators.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param sep
+	 *            The string separators.
+	 * @param count
+	 *            The number of tokens to return.
+	 * @return An array of string tokens.
+	 */
+	public static String[] split(String str, String sep, int count) {
+		if (str == null || count == 0 || count < -1) {
+			return null;
+		}
+
+		StringTokenizer tok = new StringTokenizer(str, sep, count == -1 ? false
+				: true);
+
+		if (count == -1) {
+			count = tok.countTokens();
+		}
+
+		String[] result = new String[count];
+		int i = 0;
+		while (tok.hasMoreTokens()) {
+			String t = tok.nextToken();
+			if (i < count) {
+				if ((t.length() == 1) && (sep.indexOf(t) != -1)) {
+					continue;
+				}
+				result[i++] = t;
+			} else {
+				result[count - 1] += t;
+			}
+		}
+		return result;
+	}
+
+	/**
+	 * Splits the given strings into an array of tokens based on the given
+	 * separators.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param sep
+	 *            The string separators.
+	 * @return An array of string tokens.
+	 */
+	public static String[] split(String str, String sep) {
+		return split(str, sep, -1);
+	}
+
+	/**
+	 * Returns true if a the given sub-string matches a string token in the
+	 * delimited source string.
+	 * 
+	 * @param str
+	 *            The delimited source string.
+	 * @param strSep
+	 *            The separators that delimit the source string.
+	 * @param substr
+	 *            The delimited sub-string.
+	 * @return true if the given substr is found in the source string.
+	 */
+	public static boolean contains(String str, String strSep, String substr) {
+		if (str == null || strSep == null || substr == null) {
+			return false;
+		}
+		String[] strs = split(str, strSep, -1);
+		if (strs != null) {
+			for (int i = 0; i < strs.length; i++) {
+				if (strs[i] != null && strs[i].equals(substr)) {
+					return true;
+				}
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Returns true if a string token in the given sub-string matches a string
+	 * token in the delimited source string.
+	 * 
+	 * @param str
+	 *            The delimited source string.
+	 * @param strSep
+	 *            The separators that delimit the source string.
+	 * @param substr
+	 *            Tthe delimited sub-string.
+	 * @param subStrSep
+	 *            The separators that delimit the sub-string.
+	 * @return true if the given substr is found in the source string.
+	 */
+	public static boolean contains(String str, String strSep, String substr,
+			String subStrSep) {
+		if (str == null || strSep == null || substr == null
+				|| subStrSep == null) {
+			return false;
+		}
+
+		String[] strs = split(str, strSep, -1);
+		String[] substrs = split(substr, subStrSep, -1);
+
+		if (strs != null && substrs != null) {
+			for (int i = 0; i < strs.length; i++) {
+				if (strs[i] != null) {
+					for (int j = 0; j < substrs.length; j++) {
+						if (substrs[j] != null && substrs[j].equals(strs[i])) {
+							return true;
+						}
+					}
+				}
+			}
+		}
+		return false;
+	}
+
+	/**
+	 * Replaces a substring within a string with another string. This just
+	 * replaces the first occurrence of the string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param src
+	 *            The substring to replace.
+	 * @param tgt
+	 *            The substring to use for the replacement.
+	 * @return The result string.
+	 */
+	public static String replace(String str, String src, String tgt) {
+		if (isNull(str) || isNull(src)) {
+			return str;
+		}
+
+		String tmpStr = str;
+		int index;
+		while ((index = tmpStr.indexOf(src)) != -1) {
+			tmpStr = tmpStr.substring(0, index) + tgt
+					+ tmpStr.substring(index + src.length());
+		}
+		return tmpStr;
+	}
+
+	/**
+	 * Converts all characters in a string to the given character.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param ch
+	 *            The target character.
+	 * @return The converted string.
+	 */
+	public static String convert(String str, char ch) {
+		if (isNull(str)) {
+			return str;
+		}
+
+		int len = str.length();
+		StringBuffer buf = new StringBuffer(len + 1);
+		for (int i = 0; i < len; i++) {
+			buf.append(ch);
+		}
+		return buf.toString();
+	}
+
+	/**
+	 * Replaces the given set of characters in the given string with a target
+	 * character.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param srcChars
+	 *            The set of characters to replace.
+	 * @param tgtChar
+	 *            The target character.
+	 * @return The converted string.
+	 */
+	public static String replace(String str, String srcChars, char tgtChar) {
+		for (int i = 0; i < srcChars.length(); i++) {
+			str = str.replace(srcChars.charAt(i), tgtChar);
+		}
+		return str;
+	}
+
+	/**
+	 * Gets the integer value of the given string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param defValue
+	 *            The default integer value.
+	 * @param The
+	 *            integer value of the given string.
+	 */
+	public static int getIntValue(String str, int defValue) {
+		if (StrUtil.isBlank(str)) {
+			return defValue;
+		}
+
+		try {
+			return Integer.parseInt(str);
+		} catch (NumberFormatException e) {
+			return defValue;
+		}
+	}
+
+	/**
+	 * Returns an array of bytes representing the UTF-8 encoding of the given
+	 * string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @return A byte array containing the UTF-8 encoding of the given string.
+	 */
+	public static byte[] getUTF8Bytes(String str) {
+		char[] c = str.toCharArray();
+		int len = c.length;
+		int count = 0;
+		for (int i = 0; i < len; i++) {
+			int ch = c[i];
+			if (ch <= 0x7f) {
+				count++;
+			} else if (ch <= 0x7ff) {
+				count += 2;
+			} else {
+				count += 3;
+			}
+		}
+
+		byte[] b = new byte[count];
+		int off = 0;
+		for (int i = 0; i < len; i++) {
+			int ch = c[i];
+			if (ch <= 0x7f) {
+				b[off++] = (byte) ch;
+			} else if (ch <= 0x7ff) {
+				b[off++] = (byte) ((ch >> 6) | 0xc0);
+				b[off++] = (byte) ((ch & 0x3f) | 0x80);
+			} else {
+				b[off++] = (byte) ((ch >> 12) | 0xe0);
+				b[off++] = (byte) (((ch >> 6) & 0x3f) | 0x80);
+				b[off++] = (byte) ((ch & 0x3f) | 0x80);
+			}
+		}
+		return b;
+	}
+
+	/**
+	 * Returns the hexidecimal character representation for the given integer.
+	 * 
+	 * @param value
+	 *            The integer value.
+	 * @return The hex representation.
+	 */
+	private static char toHex(int value) {
+		return HEX_DIGITS[(value & 0xF)];
+	}
+
+	/**
+	 * Returns the escaped Unicode string representation of the given Java
+	 * string.
+	 * 
+	 * @param str
+	 *            The Java string (encoded in UTF-16).
+	 * @param skipAscii
+	 *            If true, avoid escaping the ASCII characters.
+	 * @return The escaped Unicode string representation.
+	 */
+	public static String toEscapedUnicode(String str, boolean skipAscii) {
+		int len = str.length();
+		StringBuffer result = new StringBuffer(len * 2);
+
+		for (int i = 0; i < len; i++) {
+			char ch = str.charAt(i);
+
+			if (skipAscii && ch < 0x007E) {
+				result.append(ch);
+			} else {
+				result.append("\\u"); //$NON-NLS-1$
+				result.append(toHex((ch >> 12) & 0xF));
+				result.append(toHex((ch >> 8) & 0xF));
+				result.append(toHex((ch >> 4) & 0xF));
+				result.append(toHex(ch & 0xF));
+			}
+		}
+
+		return result.toString();
+	}
+
+	/**
+	 * Convert List to String Array
+	 * 
+	 * @param list
+	 * @return String[]
+	 */
+	public static String[] convertListToStrArray(List list) {
+		if (list != null) {
+			int cnt = list.size();
+			String[] strArray = new String[cnt];
+
+			for (int i = 0; i < cnt; i++) {
+				String str = (String) list.get(i);
+				strArray[i] = new String(str);
+			}
+			return strArray;
+		} else {
+			return null;
+		}
+	}
+
+	public static String convertFirstLetterCase(String s, String[] keyWords) {
+		StringBuffer sb = new StringBuffer(s.substring(0, 1).toUpperCase()
+				+ s.substring(1).toLowerCase());
+		int foundIndex;
+		for (int i = 0; i < keyWords.length; i++) {
+			if ((foundIndex = s.indexOf(keyWords[i].toLowerCase(), 1)) != -1) {
+				sb.replace(foundIndex, foundIndex + keyWords[i].length(),
+						keyWords[i]);				 
+			}
+		}
+		//System.out.println(sb.toString());
+		return sb.toString();
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/UNCUtil.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/UNCUtil.java
new file mode 100644
index 0000000..2de53ab
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/UNCUtil.java
@@ -0,0 +1,305 @@
+package org.eclipse.epf.web.search.utils;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
+import java.util.Vector;
+
+
+public class UNCUtil {
+	public static final String UNC_FILE_PREFIX = "file:/";
+	public static final String ALTERNATE_UNC_FILE_PREFIX = "file://";
+	public static final String NETSCAPE_UNC_FILE_PREFIX = "file:///";
+	public static final String UNC_FILE_ONLY = "file:";
+	public static final String UNC_SEPARATOR = "/";
+
+
+	/**
+	 * Accounts for netscape unc file prefix. Converts to standard one.
+	 * If not Netscape, nothing changes.
+	 */
+	public static String handleNetscapeFilePrefix(String filename)
+	{
+		String convertedName = null;
+
+		if (filename.startsWith(NETSCAPE_UNC_FILE_PREFIX))
+		{
+			convertedName =
+				ALTERNATE_UNC_FILE_PREFIX
+					+ filename.substring(
+						NETSCAPE_UNC_FILE_PREFIX.length(),
+						filename.length());
+		}
+		else
+		{
+			convertedName = filename;
+		}
+
+		return (convertedName);
+	}
+
+	/**
+	 * Handles URL paths for Mozilla (and other browsers) over UNC.
+	 */
+	public static URL handleURLForUNC(URL originalUrl)
+	{
+		// handle special case for Mozilla over UNC
+		URL newUrl = null;
+		try
+		{
+			if (originalUrl.toString().startsWith("file://"))
+			{
+				// change "file://" to "file://///"
+				String extractedString = originalUrl.toString().substring(5);
+				newUrl = new URL("file", "", "///" + extractedString);
+			}
+			else
+			{
+				newUrl = originalUrl;
+			}
+		}
+		catch (java.net.MalformedURLException mue)
+		{
+			newUrl = originalUrl;
+		}
+
+		return (newUrl);
+	}
+
+	/**
+	 * Converts the UNC file to a regular, platform-dependent one.
+	 */
+	public static String convertFilename(String filename)
+	{
+		String convertedName = null;
+
+		// remove any URL prefixes
+		String tempName = null;
+		if (filename.startsWith(UNC_FILE_PREFIX))
+		{
+			tempName = filename.substring(UNC_FILE_PREFIX.length(), filename.length());
+			int index = tempName.indexOf(":");
+			if (index == -1)
+			{
+				// on Unix, add separator back to first character
+				tempName = UNC_SEPARATOR + tempName;
+			}
+		}
+		else
+		{
+			tempName = filename;
+		}
+
+		// convert any UNC separators to the system default one
+		convertedName =
+			tempName.replace(UNC_SEPARATOR.charAt(0), File.separator.charAt(0));
+
+		return (convertedName);
+	}
+
+	/**
+	 * Converts the filename to a UNC filename.
+	 */
+	public static String convertFilenameToUNC(String filename)
+	{
+		// convert any UNC separators to the system default one
+		String convertedName =
+			filename.replace(File.separator.charAt(0), UNC_SEPARATOR.charAt(0));
+		String finalName = convertFileSeparator(convertedName);
+		return (finalName);
+	}
+
+	/**
+	 * Converts the URL to one which can that matches a UNC convention.
+	 */
+	public static String convertToUNC(String url)
+	{
+		String convertedUrl = null;
+		if (url.startsWith(ALTERNATE_UNC_FILE_PREFIX))
+		{
+			convertedUrl =
+				UNC_FILE_PREFIX
+					+ url.substring(ALTERNATE_UNC_FILE_PREFIX.length(), url.length());
+		}
+		else
+		{
+			convertedUrl = url;
+		}
+		return (convertedUrl);
+
+	}
+
+	/**
+	 * Removes the "%20" that is used by some browsers for spaces
+	 */
+	public static String convertFileSpacing(String filename)
+	{
+		// now remove any "%20"
+		int start = 0;
+		String newFilename = "";
+		while (start < filename.length())
+		{
+			int index = filename.indexOf("%20", start);
+			if (index == -1)
+			{
+				index = filename.length();
+			}
+			newFilename += filename.substring(start, index);
+			newFilename += " ";
+			start = index + 3;
+		}
+
+		newFilename = newFilename.trim();
+
+		return (newFilename);
+	}
+
+	/**
+	 * Removes the "%5C" that is used by some browsers for file separator.
+	 */
+	public static String convertFileSeparator(String filename)
+	{
+		// remove any "%5C"
+		int start = 0;
+		String newFilename = "";
+		while (start < filename.length())
+		{
+			int index = filename.indexOf("%5C", start);
+			if (index == -1)
+			{
+				//index = filename.length();
+				newFilename = filename.trim();
+				break;
+			}
+			newFilename += filename.substring(start, index);
+			newFilename += UNC_SEPARATOR;
+			start = index + 3;
+		}
+
+		newFilename = newFilename.trim();
+
+		// also if file separator equals unc separator, remove any Windows
+		// specific "\"
+		String windowsFileSeparator = "\\";
+		if (!File.separator.equals(windowsFileSeparator)
+			&& File.separator.equals(UNC_SEPARATOR))
+		{
+			// on Unix
+			newFilename =
+				newFilename.replace(
+					windowsFileSeparator.charAt(0),
+					UNC_SEPARATOR.charAt(0));
+		}
+		return (newFilename);
+	}
+
+	/**
+	 * Retrieves the file names from the specified directory
+	 * that matches the given suffix.  An example of a 
+	 * suffix is ".dat".
+	 */
+	public static Vector getFileList(String directory, String suffix)
+	{
+		if (directory.startsWith("http"))
+		{
+			// remote directory, use this instead
+			return (getFileListFromRemote(directory, suffix));
+		}
+
+		String tempDir = UNCUtil.convertFilename(directory);
+		File documentDirectory = new File(UNCUtil.convertFileSpacing(tempDir));
+		String[] fileNameList = documentDirectory.list();
+
+		// now parse and only take those that end with the proper suffix
+		Vector finalList = new Vector();
+		for (int i = 0; i < fileNameList.length; i++)
+		{
+			if (fileNameList[i].endsWith(suffix))
+			{
+				finalList.addElement(fileNameList[i]);
+			}
+		}
+
+		return (finalList);
+	}
+
+	/**
+	 * Retrieves the applicable default rup files from remote directory.
+	 */
+	private static Vector getFileListFromRemote(String directory, String suffix)
+	{
+		Vector filenames = new Vector();
+		try
+		{
+			// get the listing using a URL
+			URL remoteUrl = new URL(directory);
+			InputStream inStream = remoteUrl.openStream();
+			StringBuffer result = new StringBuffer();
+			int c;
+			while ((c = inStream.read()) != -1)
+			{
+				result.append((char) c);
+			}
+			inStream.close();
+
+			// convert string to upper case to handle multiple server types such as Apache
+			String directoryResult = result.toString();
+			directoryResult = directoryResult.replaceAll("href", "HREF");
+			directoryResult = directoryResult.replaceAll("</a", "</A");
+			directoryResult = directoryResult.replaceAll("<tt>", "");
+			directoryResult = directoryResult.replaceAll("</tt>", "");
+			directoryResult = directoryResult.replaceAll("<TT>", "");
+			directoryResult = directoryResult.replaceAll("</TT>", "");
+
+			// parse the content
+			//StringTokenizer lineTokenizer = new StringTokenizer( result.toString() );
+			StringTokenizer lineTokenizer = new StringTokenizer(directoryResult);
+			Vector lines = new Vector();
+			try
+			{
+				while (true)
+				{
+					lineTokenizer.nextToken("REF");
+					lineTokenizer.nextToken("\"");
+					String line = lineTokenizer.nextToken("\"");
+
+					// strip out "</A"; needed this because StringTokenizer
+					// has problems parsing filenames that being with "A"
+					int index = line.indexOf("</A");
+					if (index != -1 && index != 0)
+					{
+						String filename = line.substring(0, index);
+						lines.addElement(filename);
+					}
+					else
+					{
+						lines.addElement(line);
+					}
+				}
+			}
+			catch (NoSuchElementException ne)
+			{
+				// do nothing, end of content
+			}
+
+			// parse each line
+			for (int i = 0; i < lines.size(); i++)
+			{
+				String line = (String) lines.elementAt(i);
+
+				if (line.endsWith(suffix))
+				{
+					// we want this
+					filenames.addElement(line);
+				}
+			}
+		}
+		catch (Exception e)
+		{
+			e.printStackTrace();
+		}
+		return (filenames);
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/XMLUtil.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/XMLUtil.java
new file mode 100644
index 0000000..239d02b
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/XMLUtil.java
@@ -0,0 +1,562 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2006 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.w3c.dom.Attr;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXParseException;
+
+/**
+* Utility class for processing XML documents.
+* 
+* @author Kelvin Low
+* @author Jinhua Xi
+* @since 7.0
+*/
+public class XMLUtil {
+
+	/**
+	 * XML declaration.
+	 */
+	public final static String XML_DECLARATION = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"; //$NON-NLS-1$
+
+	/**
+	 * XML Escape characters.
+	 */
+	public final static String XML_AMP = "&amp;"; //$NON-NLS-1$
+
+	public final static String XML_APOS = "&apos;"; //$NON-NLS-1$
+
+	public final static String XML_CR = "&#13;"; //$NON-NLS-1$
+
+	public final static String XML_GT = "&gt;"; //$NON-NLS-1$
+
+	public final static String XML_LT = "&lt;"; //$NON-NLS-1$
+
+	public final static String XML_LF = "&#10;"; //$NON-NLS-1$	
+
+	public final static String XML_QUOT = "&quot;"; //$NON-NLS-1$
+
+	public final static String XML_TAB = "&#9;"; //$NON-NLS-1$
+
+	private static final String CRLF = "\r\n"; //$NON-NLS-1$
+
+	private static final byte[] CRLF_BYTES = CRLF.getBytes();
+
+	/**
+	 * Private constructor to prevent this class from being instantiated. All
+	 * methods in this class should be static.
+	 */
+	private XMLUtil() {
+	}
+
+	/**
+	 * Clones the given DOM node into the given DOM document.
+	 * 
+	 * @param node
+	 *            The DOM node to clone.
+	 * @param doc
+	 *            The target DOM document.
+	 * @return The cloned node in the target DOM document.
+	 */
+	public static Node cloneNode(Node node, Document doc) {
+		Node clone = null;
+		switch (node.getNodeType()) {
+		case Node.ELEMENT_NODE:
+			clone = doc.createElement(node.getNodeName());
+			NamedNodeMap attrs = node.getAttributes();
+			for (int i = 0; i < attrs.getLength(); i++) {
+				Node attrNode = attrs.item(i);
+				Attr attrClone = doc.createAttribute(attrNode.getNodeName());
+				attrClone.setNodeValue(attrNode.getNodeValue());
+				((Element) clone).setAttributeNode(attrClone);
+			}
+
+			// Iterate through each child nodes.
+			NodeList childNodes = node.getChildNodes();
+			if (childNodes != null) {
+				for (int i = 0; i < childNodes.getLength(); i++) {
+					Node childNode = childNodes.item(i);
+					Node childClone = cloneNode(childNode, doc);
+					clone.appendChild(childClone);
+				}
+			}
+			break;
+
+		case Node.TEXT_NODE:
+		case Node.CDATA_SECTION_NODE:
+			clone = doc.createTextNode(node.getNodeName());
+			clone.setNodeValue(node.getNodeValue());
+			break;
+		}
+		return clone;
+	}
+
+	/**
+	 * Escapes the given string to make it XML parser friendly.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @return The escaped string.
+	 */
+	public static String escape(String str) {
+		if (str == null || str.length() == 0)
+			return ""; //$NON-NLS-1$
+		StringBuffer sb = new StringBuffer();
+		int len = str.length();
+		for (int i = 0; i < len; i++) {
+			char ch = str.charAt(i);
+			switch (ch) {
+			case '<':
+				sb.append(XML_LT);
+				break;
+			case '>':
+				sb.append(XML_GT);
+				break;
+			case '&':
+				sb.append(XML_AMP);
+				break;
+			case '"':
+				sb.append(XML_QUOT);
+				break;
+			case '\'':
+				sb.append(XML_APOS);
+				break;
+			case '\r':
+				sb.append(XML_CR);
+				break;
+			case '\n':
+				sb.append(XML_LF);
+				break;
+			default:
+				sb.append(ch);
+				break;
+			}
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Escapes the given string to make it XML parser friendly.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @param ignoreCRLF
+	 *            If true, do not escape the CR and LF characters.
+	 * @return The escaped string.
+	 */
+	public static String escape(String str, boolean ignoreCRLF) {
+		if (str == null || str.length() == 0)
+			return ""; //$NON-NLS-1$
+		StringBuffer sb = new StringBuffer();
+		int len = str.length();
+		for (int i = 0; i < len; i++) {
+			char ch = str.charAt(i);
+			switch (ch) {
+			case '<':
+				sb.append(XML_LT);
+				break;
+			case '>':
+				sb.append(XML_GT);
+				break;
+			case '&':
+				sb.append(XML_AMP);
+				break;
+			case '"':
+				sb.append(XML_QUOT);
+				break;
+			case '\'':
+				sb.append(XML_APOS);
+				break;
+			case '\r':
+				if (ignoreCRLF)
+					sb.append(ch);
+				else
+					sb.append(XML_CR);
+				break;
+			case '\n':
+				if (ignoreCRLF)
+					sb.append(ch);
+				else
+					sb.append(XML_LF);
+				break;
+			default:
+				sb.append(ch);
+				break;
+			}
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Unescapes the given XML string.
+	 * 
+	 * @param str
+	 *            The source string.
+	 * @return The escaped string.
+	 */
+	public static String unescape(String str) {
+		if (str == null || str.length() == 0)
+			return ""; //$NON-NLS-1$
+		StringBuffer sb = new StringBuffer();
+		int len = str.length();
+		for (int i = 0; i < len; i++) {
+			char ch = str.charAt(i);
+			switch (ch) {
+			case '&':
+				if (str.startsWith(XML_LT, i)) {
+					sb.append('<');
+					i += 3;
+				} else if (str.startsWith(XML_GT, i)) {
+					sb.append('>');
+					i += 3;
+				} else if (str.startsWith(XML_AMP, i)) {
+					sb.append('&');
+					i += 4;
+				} else if (str.startsWith(XML_QUOT, i)) {
+					sb.append('"');
+					i += 5;
+				} else if (str.startsWith(XML_APOS, i)) {
+					sb.append("\'"); //$NON-NLS-1$
+					i += 5;
+				} else if (str.startsWith(XML_CR, i)) {
+					sb.append('\r');
+					i += 4;
+				} else if (str.startsWith(XML_LF, i)) {
+					sb.append('\n');
+					i += 4;
+				} else {
+					sb.append(ch);
+				}
+				break;
+			default:
+				sb.append(ch);
+				break;
+			}
+		}
+		return sb.toString();
+	}
+
+	/**
+	 * Writes the content of the given DOM document to the output stream.
+	 * 
+	 * @param xmlDoc
+	 *            The DOM document.
+	 * @param output
+	 *            The output stream.
+	 * @throws IOException
+	 *             if an I/O error occur while accessing the output stream.
+	 */
+	public static void writeDocument(Document xmlDoc, OutputStream output)
+			throws IOException {
+		DataOutputStream out = new DataOutputStream(output);
+		writeNode(xmlDoc, "", out); //$NON-NLS-1$
+		out.flush();
+	}
+
+	/**
+	 * Writes the content of the given DOM document to the PrintWriter.
+	 * 
+	 * @param xmlDoc
+	 *            The DOM document.
+	 * @param pw
+	 *            The PrintWriter object.
+	 * @throws IOException
+	 *             if an I/O error occur while accessing the output stream.
+	 */
+	public static void writeDocument(Document xmlDoc, PrintWriter pw)
+			throws IOException {
+		ByteArrayOutputStream os = new ByteArrayOutputStream();
+		DataOutputStream out = new DataOutputStream(os);
+		writeNode(xmlDoc, "", out); //$NON-NLS-1$
+		out.flush();
+		// TODO: Investigate whether encoding should be specified?
+		String s = os.toString();
+		pw.write(s);
+		pw.flush();
+	}
+
+	/**
+	 * Saves the content of the given DOM document to file.
+	 * 
+	 * @param xmlDoc
+	 *            The DOM document.
+	 * @param xmlFile
+	 *            The XML file.
+	 * @throws IOException
+	 *             if an I/O error occur while accessing the output stream.
+	 */
+	public static void saveDocument(Document xmlDoc, String xmlFile)
+			throws IOException {
+		DataOutputStream out = new DataOutputStream(new FileOutputStream(
+				xmlFile));
+		writeNode(xmlDoc, "", out); //$NON-NLS-1$
+		out.flush();
+		out.close();
+	}
+
+	/**
+	 * Saves the given XML string to the given file.
+	 * 
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param xmlFile
+	 *            The XML file.
+	 * @throws IOException
+	 *             if an I/O error occur while accessing the output stream.
+	 */
+	public static void saveDocument(String xmlStr, String xmlFile)
+			throws IOException {
+		DataOutputStream out = new DataOutputStream(new FileOutputStream(
+				xmlFile));
+		out.write(xmlStr.getBytes());
+		out.flush();
+		out.close();
+	}
+
+	/**
+	 * Writes the given DOM tree node to the given output stream.
+	 * 
+	 * @param node
+	 *            The DOM node.
+	 * @param indent
+	 *            The string indentation (containing space characters).
+	 * @param out
+	 *            The output stream.
+	 * @throws IOException
+	 *             if an I/O error occur while accessing the output stream.
+	 */
+	private static void writeNode(Node node, String indent, DataOutputStream out)
+			throws IOException {
+		String text;
+
+		switch (node.getNodeType()) {
+		case Node.DOCUMENT_NODE:
+			// Write the XML file signature.
+			out.write(StrUtil.getUTF8Bytes(XML_DECLARATION));
+			out.write(CRLF_BYTES);
+
+			// Iterate through each child nodes.
+			NodeList nodes = node.getChildNodes();
+			if (nodes != null) {
+				for (int i = 0; i < nodes.getLength(); i++) {
+					writeNode(nodes.item(i), "", out); //$NON-NLS-1$
+				}
+			}
+			break;
+
+		case Node.ELEMENT_NODE:
+			String name = node.getNodeName();
+			out.write(StrUtil.getUTF8Bytes(indent + "<" + name)); //$NON-NLS-1$
+			NamedNodeMap attrs = node.getAttributes();
+			for (int i = 0; i < attrs.getLength(); i++) {
+				Node attrNode = attrs.item(i);
+				out.write(StrUtil.getUTF8Bytes(" " + attrNode.getNodeName() //$NON-NLS-1$
+						+ "=\"" + escape(attrNode.getNodeValue()) + "\"")); //$NON-NLS-1$ //$NON-NLS-2$
+			}
+			out.write(StrUtil.getUTF8Bytes(">")); //$NON-NLS-1$
+			out.write(CRLF_BYTES);
+
+			// Iterate through each child nodes.
+			NodeList childNodes = node.getChildNodes();
+			if (childNodes != null) {
+				for (int i = 0; i < childNodes.getLength(); i++) {
+					writeNode(childNodes.item(i), indent, out);
+				}
+			}
+			out.write(StrUtil.getUTF8Bytes(indent + "</" + name + ">")); //$NON-NLS-1$ //$NON-NLS-2$
+			out.write(CRLF_BYTES);
+
+			break;
+
+		case Node.TEXT_NODE:
+			text = StrUtil.removeWhiteSpaceChars(node.getNodeValue());
+			if (text.length() > 0) {
+				out.write(StrUtil.getUTF8Bytes(escape(text)));
+			}
+			break;
+
+		case Node.CDATA_SECTION_NODE:
+			text = StrUtil.removeWhiteSpaceChars(node.getNodeValue());
+			if (text.length() > 0) {
+				out.write(StrUtil.getUTF8Bytes("<![CDATA[")); //$NON-NLS-1$
+				out.write(StrUtil.getUTF8Bytes(text));
+				out.write(StrUtil.getUTF8Bytes("]]>")); //$NON-NLS-1$
+				out.write(CRLF_BYTES);
+			}
+			break;
+
+		case Node.PROCESSING_INSTRUCTION_NODE:
+			out.write(StrUtil.getUTF8Bytes("<?" + node.getNodeName() //$NON-NLS-1$
+					+ " " + node.getNodeValue() + "?>")); //$NON-NLS-1$ //$NON-NLS-2$
+			out.write(CRLF_BYTES);
+			break;
+
+		case Node.ENTITY_REFERENCE_NODE:
+			out.write(StrUtil.getUTF8Bytes("&" + node.getNodeName() + ";")); //$NON-NLS-1$ //$NON-NLS-2$
+			break;
+		}
+	}
+
+	/**
+	 * Returns the file location where the given SAX exception occurred.
+	 * 
+	 * @param e
+	 *            The SAX parse exception.
+	 * @return A string containing the file location where the exception
+	 *         occurred.
+	 */
+	public static String getLocationOfException(SAXParseException e) {
+		StringBuffer sb = new StringBuffer();
+		sb.append("row "); //$NON-NLS-1$
+		sb.append(e.getLineNumber());
+		sb.append(", col "); //$NON-NLS-1$
+		sb.append(e.getColumnNumber());
+
+		String systemId = e.getSystemId();
+		if (systemId != null) {
+			int index = systemId.lastIndexOf('/');
+			if (index != -1) {
+				systemId = systemId.substring(index + 1);
+			}
+			sb.append(" of XML document "); //$NON-NLS-1$
+			sb.append(systemId);
+		}
+
+		return sb.toString();
+	}
+
+	public static Document loadXml(File file) throws Exception {
+		DocumentBuilderFactory builderFactory = DocumentBuilderFactory
+				.newInstance();
+		DocumentBuilder builder = builderFactory.newDocumentBuilder();
+
+		return builder.parse(file);
+	}
+
+	public static Document createDocument() throws Exception {
+		DocumentBuilderFactory builderFactory = DocumentBuilderFactory
+				.newInstance();
+		DocumentBuilder builder = builderFactory.newDocumentBuilder();
+		return builder.newDocument();
+	}
+
+	/**
+	 * text of a leaf node, without child element
+	 * 
+	 * @param tag
+	 * @return String
+	 */
+	public static String getNodeText(Element tag) {
+		String text = tag.toString();
+		int i = text.indexOf(">"); //$NON-NLS-1$
+		int j = text.lastIndexOf("</"); //$NON-NLS-1$
+		if (i < 0 || j < 0 || j < i) {
+			return ""; //$NON-NLS-1$
+		}
+
+		return text.substring(i + 1, j);
+	}
+
+	public static String getChildText(Element tag, String childTagName) {
+		Element child = getFirstChild(tag, childTagName);
+		if (child != null) {
+			return getNodeText(child);
+		}
+
+		return ""; //$NON-NLS-1$
+	}
+
+	public static Element getFirstChild(Element tag, String childTagName) {
+		NodeList nodes = tag.getElementsByTagName(childTagName);
+		if (nodes == null || nodes.getLength() == 0) {
+			return null;
+		}
+
+		return (Element) nodes.item(0);
+	}
+
+	/**
+	 * iterator of all the children of the element
+	 * 
+	 * @param tag
+	 * @return Iterator
+	 */
+	public static Iterator childIterator(Element tag) {
+		NodeList nodes = tag.getChildNodes();
+
+		// NodeList contains no Element nodes such as text nodes, ignore those
+		List elements = new ArrayList();
+		if (nodes != null) {
+			int size = nodes.getLength();
+			for (int i = 0; i < size; i++) {
+				Node node = nodes.item(i);
+				if (node instanceof Element) {
+					elements.add(node);
+				}
+			}
+		}
+
+		return elements.iterator();
+	}
+
+	private static class NodeIterator implements Iterator {
+		int currentIndex = -1;
+
+		int size = 0;
+
+		NodeList nodes = null;
+
+		public NodeIterator(NodeList nodes) {
+			this.nodes = nodes;
+			if (nodes != null)
+				size = nodes.getLength();
+		}
+
+		public void remove() {
+			// Do nothing, this is a readonly iterator.
+		}
+
+		public boolean hasNext() {
+			return currentIndex + 1 < size;
+		}
+
+		public Object next() {
+			if (hasNext()) {
+				return nodes.item(++currentIndex);
+			}
+
+			return null;
+		}
+	}
+	
+	/**
+	 * iterator of all the children of the element
+	 * 
+	 * @param tag
+	 * @return Iterator
+	 */
+	public static Iterator childIterator(Element tag, String childTagName) {
+		NodeList nodes = tag.getElementsByTagName(childTagName);
+		return new NodeIterator(nodes);
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/XSLTProcessor.java b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/XSLTProcessor.java
new file mode 100644
index 0000000..c46beb9
--- /dev/null
+++ b/org.eclipse.epf.web.search/src/org/eclipse/epf/web/search/utils/XSLTProcessor.java
@@ -0,0 +1,403 @@
+//------------------------------------------------------------------------------
+//Copyright (c) 2004, 2006 IBM Corporation.  All Rights Reserved.
+//------------------------------------------------------------------------------
+package org.eclipse.epf.web.search.utils;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.InputStream;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+
+/**
+* A wrapper over the XSLT processor bundled with the JRE.
+* 
+* @author Kelvin Low
+* @since 7.0
+*/
+public class XSLTProcessor {
+
+	// If true, cache the compiled the XSL transformer with the compiled XSL
+	// templates.
+	private static boolean cacheXSL = true;
+
+	private static Map cache = new HashMap();
+
+	/**
+	 * Default private constructor to prevent this class from being
+	 * instantiated.
+	 */
+	private XSLTProcessor() {
+	}
+	
+	public static Map getCache()
+	{
+		return cache;
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL source, XML source, target
+	 * output and encoding.
+	 * 
+	 * @param xslSource
+	 *            The XSL source.
+	 * @param xmlSource
+	 *            The XML source.
+	 * @param output
+	 *            The output target.
+	 * @param params
+	 *            The parameters for the XSL transformation.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(Source xslSource, Source xmlSource,
+			Writer output, Properties params, String encoding) throws Exception {
+		if (xslSource != null && xmlSource != null) {
+			Transformer transformer = null;
+			String xslSystemId = xslSource.getSystemId();
+			if (cacheXSL && xslSystemId != null) {
+				synchronized (cache) {
+					transformer = (Transformer) cache.get(xslSystemId);
+					if (transformer == null) {
+						TransformerFactory factory = TransformerFactory
+								.newInstance();
+						transformer = factory.newTransformer(xslSource);
+						cache.put(xslSystemId, transformer);
+					}
+				}
+			} else {
+				TransformerFactory factory = TransformerFactory.newInstance();
+				transformer = factory.newTransformer(xslSource);
+			}
+			if (params != null && params.size() > 0) {
+				for (Iterator i = params.keySet().iterator(); i.hasNext();) {
+					String paramName = (String) i.next();
+					String paramValue = params.getProperty(paramName);
+					transformer.setParameter(paramName, paramValue);
+				}
+			}
+			if (encoding != null && encoding.length() > 0) {
+				transformer.setOutputProperty("encoding", encoding); //$NON-NLS-1$
+			} else {
+				transformer.setOutputProperty("encoding", "utf-8"); //$NON-NLS-1$ //$NON-NLS-2$
+			}
+			transformer.transform(xmlSource, new StreamResult(output));
+		}
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL source, XML source, target
+	 * output and encoding.
+	 * 
+	 * @param xslSource
+	 *            The XSL source.
+	 * @param xmlSource
+	 *            The XML source.
+	 * @param output
+	 *            The output target.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(Source xslSource, Source xmlSource,
+			Writer output, String encoding) throws Exception {
+		transform(xslSource, xmlSource, output, null, encoding);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML source,
+	 * target output and encoding.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML source.
+	 * @param output
+	 *            The output target.
+	 * @param params
+	 *            The parameters for the XSL transformation.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(String xslUri, Source xmlSource,
+			Writer output, Properties params, String encoding) throws Exception {
+		InputStream xslInput = getXslInputStream(xslUri);
+		if (xslInput != null) {
+			StreamSource xslSource = new StreamSource(xslInput);
+			xslSource.setSystemId(new File(xslUri));
+			transform(xslSource, xmlSource, output, params, encoding);
+			try {
+				xslInput.close();
+			} catch (Exception e) {
+			}
+		}
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML source,
+	 * target output and encoding.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML source.
+	 * @param output
+	 *            The output target.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(String xslUri, Source xmlSource,
+			Writer output, String encoding) throws Exception {
+		transform(xslUri, xmlSource, output, null, encoding);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string,
+	 * target output and encoding.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param output
+	 *            The output target.
+	 * @param params
+	 *            The parameters for the XSL transformation.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(String xslUri, String xmlStr, Writer output,
+			Properties params, String encoding) throws Exception {
+		InputStream xslInput = null;
+		if (cacheXSL && xslUri != null) {
+			synchronized (cache) {
+				xslInput = (InputStream) cache.get(xslUri);
+				if (xslInput == null) {
+					xslInput = getXslInputStream(xslUri);
+					cache.put(xslUri, xslInput);
+				}
+			}
+		} else {
+			xslInput = getXslInputStream(xslUri);
+		}
+		if (xslInput != null) {
+			StreamSource xslSource = new StreamSource(xslInput);
+			xslSource.setSystemId(new File(xslUri));
+
+			byte[] xml = xmlStr.getBytes("utf-8"); //$NON-NLS-1$
+			ByteArrayInputStream xmlInput = new ByteArrayInputStream(xml);
+			StreamSource xmlSource = new StreamSource(xmlInput);
+
+			transform(xslSource, xmlSource, output, params, encoding);
+
+			try {
+				xslInput.close();			 
+			} catch (FileNotFoundException fne) {
+				fne.printStackTrace();
+			} catch (Exception e) {
+				e.printStackTrace();
+				//throw new Exception(e);
+			}
+		}
+		else
+			throw new FileNotFoundException(xslUri);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string,
+	 * target output and encoding.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param output
+	 *            The output target.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(String xslUri, String xmlStr, Writer output,
+			String encoding) throws Exception {
+		transform(xslUri, xmlStr, output, null, encoding);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string,
+	 * target output and encoding.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param file
+	 *            The output file.
+	 * @param params
+	 *            The parameters for the XSL transformation.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(String xslUri, String xmlStr, File file,
+			Properties params, String encoding) throws Exception {
+		FileWriter output = new FileWriter(file);
+		if (output != null) {
+			transform(xslUri, xmlStr, output, params, encoding);
+			try {
+				output.close();
+			} catch (Exception e) {
+			}
+		}
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string,
+	 * target output and encoding.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param file
+	 *            The output file.
+	 * @param encoding
+	 *            The target encoding.
+	 */
+	public static void transform(String xslUri, String xmlStr, File file,
+			String encoding) throws Exception {
+		transform(xslUri, xmlStr, file, null, encoding);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML source
+	 * and target output.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML source.
+	 * @param params
+	 *            The parameters for the XSL transformation.
+	 * @param output
+	 *            The output target.
+	 */
+	public static void transform(String xslUri, Source xmlSource,
+			Properties params, Writer output) throws Exception {
+		transform(xslUri, xmlSource, output, params, null);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML source
+	 * and target output.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML source.
+	 * @param output
+	 *            The output target.
+	 */
+	public static void transform(String xslUri, Source xmlSource, Writer output)
+			throws Exception {
+		transform(xslUri, xmlSource, output, null, null);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string
+	 * and target output.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param params
+	 *            The parameters for the XSL transformation.
+	 * @param output
+	 *            The output target.
+	 */
+	public static void transform(String xslUri, String xmlStr,
+			Properties params, Writer output) throws Exception {
+		transform(xslUri, xmlStr, output, params, null);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string
+	 * and target output.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param output
+	 *            The output target.
+	 */
+	public static void transform(String xslUri, String xmlStr, Writer output)
+			throws Exception {
+		transform(xslUri, xmlStr, output, null, null);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string
+	 * and target output file.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param params
+	 *            The parameters for the XSL transformation.
+	 * @param output
+	 *            The output target.
+	 */
+	public static void transform(String xslUri, String xmlStr,
+			Properties params, File file) throws Exception {
+		transform(xslUri, xmlStr, file, params, null);
+	}
+
+	/**
+	 * Executes the XSL transformation given the XSL stylesheet URI, XML string
+	 * and target output file.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 * @param xmlStr
+	 *            The XML string.
+	 * @param output
+	 *            The output target.
+	 */
+	public static void transform(String xslUri, String xmlStr, File file)
+			throws Exception {
+		transform(xslUri, xmlStr, file, null, null);
+	}
+
+	/**
+	 * Returns the XSL input stream given the XSL stylesheet URI.
+	 * 
+	 * @param xslURI
+	 *            The XSL stylesheet URI.
+	 */
+	private static InputStream getXslInputStream(String xslUri) {
+		InputStream xslInput = null;
+		try {			
+			xslInput = (xslUri.startsWith("http")) ? XSLTProcessor.class.getClassLoader()
+				.getResourceAsStream(xslUri) : new FileInputStream(UNCUtil.convertFilename(xslUri));
+		} catch (Exception e) {
+			//e.printStackTrace();
+			if (xslInput == null) {
+				xslInput = XSLTProcessor.class.getClassLoader()
+						.getResourceAsStream(xslUri);
+			}
+		}
+		return xslInput;
+	}
+}
\ No newline at end of file