//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.

package de.ugoe.cs.autoquest.plugin.php;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;

import de.ugoe.cs.autoquest.eventcore.Event;
import de.ugoe.cs.autoquest.eventcore.IEventTarget;
import de.ugoe.cs.autoquest.eventcore.IEventType;
import de.ugoe.cs.autoquest.plugin.php.eventcore.PHPEventTarget;
import de.ugoe.cs.autoquest.plugin.php.eventcore.PHPEventType;
import de.ugoe.cs.autoquest.plugin.php.eventcore.WebRequest;
import de.ugoe.cs.util.FileTools;
import de.ugoe.cs.util.console.Console;

/**
 * <p>
 * Provides functionality to parse log files with web request.
 * </p>
 * 
 * @author Steffen Herbold
 * @version 1.0
 */
public class WeblogParser {

	/**
	 * <p>
	 * Timeout between two sessions in milliseconds.
	 * </p>
	 */
	private long timeout;

	/**
	 * <p>
	 * Minimal length of a session. All shorter sessions will be pruned.<br>
	 * Default: 2
	 * </p>
	 */
	private int minLength = 2;

	/**
	 * <p>
	 * Maximal length of a session. All longer sessions will be pruned.<br>
	 * Default: 100
	 * </p>
	 */
	private int maxLength = 100;

	/**
	 * <p>
	 * URL of the server that generated the log that is currently parser; null
	 * of URL is not available.<br>
	 * Default: null
	 * </p>
	 */
	private String url = null;

	/**
	 * <p>
	 * Collection of generated sequences.
	 * </p>
	 */
	private List<List<Event>> sequences;

	/**
	 * <p>
	 * List that stores the users (identified through their cookie id) to each
	 * sequence.
	 * </p>
	 */
	private List<String> users;

	/**
	 * <p>
	 * List that stores the frequent users (identified through their cookie id)
	 * to each sequence.
	 * </p>
	 */
	private List<String> frequentUsers;

	/**
	 * <p>
	 * Sequences for all frequent users.
	 * </p>
	 */
	private List<Collection<List<Event>>> sequencesFrequentUsers;

	/**
	 * <p>
	 * Threshold that defines how many sessions of a user are require to deem
	 * the user frequent. Note, that only sessions whose lengths is in range if
	 * {@link #minLength} and {@link #maxLength} are counted.
	 * </p>
	 */
	private int frequentUsersThreshold = -1;

	/**
	 * <p>
	 * Name and path of the robot filter.
	 * </p>
	 */
	private static final String ROBOTFILTERFILE = "misc/robotfilter.txt";

	/**
	 * <p>
	 * Field that contains a regular expression that matches all robots
	 * contained in {@link #ROBOTFILTERFILE}.
	 * </p>
	 */
	private String robotRegex = null;

	/**
	 * <p>
	 * Constructor. Creates a new WeblogParser with a default timeout of
	 * 3,600,000 milliseconds (1 hour).
	 * </p>
	 */
	public WeblogParser() {
		this(3600000);
	}

	/**
	 * <p>
	 * Constructor. Creates a new WeblogParser.
	 * </p>
	 * 
	 * @param timeout
	 *            session timeout
	 */
	public WeblogParser(long timeout) {
		this.timeout = timeout;
	}

	/**
	 * <p>
	 * Returns the generated event sequences.
	 * </p>
	 * 
	 * @return generated event sequences
	 */
	public Collection<List<Event>> getSequences() {
		return sequences;
	}

	/**
	 * <p>
	 * Sets the session timeout.
	 * </p>
	 * 
	 * @param timeout
	 *            new session timeout
	 */
	public void setTimeout(long timeout) {
		this.timeout = timeout;
	}

	/**
	 * <p>
	 * Sets the minimal length of a session. All sessions that contain less
	 * events will be pruned.
	 * </p>
	 * 
	 * @param minLength
	 *            new minimal length
	 */
	public void setMinLength(int minLength) {
		this.minLength = minLength;
	}

	/**
	 * <p>
	 * Sets the maximal length of a session. All sessions that contain more
	 * events will be pruned.
	 * </p>
	 * 
	 * @param maxLength
	 *            new maximal length
	 */
	public void setMaxLength(int maxLength) {
		this.maxLength = maxLength;
	}

	/**
	 * <p>
	 * Sets the URL of the server from which this log was generated. Often
	 * required for replay generation
	 * </p>
	 * 
	 * @param url
	 *            URL of the server
	 */
	public void setUrl(String url) {
		this.url = url;
	}

	/**
	 * <p>
	 * Sets the threshold for frequent users.
	 * </p>
	 * 
	 * @param threshold
	 *            threshold value; if the value is &lt;1, the sessions of the
	 *            frequent users will not be determined
	 */
	public void setFrequentUserThreshold(int threshold) {
		this.frequentUsersThreshold = threshold;
	}

	/**
	 * <p>
	 * Returns the IDs of all frequent users.
	 * </p>
	 * 
	 * @return IDs of the frequent users
	 */
	public List<String> getFrequentUsers() {
		return frequentUsers;
	}

	/**
	 * <p>
	 * Returns the sequences of all frequent users.
	 * </p>
	 * 
	 * @return list of the sequences of all frequent users
	 */
	public List<Collection<List<Event>>> getFrequentUserSequences() {
		return sequencesFrequentUsers;
	}

	/**
	 * <p>
	 * Parses a web log file.
	 * </p>
	 * 
	 * @param filename
	 *            name and path of the log file
	 * @throws IOException
	 *             thrown if there is a problem with reading the log file
	 * @throws FileNotFoundException
	 *             thrown if the log file is not found
	 * @throws ParseException
	 *             thrown the date format is invalid
	 */
	public void parseFile(String filename) throws IOException,
			FileNotFoundException, ParseException {
		String[] lines = FileTools.getLinesFromFile(filename);

		Map<String, List<Integer>> cookieSessionMap = new HashMap<String, List<Integer>>();
		Map<String, Long> cookieTimestampMap = new HashMap<String, Long>();
		
		int lastId = -1;

		SimpleDateFormat dateFormat = new SimpleDateFormat(
				"yyyy-MM-dd HH:mm:ss");
		loadRobotRegex();

		sequences = new ArrayList<List<Event>>();
		users = new ArrayList<String>();

		int lineCounter = 0;
		for (String line : lines) {
			lineCounter++;
			String[] values = line.substring(1, line.length() - 1).split(
					"\" \"");

			// use cookie as session identifier
			int cookieStart = values[0].lastIndexOf('.');
			String cookie = values[0].substring(cookieStart + 1);
			String dateString = values[1];
			long timestamp = dateFormat.parse(dateString).getTime();
			String uriString = values[2];
			// String ref = values[3]; // referer is not yet used!
			String agent;
			if (values.length > 4) {
				agent = values[4];
			} else {
				agent = "noagent";
			}

			List<String> postedVars = new ArrayList<String>();
			if (values.length == 6) { // post vars found
				for (String postVar : values[5].trim().split(" ")) {
					if (!isBrokenVariable(postVar)) {
						postedVars.add(postVar);
					}
				}
			}
			if (!isRobot(agent)) {
				try {
					URI uri = new URI(uriString);
					String path = uri.getPath();
					List<String> getVars = extractGetVarsFromUri(uri);
					
					IEventType type = new PHPEventType(path, postedVars, getVars);
					IEventTarget target = new PHPEventTarget(path);
					Event event = new Event(type, target);
					event.addReplayable(new WebRequest(url, path, postedVars, getVars));

					// find session and add event
					List<Integer> sessionIds = cookieSessionMap.get(cookie);
					if (sessionIds == null) {
						sessionIds = new ArrayList<Integer>();
						// start new session
						sessionIds.add(++lastId);
						cookieSessionMap.put(cookie, sessionIds);
						sequences.add(new LinkedList<Event>());
						users.add(cookie);
					}
					Integer lastSessionIndex = sessionIds
							.get(sessionIds.size() - 1);
					List<Event> lastSession = sequences
							.get(lastSessionIndex);
					long lastEventTime = timestamp;
					if (!lastSession.isEmpty()) {
						lastEventTime = cookieTimestampMap.get(cookie);
					}
					if (timestamp - lastEventTime > timeout) {
						sessionIds.add(++lastId);
						List<Event> newSession = new LinkedList<Event>();
						newSession.add(event);
						sequences.add(newSession);
						users.add(cookie);
					} else {
						lastSession.add(event);
					}
					cookieTimestampMap.put(cookie, timestamp);
				} catch (URISyntaxException e) {
					Console.traceln(Level.FINE, "Ignored line " + lineCounter + ": "
							+ e.getMessage());
				}
			}
		}
		Console.traceln(Level.INFO, "" + sequences.size() + " user sequences found");
		pruneSequences();
		Console.traceln(Level.INFO, "" + sequences.size()
				+ " remaining after pruning of sequences shorter than "
				+ minLength);
		Set<String> uniqueUsers = new HashSet<String>(users);
		Console.traceln(Level.INFO, "" + uniqueUsers.size() + " unique users");
		if (frequentUsersThreshold > 0) {
			generateFrequentUserSequences(uniqueUsers);
		}
	}

	/**
	 * <p>
	 * Generates the frequent user sequences, according to the threshold
	 * {@link #frequentUsersThreshold}.
	 * </p>
	 * 
	 * @param uniqueUsers
	 *            set with all user IDs
	 */
	private void generateFrequentUserSequences(Set<String> uniqueUsers) {
		frequentUsers = new ArrayList<String>();
		sequencesFrequentUsers = new ArrayList<Collection<List<Event>>>();
		for (String user : uniqueUsers) {
			List<String> tmp = new ArrayList<String>();
			tmp.add(user);
			List<String> usersCopy = new LinkedList<String>(users);
			usersCopy.retainAll(tmp);
			int size = usersCopy.size();
			if (size >= frequentUsersThreshold) {
				frequentUsers.add(user);
				Collection<List<Event>> sequencesUser = new ArrayList<List<Event>>();
				for (int i = 0; i < sequences.size(); i++) {
					if (users.get(i).equals(user)) {
						sequencesUser.add(sequences.get(i));
					}
				}
				sequencesFrequentUsers.add(sequencesUser);

			}
		}
		Console.traceln(Level.INFO, "" + frequentUsers.size() + " users with more than "
				+ frequentUsersThreshold + " sequences");
	}

	/**
	 * <p>
	 * Prunes sequences shorter than {@link #minLength} and longer than
	 * {@link #maxLength}.
	 * </p>
	 */
	private void pruneSequences() {
		int i = 0;
		while (i < sequences.size()) {
			if ((sequences.get(i).size() < minLength)
					|| sequences.get(i).size() > maxLength) {
				sequences.remove(i);
				users.remove(i);
			} else {
				i++;
			}
		}

	}

	/**
	 * <p>
	 * Reads {@link #ROBOTFILTERFILE} and creates a regular expression that
	 * matches all the robots defined in the file. The regular expression is
	 * stored in the field {@link #robotRegex}.
	 * </p>
	 * 
	 * @throws IOException
	 *             thrown if there is a problem reading the robot filter
	 * @throws FileNotFoundException
	 *             thrown if the robot filter is not found
	 */
	private void loadRobotRegex() throws IOException, FileNotFoundException {
		String[] lines = FileTools.getLinesFromFile(ROBOTFILTERFILE);
		StringBuilder regex = new StringBuilder();
		for (int i = 0; i < lines.length; i++) {
			regex.append("(.*" + lines[i] + ".*)");
			if (i != lines.length - 1) {
				regex.append('|');
			}
		}
		robotRegex = regex.toString();
	}

	/**
	 * <p>
	 * Checks whether an agent is a robot.
	 * </p>
	 * 
	 * @param agent
	 *            agent that is checked
	 * @return true, if the agent is a robot; false otherwise
	 */
	private boolean isRobot(String agent) {
		return agent.matches(robotRegex);
	}

	/**
	 * <p>
	 * Parses the URI and extracts the GET variables that have been passed.
	 * </p>
	 * 
	 * @param uri
	 *            URI that is parsed
	 * @return a list with all GET variables
	 * @throws URISyntaxException
	 *             thrown if one of the variables seems to indicate that the
	 *             request is a malicious attack on the web application
	 */
	private List<String> extractGetVarsFromUri(URI uri)
			throws URISyntaxException {
		List<String> getVars = new ArrayList<String>();
		String query = uri.getQuery();
		if (query != null) {
			String[] paramPairs = query.split("&");
			for (String paramPair : paramPairs) {
				String[] paramSplit = paramPair.split("=");
				if (!isBrokenVariable(paramSplit[0])) {
					for (int i = 1; i < paramSplit.length; i++) {
						checkForAttack(paramSplit[i]);
					}
					getVars.add(paramSplit[0]);
				}
			}
		}
		return getVars;
	}

	/**
	 * <p>
	 * Checks if a variable is broken.Currently, the check rather imprecise and
	 * checks only if the term &quot;and&quot; is part of the variable name.
	 * </p>
	 * 
	 * @param var
	 *            variable that is checked
	 * @return true if the variable is broken, false otherwise
	 */
	private boolean isBrokenVariable(String var) {
		return var.contains("and");
	}

	/**
	 * <p>
	 * Checks if the variable name send with a request seems like an attack on the server.
	 * </p>
	 * @param value
	 * @throws URISyntaxException
	 */
	private void checkForAttack(String value) throws URISyntaxException {
		if (value.contains("UNION+") || value.contains("SELECT+")) {
			throw new URISyntaxException(value, "possible injection attack");
		}
	}
}
