/* $This file is distributed under the terms of the license in LICENSE$ */

package edu.cornell.mannlib.vitro.webapp.config;

import static edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean.DUMMY_BEAN;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import edu.cornell.mannlib.vitro.webapp.config.RevisionInfoBean.LevelRevisionInfo;
import edu.cornell.mannlib.vitro.webapp.startup.StartupStatus;

/**
 * <pre>
 * Read the revision information, and store it in the servlet context.
 *
 * - The revision information is in a file in the classpath.
 * - The name of the file is in RESOURCE_PATH, below.
 * - The first line is the build date, with a format as in DATE_FORMAT, below.
 * - Each additional non-blank line holds revision info for one application level:
 *    - level info is from inner (vitro) to outer (top-level product).
 *    - level info appears as product name, release name and revision level,
 *        delimited by " ~ ".
 *    - additional white space before and after info values is ignored.
 *
 * Example file:
 *    2010-11-14 23:58:00
 *    vitroCore ~ Release 1.1 ~ 6604
 *    nihvivo ~ Release 1.1 ~ 1116
 * </pre>
 */
public class RevisionInfoSetup implements ServletContextListener {
	private static final Log log = LogFactory.getLog(RevisionInfoSetup.class);

	private static final Pattern LEVEL_INFO_PATTERN = Pattern
			.compile("(.+) ~ (.+) ~ (.+)");

	static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
	static final String RESOURCE_PATH = "/WEB-INF/resources/revisionInfo.txt";

	/**
	 * On startup, read the revision info from the resource file in the
	 * classpath.
	 *
	 * If we can't find the file, or can't parse it, store an empty bean.
	 *
	 * Don't allow any Exceptions to percolate up past this point.
	 */
	@Override
	public void contextInitialized(ServletContextEvent sce) {
		ServletContext context = sce.getServletContext();
		RevisionInfoBean bean;
		try {
			List<String> lines = readRevisionInfo(context);
			bean = parseRevisionInformation(lines);
		} catch (Exception e) {
			StartupStatus.getBean(context).warning(this,
					"Failed to load the revision info", e);
			bean = DUMMY_BEAN;
		}

		RevisionInfoBean.setBean(sce.getServletContext(), bean);
	}

	private List<String> readRevisionInfo(ServletContext context)
			throws IOException {
		BufferedReader reader = null;
		try {
			reader = openRevisionInfoReader(context);
			return readSignificantLines(reader);
		} finally {
			closeReader(reader);
		}
	}

	private BufferedReader openRevisionInfoReader(ServletContext context)
			throws FileNotFoundException {
		InputStream stream = context.getResourceAsStream(RESOURCE_PATH);
		if (stream == null) {
			throw new FileNotFoundException(
					"Can't find a resource in the webapp at '" + RESOURCE_PATH
							+ "'.");
		} else {
			return new BufferedReader(new InputStreamReader(stream));
		}
	}

	private List<String> readSignificantLines(BufferedReader reader)
			throws IOException {
		List<String> lines = new ArrayList<String>();

		String line = null;
		while (null != (line = reader.readLine())) {
			line = line.trim();
			if ((!line.isEmpty()) && (!line.startsWith("#"))) {
				lines.add(line);
			}
		}

		return lines;
	}

	private void closeReader(Reader reader) {
		if (reader != null) {
			try {
				reader.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	private RevisionInfoBean parseRevisionInformation(List<String> lines)
			throws ParseException {
		checkValidNumberOfLines(lines);
		String dateLine = lines.get(0);
		List<String> levelLines = lines.subList(1, lines.size());

		Date buildDate = parseDateLine(dateLine);
		List<LevelRevisionInfo> levelInfos = parseLevelLines(levelLines);
		return new RevisionInfoBean(buildDate, levelInfos);
	}

	private void checkValidNumberOfLines(List<String> lines)
			throws ParseException {
		if (lines.isEmpty()) {
			throw new ParseException(
					"The revision info resource file contains no data.", 0);
		}
	}

	private Date parseDateLine(String dateLine) throws ParseException {
		return new SimpleDateFormat(DATE_FORMAT).parse(dateLine);
	}

	private List<LevelRevisionInfo> parseLevelLines(List<String> levelLines)
			throws ParseException {
		List<LevelRevisionInfo> infos = new ArrayList<LevelRevisionInfo>();
		for (String line : levelLines) {
			Matcher m = LEVEL_INFO_PATTERN.matcher(line);
			if (m.matches()) {
				String name = m.group(1).trim();
				String release = m.group(2).trim();
				String revision = m.group(3).trim();
				infos.add(new LevelRevisionInfo(name, release, revision));
			} else {
				throw new ParseException(
						"Failed to parse the revision info in '" + line + "'",
						0);
			}
		}
		return infos;
	}

	/** On shutdown, clean up. */
	@Override
	public void contextDestroyed(ServletContextEvent sce) {
		RevisionInfoBean.removeBean(sce.getServletContext());
	}

}
