package com.zandero.utils;

import java.io.UnsupportedEncodingException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public final class StringUtils {

	private StringUtils() {
		// hiding constructor
	}

	/**
	 * Checks if string is null or empty == ""
	 *
	 * @param value string to test
	 * @return true if null or empty, false otherwise.
	 */
	public static boolean isNullOrEmpty(String value) {

		return (value == null || value.length() == 0);
	}

	/**
	 * Checks if string is null, empty or contains only spaces
	 *
	 * @param value string to test
	 * @return true if null, empty or spaces only, false otherwise.
	 */
	public static boolean isNullOrEmptyTrimmed(String value) {

		return (value == null || value.trim().length() == 0);
	}

	/**
	 * Checks if non null string is empty
	 *
	 * @param value string to test
	 * @return true if empty, false if null or not empty.
	 */
	public static boolean isEmpty(String value) {
		return (value != null && value.length() == 0);
	}

	/**
	 * Checks if non null string is empty when trimmed
	 *
	 * @param value string to test
	 * @return true if empty when trimmed, false if null or not empty.
	 */
	public static boolean isEmptyTrimmed(String value) {
		return (value != null && value.trim().length() == 0);
	}

	/**
	 * compares two strings if equal ({@code String.equals} method)
	 *
	 * @param original string to compare against
	 * @param compare  string to compare to
	 * @return true if equal or both null, false otherwise
	 */
	public static boolean equals(String original, String compare) {

		return equals(original, compare, false);
	}

	/**
	 * compares two strings if equal ({@code String.equals} or
	 * {@code String.equalsIgnoreCase} method)
	 *
	 * @param original   string to compare against
	 * @param compare    string to compare to
	 * @param ignoreCase true equalsIgnoreCase is used, false equals is used
	 * @return true if equal or both null, false otherwise
	 */
	public static boolean equals(String original, String compare, boolean ignoreCase) {

		if (original == null && compare == null) {
			return true;
		}

		if (original != null) {
			if (ignoreCase) {
				return original.equalsIgnoreCase(compare);
			}
			else {
				return original.equals(compare);
			}
		}

		return false;
	}

	/**
	 * Compares two strings with String.compare(). Method allows
	 * null values.
	 *
	 * @param one to compare
	 * @param two to compare
	 * @return 0 if both null, -1 if one is null, 1 if two is null, otherwise String.compare() is
	 * called
	 */
	public static int compare(String one, String two) {

		if (one == null && two == null) {
			return 0;
		}

		if (one == null) {
			return -1;
		}

		if (two == null) {
			return 1;
		}

		return one.compareTo(two);
	}

	/**
	 * null resilient trim
	 *
	 * @param value trims value or returns null if null
	 * @return null or trimmed value (empty string is left empty)
	 */
	public static String trim(String value) {

		return value != null ? value.trim() : null;
	}

	/**
	 * Additionally to outer trim() double spaces are removed.
	 *
	 * @param value to remove double spaces from
	 * @return value with single spaces if any
	 */
	public static String trimDoubleSpaces(String value) {

		if (isNullOrEmpty(value)) {
			return value;
		}

		value = value.trim();
		return value.replaceAll("\\s+", " ");
	}

	/**
	 * Additionally to outer trim() all inner spaces are removed.
	 *
	 * @param value to remove inner spaces from
	 * @return value with single spaces if any
	 */
	public static String trimInner(String value) {

		if (isNullOrEmpty(value)) {
			return value;
		}

		value = value.trim();
		return value.replaceAll("\\s+", "");
	}

	/**
	 * Trims only end of text
	 *
	 * @param text to be trimmed at the end
	 * @return trimmed text or original if no changes were applied
	 */
	public static String trimEnd(String text) {

		if (isNullOrEmpty(text)) {
			return text;
		}

		return text.replaceAll("\\s+$", "");
	}

	public static String trimStart(String text) {

		if (isNullOrEmpty(text)) {
			return text;
		}

		return text.replaceAll("^\\s+", "");
	}

	public static String trimAll(String text, String toBeTrimmed) {

		if (isNullOrEmpty(text) || isNullOrEmpty(toBeTrimmed)) {
			return text;
		}

		return text.replaceAll(toBeTrimmed, "");
	}

	/**
	 * Trims down text to null if empty
	 *
	 * @param text to be trimmed
	 * @return text or null if empty
	 */
	public static String trimToNull(String text) {

		text = trim(text);

		if (text == null || text.isEmpty()) {
			return null;
		}

		return text;
	}

	/**
	 * Capitalizes first character in given string
	 *
	 * @param input to capitalize first character
	 * @return capitalized string or null if null
	 */
	public static String capitalize(String input) {

		if (input == null) {
			return null;
		}

		if (input.length() > 1) {
			for (int i = 0; i < input.length(); i++) {
				if (Character.isAlphabetic(input.charAt(i))) {
					return input.substring(0, i) + Character.toString(input.charAt(i)).toUpperCase() + input.substring(i + 1);
				}
			}
		}

		return input.toUpperCase();
	}

	/**
	 * Joins list of string items to a single string, where items are separated
	 * with a defined separator.
	 *
	 * @param list      to join into string
	 * @param separator to be used between elements
	 * @return items joined into a single string
	 */
	public static String join(List<?> list, String separator) {

		return join(list, separator, null);
	}

	/**
	 * Joins list of string items to a single string, where items are separated
	 * with a defined separator.
	 *
	 * @param list      to join into string
	 * @param separator to be used between elements
	 * @param limit     max items to consider when joining, null or 0 to join all
	 * @return items joined into a single string
	 */
	public static String join(List<?> list, String separator, Integer limit) {

		if (separator == null || separator.equalsIgnoreCase("")) {
			throw new IllegalArgumentException("Missing separator!");
		}

		StringBuilder output = new StringBuilder();
		if (limit == null || limit <= 0) {
			limit = list.size();
		}

		if (list != null && limit > 0) {
			for (int i = 1; i <= limit; i++) {

				if (i > list.size()) {
					break;
				}

				if (i <= limit) {
					output.append(list.get(i - 1));

					if (i == limit && list.size() > limit) {
						// ok list is larger than limit .. and we hit the limit
						output.append(separator)
						      .append("... [")
						      .append(limit)
						      .append("/")
						      .append(list.size())
						      .append("]");
					}

					if (i < limit) {
						output.append(separator);
					}
				}
			}
		}

		return output.toString();
	}

	/**
	 * Joins list of string items to a single string, where items are separated
	 * with a defined separator.
	 *
	 * @param args      to join into string
	 * @param separator to be used between elements
	 * @param limit     max items to consider when joining, null or 0 to join all
	 * @return items joined into a single string
	 */
	public static String join(Object[] args, String separator, Integer limit) {

		if (separator == null || separator.equalsIgnoreCase("")) {
			throw new IllegalArgumentException("Missing separator!");
		}

		if (args != null && args.length > 0) {
			List<Object> array = Arrays.asList(args);
			return join(array, separator, limit);
		}

		return "";
	}

	/**
	 * Joins array of objects into string, where items are separated
	 * with a defined separator.
	 *
	 * @param args      array of Object
	 * @param separator to be used between elements
	 * @return items joined into a single string
	 */
	public static String join(Object[] args, String separator) {

		return join(args, separator, null);
	}

	/**
	 * Joins list of string items to a single string, where items are separated
	 * with a defined separator.
	 *
	 * @param items     to join into string
	 * @param separator to be used between elements
	 * @param limit     max items to consider when joining, null or 0 to join all
	 * @return items joined into a single string
	 */
	public static String join(Set<?> items, String separator, Integer limit) {

		if (separator == null || separator.equalsIgnoreCase("")) {
			throw new IllegalArgumentException("Missing separator!");
		}

		if (items != null && items.size() > 0) {
			Object[] array = items.toArray();
			return join(array, separator, limit);
		}

		return "";
	}

	/**
	 * Joins set of items to a single string, where items are separated
	 * with a defined separator.
	 *
	 * @param items     to join into string
	 * @param separator to be used between elements
	 * @return items joined into a single string
	 */
	public static String join(Set<?> items, String separator) {

		return join(items, separator, null);
	}

	/**
	 * Joins list of string items to a single string, where items are separated
	 * with a defined separator.
	 *
	 * @param map       to join into key=value list
	 * @param separator to be used between elements
	 * @param <T> key class type
	 * @param <V> value class type
	 * @return map key values joined into a single string
	 */
	public static <T, V> String join(Map<T, V> map, String separator) {

		if (separator == null || separator.equalsIgnoreCase("")) {
			throw new IllegalArgumentException("Missing separator!");
		}

		StringBuilder output = new StringBuilder();
		if (map != null) {
			for (Object key : map.keySet()) {
				if (output.length() > 0) {
					output.append(separator);
				}

				output.append(key).append("=").append(map.get(key));
			}
		}

		return output.toString();
	}

	/**
	 * Extracts words from text removing non alpha characters
	 *
	 * @param text to extract words from
	 * @return list of found words or empty list if none found
	 */
	public static List<String> getWords(String text) {

		List<String> output = new ArrayList<>();
		if (isNullOrEmptyTrimmed(text)) {
			return output;
		}

		Pattern p = Pattern.compile("\\b\\p{L}+\\b");

		Matcher m = p.matcher(text);
		while (m.find()) {
			output.add(m.group());
		}

		return output;
	}

	/**
	 * Removes punctuation from text
	 *
	 * @param text to remove punctuation from
	 * @return cleaned up text or original if nothing to remove
	 */
	public static String removePunctuation(String text) {

		if (text == null) {
			return null;
		}

		return text.replaceAll("\\p{P}", "");
	}

	/**
	 * Wrapper around String.split(regEx) method
	 *
	 * @param text  to split
	 * @param regEx regular expression to use for splitting
	 * @return list of strings or empty list if could not split
	 */
	public static List<String> split(String text, String regEx) {

		if (isNullOrEmptyTrimmed(text)) {
			return Collections.emptyList();
		}

		String[] items = text.trim().split(regEx);
		return Arrays.asList(items);
	}

	/**
	 * Reduces text to max given size preserving words
	 *
	 * @param text to trim down
	 * @param size max desired size
	 * @return trimmed down text with "..." or original text if fitting size
	 */
	public static String trimTextDown(String text, int size) {

		if (text == null || text.length() <= size) {
			return text;
		}

		int pos = text.lastIndexOf(" ", size);

		if (pos < 0) {
			return text.substring(0, size);
		}

		return text.substring(0, pos);
	}

	/**
	 * Reduces text size to a given size
	 *
	 * @param text            to reduce
	 * @param sizeMinusAppend size final string should be max long
	 * @param append          string to append at the end to indicate trimming
	 * @return trimmed text with appended text of given max size, or original text if shorter than desired
	 */
	public static String trimTextDown(String text, int sizeMinusAppend, String append) {

		Assert.notNull(append, "Missing append!");

		if (text == null || text.length() <= sizeMinusAppend) {
			return text;
		}

		sizeMinusAppend = sizeMinusAppend - append.length();

		int pos = text.lastIndexOf(" ", sizeMinusAppend);

		if (pos < 0) {
			return text.substring(0, sizeMinusAppend) + append;
		}

		return text.substring(0, pos) + append;
	}

	/**
	 * NPE safe toString call
	 *
	 * @param value object to call toString upon
	 * @return toString value or null if object is null
	 */
	public static String toStringOrNull(Object value) {

		return null != value ? value.toString() : null;
	}

	/**
	 * Converts text to list of characters
	 *
	 * @param text to be converted
	 * @return list of characters
	 */
	public static List<String> asListOfChars(String text) {

		List<String> list = new ArrayList<>();

		if (StringUtils.isNullOrEmptyTrimmed(text)) {
			return list;
		}

		for (int i = 0; i < text.length(); i++) {
			list.add(Character.toString(text.charAt(i)));
		}

		return list;
	}

	/**
	 * Checks if given string is a single word (doesn't accepts words with "-" as a single word!)
	 *
	 * @param word to test
	 * @return true if word, false otherwise
	 */
	public static boolean isWord(String word) {

		if (isNullOrEmptyTrimmed(word)) {
			return false;
		}

		List<String> list = getWords(word);
		return list.size() == 1;
	}

	/**
	 * Calculates matching relevance between given string and search expression
	 *
	 * @param value  to search in
	 * @param search to search for
	 * @return -1 not relevant, 0..N - where lower values represents more relevant results
	 */
	public static int relevance(String value, String search) {

		if (StringUtils.isNullOrEmptyTrimmed(value) ||
			StringUtils.isNullOrEmptyTrimmed(search)) {
			return -1;
		}

		if (search.length() > value.length()) {
			return -1;
		}

		int relevance = -1; // start at -1 ... so -1 is returned for no result
		int delta = 1; // first delta at 1 ... producing a sum of 0 when first letter is found

		int len = value.length();
		int searchLen = search.length();
		int letterIndex = 0;

		for (int searchIndex = 0; searchIndex < searchLen; searchIndex++) {

			char match = search.charAt(searchIndex);

			while (letterIndex < len) {
				char letter = value.charAt(letterIndex);
				letterIndex++;

				if (match == letter) {
					relevance = relevance + delta; // reverse to get higher value for better match
					delta = 0;
					break;
				}
				else {
					delta++;
				}
			}

			// we matched all characters ... and found the last one ...
			if (delta == 0 && searchIndex == searchLen - 1) {
				return relevance;
			}

			if (letterIndex == len) {
				break;
			}
		}

		return -1;
	}

	/**
	 * Removes all multiple-consecutive whitespace characters (space, tab, newline) and replaces them with single space.
	 * Also removes leading and trailing spaces.
	 *
	 * @param input to be sanitized
	 * @return sanitized output
	 *
	 * @throws IllegalArgumentException in case input is null or empty
	 */
	public static String sanitizeWhitespace(String input) {

		Assert.notNull(input, "Missing input!");
		input = input.replaceAll("\\s+", " ");
		return input.trim();
	}

	/**
	 * Removes all multiple-consecutive whitespace characters (space, tab, newline) and replaces them with single space.
	 * Also removes leading and trailing spaces.
	 *
	 * @param input text to be sanitized as byte array
	 * @return sanitized byte array
	 */
	public static byte[] sanitizeWhitespace(byte[] input) {

		Assert.isFalse(input == null || input.length == 0, "Missing input!");

		try {
			String stringFileData = new String(input, "utf-8");
			return sanitizeWhitespace(stringFileData).getBytes("utf-8");
		}
		catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Simple enumeration: first, second, third ... 5th, 6th .. etc for given number
	 *
	 * @param number to be enumerated (must be grated than 0)
	 * @return enumeration or null if not applicable
	 */
	public static String enumerate(Integer number) {

		if (number == null || number <= 0) {
			return null;
		}

		int lastDigit = number % 10;

		switch (number) { // special case for first five
			case 1:
				return "first";
			case 2:
				return "second";
			case 3:
				return "third";
			case 4:
				return "fourth";
			case 5:
				return "fifth";

			default:
				switch (lastDigit) {
					case 1:
						return number + "st";
					case 2:
						return number + "nd";
					case 3:
						return number + "rd";

					default:
						return number + "th";
				}
		}
	}

	/**
	 * Removes double or single quotes if any are present
	 *
	 * @param text to remove double quotes from
	 * @return un quoted string
	 */
	public static String unQuote(String text) {

		if (isNullOrEmptyTrimmed(text)) {
			return text;
		}

		if ((text.startsWith("\"") && text.endsWith("\"")) ||
		    (text.startsWith("'") && text.endsWith("'"))) {
			return text.substring(1, text.length() - 1);
		}

		return text;
	}
}


package com.zandero.utils;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
  * Utilities to help using date and time
 * @author drejc (Andrej Završnik)
 */
@Deprecated
public final class DateTimeUtils {

	/**
	 * On second in milliseconds
	 */
	public static final long ONE_SECOND = 1000L;

	/**
	 * Ten seconds in milliseconds
	 */
	public static final long TEN_SECONDS = 10_000L;

	/**
	 * On minute in milliseconds
	 */
	public final static long ONE_MINUTE = 60L * ONE_SECOND;

	/**
	 * On hour in milliseconds
	 */
	public final static long ONE_HOUR = 60L * ONE_MINUTE;

	/**
	 * On day in milliseconds
	 */
	public final static long ONE_DAY = 24L * ONE_HOUR;

	private DateTimeUtils() {
		// hiding constructor
	}

	// TODO: Migrate to: DateTimeFormatter
	private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
		@Override
		protected SimpleDateFormat initialValue() {

			return new SimpleDateFormat("yyyy-MM-dd");
		}
	};

	private static ThreadLocal<SimpleDateFormat> simpleTimeFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
		@Override
		protected SimpleDateFormat initialValue() {

			return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
		}
	};

	private static ThreadLocal<SimpleDateFormat> simpleDateTimeFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
		@Override
		protected SimpleDateFormat initialValue() {

			return new SimpleDateFormat("yyyy-MM-dd HH:mm");
		}
	};

	/**
	 * Prepared date format for yyyy-MM-dd for date only
	 *
	 * @return Simple date format for yyyy-MM-dd
	 */
	public static SimpleDateFormat getDateFormat() {

		return simpleDateFormatThreadLocal.get();
	}

	/**
	 * Prepare date time format (without milliseconds and timezone)
	 *
	 * @return Simple date format for yyy-MM-dd HH:mm
	 */
	public static SimpleDateFormat getDateTimeFormat() {

		return simpleDateTimeFormatThreadLocal.get();
	}

	/**
	 * Prepared full date format
	 * @return Simple date format for yyyy-MM-dd HH:mm:ss Z
	 */
	public static SimpleDateFormat getTimeFormat() {

		return simpleTimeFormatThreadLocal.get();
	}

	/**
	 * @return New UTC calendar instance
	 */
	public static Calendar getCalendar() {

		return Calendar.getInstance(TimeZone.getTimeZone("UTC"));
	}

	/**
	 * @param time to be set (UTC)
	 * @return New UTC calendar with time set
	 */
	public static Calendar getCalendar(long time) {

		Calendar calendar = getCalendar();
		calendar.setTimeInMillis(time);
		return calendar;
	}

	/**
	 * Formats time to String
	 * @param time to be formatted (UTC)
	 * @return date time formatted to yyyy-MM-dd HH:mm:ss Z
	 */
	public static String formatDateTime(long time) {

		SimpleDateFormat format = getTimeFormat();
		Calendar calendar = getCalendar(time);

		return format.format(calendar.getTime());
	}

	/**
	 * Formats time to String
	 * @param time to be formatted (UTC)
	 * @return date time formatted to yyyy-MM-dd HH:mm
	 */
	public static String formatDateTimeShort(long time) {

		SimpleDateFormat format = getDateTimeFormat();
		Calendar calendar = getCalendar(time);

		return format.format(calendar.getTime());
	}

	/**
	 * Formats time to date only
	 * @param time to be formatted (UTC)
	 * @return date formatted yyyy-MM-dd
	 */
	public static String formatDate(long time) {

		SimpleDateFormat format = getDateFormat();
		Calendar calendar = getCalendar(time);

		return format.format(calendar.getTime());
	}

	/**
	 * Custom date time format
	 * @param time to be formatted
	 * @param format simple date time format, or null for default yyyy-MM-dd HH:mm:ss Z format
	 * @return formated date time
	 */
	public static String format(long time, SimpleDateFormat format) {

		if (format == null) {
			return formatDateTime(time);
		}

		Calendar calendar = getCalendar(time);
		return format.format(calendar.getTime());
	}

	/**
	 * Returns time for given time zone
	 * @param time current time
	 * @param timezone time zone (-12 / +12)
	 * @return time in other time zone
	 */
	public static long getTimezoneTime(long time, int timezone) {

		Calendar calendar = getCalendar(time);
		int hour = calendar.get(Calendar.HOUR_OF_DAY);

		hour = (hour + timezone) % 24;
		if (hour < 0) {
			hour = 24 + hour;
			calendar.add(Calendar.DAY_OF_MONTH, -1);
		}

		calendar.set(Calendar.HOUR_OF_DAY, hour);
		return calendar.getTimeInMillis();
	}

	/**
	 * Converts local hour back to UTC hour
	 *
	 * @param hour     local hour in time zone
	 * @param timezone current time zone
	 * @return UTC hour
	 */
	public static int getUtcHour(int hour, int timezone) {

		hour = (hour - timezone) % 24;
		return (hour < 0) ? 24 + hour : hour;
	}

	/**
	 * Converts UTC hour to time zone hour
	 *
	 * @param hour     UTC hour
	 * @param timezone time zone
	 * @return hour as seen in the given time zone
	 */
	public static int getTimezoneHour(int hour, int timezone) {

		hour = (hour + timezone) % 24;
		return (hour < 0) ? 24 + hour : hour;
	}

	/**
	 * Converts timestamp into OffsetDatetime
	 *
	 * @param timestamp to be converted
	 * @return OffsetDatetime representation of timestamp
	 */
	public static OffsetDateTime toOffsetDateTime(long timestamp) {

		Calendar calendar = DateTimeUtils.getCalendar(timestamp);
		return OffsetDateTime.ofInstant(calendar.toInstant(), calendar.getTimeZone().toZoneId());
	}

	/**
	 * Converts offset date time to long timestamp
	 * @param timestamp to be converted
	 * @return time stamp in milliseconds
	 */
	public static long fromOffsetDateTime(OffsetDateTime timestamp) {

		Assert.notNull(timestamp, "Missing offset time stamp!");
		return timestamp.toInstant().toEpochMilli();
	}

	/**
	 * Gets first millisecond of first day in month
	 *
	 * @param time to get first millisecond
	 * @return first millisecond of month for given time
	 */
	public static long getMonthStart(long time) {

		LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC);
		dateTime = dateTime.withDayOfMonth(1).withHour(0).withMinute(0).withSecond(0).with(ChronoField.MILLI_OF_SECOND, 0);
		return dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
	}

	/**
	 * Returns last millisecond of last day in month ... +1 = next first day in month
	 *
	 * @param time to get last second in month
	 * @return last millisecond of month for given time
	 */
	public static long getMonthEnd(long time) {

		LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC);
		dateTime = dateTime.withDayOfMonth(1).withHour(23).withMinute(59).withSecond(59).with(ChronoField.MILLI_OF_SECOND, 999);
		dateTime = dateTime.plus(1, ChronoUnit.MONTHS).minus(1, ChronoUnit.DAYS);
		return dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
	}

	/**
	 * Returns day in month, where first day of month == 1
	 *
	 * @param time to get day of month
	 * @return 1..31
	 */
	public static int getDayInMonth(long time) {

		return DateTimeUtils.getCalendar(time).get(Calendar.DAY_OF_MONTH);
	}

	/**
	 * Gets first millisecond of first day in week
	 *
	 * @param time to get first millisecond (this week)
	 * @return first millisecond of week for given time
	 */
	public static long getWeekStart(long time) {

		LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC);
		dateTime = dateTime.with(ChronoField.DAY_OF_WEEK, 1).withHour(0).withMinute(0).withSecond(0).with(ChronoField.MILLI_OF_SECOND, 0);
		return dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
	}

	/**
	 * Returns last millisecond of last day in week
	 *
	 * @param time to get last second (this week)
	 * @return last millisecond of week for given time
	 */
	public static long getWeekEnd(long time) {

		LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC);
		dateTime = dateTime.with(ChronoField.DAY_OF_WEEK, 7).withHour(23).withMinute(59).withSecond(59).with(ChronoField.MILLI_OF_SECOND, 999);
		return dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
	}

	/**
	 * Returns number of day in week, where monday is the first day of the week (1)
	 *
	 * @param time to check day in week
	 * @return 1 - monday, 2 - tuesday ... 7 - saturday
	 */
	public static int getDayInWeek(long time) {

		int dayInWeek = getCalendar(time).get(Calendar.DAY_OF_WEEK) - 1;

		// SUNDAY == 1 - 1 == 0
		if (dayInWeek == 0) {
			dayInWeek = 7; // make sunday the last day of the week
		}

		return dayInWeek;
	}

	/**
	 * @param time to check if it is a weekend day
	 * @return true if Saturday or Sunday, otherwise false
	 */
	public static boolean isWeekend(long time) {

		return getDayInWeek(time) >= 6;
	}

	/**
	 * Removes time component from date time
	 *
	 * @param time to remove hours, minutes ... from
	 * @return 0 millisecond in day
	 */
	public static long getDateOnly(long time) {

		LocalDateTime dateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(time), ZoneOffset.UTC);
		dateTime = dateTime.withHour(0).withMinute(0).withSecond(0).with(ChronoField.MILLI_OF_SECOND, 0);
		return dateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
	}

	/**
	 * Convert any given timestamp string to long
	 * Note that in case conversion fails to produce a result a 0 timestamp is returned!
	 *
	 * @param value   to be converted
	 * @param formats list of formats to try out
	 * @return time stamp or 0 if conversion failed
	 */
	public static long getTimestamp(String value, SimpleDateFormat[] formats) {

		Assert.notNull(formats, "Missing date time formats");
		Assert.isTrue(formats.length > 0, "Missing date time formats");

		if (StringUtils.isNullOrEmptyTrimmed(value)) {
			return 0;
		}

		// OK check if we have 6 digit milliseconds ... if so truncate
		if (value.matches(".*\\.\\d{6}.*")) {
			// remove 3 milliseconds places
			int index = value.indexOf(".");
			value = value.substring(0, index + 4) + value.substring(index + 7);
		}

		// Go over all formats ... and if success return value
		for (SimpleDateFormat format : formats) {

			TimeZone oldDefaultTimeZone = TimeZone.getDefault();

			try {
				// make sure we are in the correct time zone while doing this
				TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

				Date date = format.parse(value);
				return date.getTime();
			}
			catch (ParseException e) {
				// try next one ...
			}
			finally {
				TimeZone.setDefault(oldDefaultTimeZone);
			}
		}

		return 0;
	}

	/**
	 * Check is time periods overlap
	 * @param startOne start time of first period / null for full
	 * @param endOne end tme of first period / null for full
	 * @param startTwo start time of second period / null for full
	 * @param endTwo end time of second period / null for full
	 * @return true if periods overlap, false if they don't
	 * @throws IllegalArgumentException in case start time is after end time of first/second period
	 */
	public static boolean overlaps(Instant startOne, Instant endOne, Instant startTwo, Instant endTwo) {

		return InstantTimeUtils.overlaps(startOne, endOne, startTwo, endTwo);
	}
}


/*
  Copyright 2011-2014 Red Hat, Inc

  This file is part of PressGang CCMS.

  PressGang CCMS is free software: you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  PressGang CCMS is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with PressGang CCMS.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.jboss.pressgang.ccms.model;

import static javax.persistence.GenerationType.IDENTITY;

import javax.persistence.Cacheable;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.hibernate.annotations.BatchSize;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.envers.Audited;
import org.hibernate.validator.constraints.NotBlank;
import org.jboss.pressgang.ccms.model.base.AuditedEntity;
import org.jboss.pressgang.ccms.model.constants.Constants;
import org.jboss.pressgang.ccms.utils.constants.CommonFilterConstants;

/**
 * Filter generated by hbm2java
 */
@Entity
@Audited
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
@Table(name = "Filter", uniqueConstraints = @UniqueConstraint(columnNames = {"FilterName"}))
public class Filter extends AuditedEntity implements java.io.Serializable {
    public static final String SELECT_ALL_QUERY = "select filter from Filter filter";
    /**
     * Serializable version identifier
     */
    private static final long serialVersionUID = 1378015715100292871L;

    private Integer filterId;
    private String filterName;
    private String filterDescription;
    private Integer filterClassType;
    private Set<FilterTag> filterTags = new HashSet<FilterTag>(0);
    private Set<FilterCategory> filterCategories = new HashSet<FilterCategory>(0);
    private Set<FilterField> filterFields = new HashSet<FilterField>(0);
    private Set<FilterOption> filterOptions = new HashSet<FilterOption>(0);
    private Set<FilterLocale> filterLocales = new HashSet<FilterLocale>(0);

    public Filter() {
    }

    public Filter(final String filterName) {
        this.filterName = filterName;
    }

    public Filter(final String filterName, final String filterDescription, final Set<FilterTag> filterTags,
            final Set<FilterCategory> filterCategories) {
        this.filterName = filterName;
        this.filterDescription = filterDescription;
        this.filterTags = filterTags;
        this.filterCategories = filterCategories;
    }

    @Id
    @GeneratedValue(strategy = IDENTITY)
    @Column(name = "FilterID", unique = true, nullable = false)
    public Integer getFilterId() {
        return filterId;
    }

    public void setFilterId(Integer filterId) {
        this.filterId = filterId;
    }

    @Column(name = "FilterName", nullable = false, length = 255)
    @NotNull(message = "{filter.name.notBlank}")
    @NotBlank(message = "{filter.name.notBlank}")
    @Size(max = 255)
    public String getFilterName() {
        return filterName;
    }

    public void setFilterName(final String filterName) {
        this.filterName = filterName;
    }

    @Column(name = "FilterDescription", columnDefinition = "TEXT")
    @Size(max = 65535)
    public String getFilterDescription() {
        return filterDescription;
    }

    public void setFilterDescription(final String filterDescription) {
        this.filterDescription = filterDescription;
    }

    @Column(name = "FilterClassType", columnDefinition = "TINYINT")
    @NotNull
    public Integer getFilterClassType() {
        return filterClassType;
    }

    public void setFilterClassType(Integer filterClassType) {
        this.filterClassType = filterClassType;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "filter", orphanRemoval = true, cascade = CascadeType.ALL)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<FilterTag> getFilterTags() {
        return filterTags;
    }

    public void setFilterTags(final Set<FilterTag> filterTags) {
        this.filterTags = filterTags;
    }

    @Transient
    public List<FilterTag> getFilterTagsList() {
        final List<FilterTag> retValue = new ArrayList<FilterTag>();
        for (final FilterTag filterTag : filterTags) {
            retValue.add(filterTag);
        }

        return retValue;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "filter", orphanRemoval = true, cascade = CascadeType.ALL)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<FilterCategory> getFilterCategories() {
        return filterCategories;
    }

    public void setFilterCategories(final Set<FilterCategory> filterCategories) {
        this.filterCategories = filterCategories;
    }

    @Transient
    public List<FilterCategory> getFilterCategoriesList() {
        final List<FilterCategory> retValue = new ArrayList<FilterCategory>();
        for (final FilterCategory filterCategory : filterCategories) {
            retValue.add(filterCategory);
        }

        return retValue;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "filter", orphanRemoval = true, cascade = CascadeType.ALL)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<FilterField> getFilterFields() {
        return filterFields;
    }

    public void setFilterFields(final Set<FilterField> filterFields) {
        this.filterFields = filterFields;
    }

    @Transient
    public List<FilterField> getFilterFieldsList() {
        final List<FilterField> retValue = new ArrayList<FilterField>();
        for (final FilterField filterField : filterFields) {
            retValue.add(filterField);
        }

        return retValue;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "filter", orphanRemoval = true, cascade = CascadeType.ALL)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<FilterOption> getFilterOptions() {
        return filterOptions;
    }

    public void setFilterOptions(final Set<FilterOption> filterOptions) {
        this.filterOptions = filterOptions;
    }

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "filter", orphanRemoval = true, cascade = CascadeType.ALL)
    @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
    @BatchSize(size = Constants.DEFAULT_BATCH_SIZE)
    public Set<FilterLocale> getFilterLocales() {
        return filterLocales;
    }

    public void setFilterLocales(final Set<FilterLocale> filterLocales) {
        this.filterLocales = filterLocales;
    }

    @Transient
    public List<FilterLocale> getFilterLocalesList() {
        final List<FilterLocale> retValue = new ArrayList<FilterLocale>();
        for (final FilterLocale filterLocale : filterLocales) {
            retValue.add(filterLocale);
        }

        return retValue;
    }

    /**
     * Loops through the FilterTags held by this Filter, and returns the state of the tag if it exists, and -1 if it does not.
     *
     * @param tagID The id of the tag to get the state of
     * @return -1 if the tag was not found, otherwise the state of the tag
     */
    public List<Integer> hasTag(final Integer tagID) {
        final List<Integer> retValue = new ArrayList<Integer>();

        for (final FilterTag tag : getFilterTags()) {
            if (tag.getTag().getTagId().equals(tagID)) retValue.add(tag.getTagState());
        }

        return retValue;
    }

    public ArrayList<Integer> hasCategory(final Integer categoryId, final Integer projectId) {
        ArrayList<Integer> states = new ArrayList<Integer>();

        for (final FilterCategory cat : getFilterCategories()) {
            if (cat.getCategory().getCategoryId().equals(categoryId)
                    // Check if the project id matches. If the project is null then its the common project.
                    && ((cat.getProject() == null && projectId == null) || (cat.getProject() != null && cat.getProject().getId().equals(
                    projectId)))) states.add(cat.getCategoryState());
        }

        return states;
    }

    public ArrayList<Integer> hasLocale(final String localeName) {
        ArrayList<Integer> states = new ArrayList<Integer>();

        for (final FilterLocale locale : getFilterLocales()) {
            if (locale.getLocaleName().equals(localeName)) states.add(locale.getLocaleState());
        }

        return states;
    }

    /**
     * Not all FilterTags assigned to a filter have an associated FilterCategory. If a FilterTags belongs to a category that
     * does not have an associated FilterCategory, the default boolean logic is used.
     * <p/>
     * This function returns all the categories that the tags in this filter belong to. These are then matched to any associated
     * FilterCategories to find out how the tags are matched (i.e. are we matching all the tags ("And" logic), or one or more of
     * the tags matches ("Or" logic)), using the default logic ("Or") if no associated FilterCategory exists.
     *
     * @return An ArrayList containing the Category IDs that the tags in this filter belong to
     */
    @Transient
    public ArrayList<Category> getFilterTagCategories() {
        final ArrayList<Category> categories = new ArrayList<Category>();

        for (final FilterTag filterTag : filterTags) {
            final int filterTagState = filterTag.getTagState();

            if (filterTagState == CommonFilterConstants.MATCH_TAG_STATE || filterTagState == CommonFilterConstants.NOT_MATCH_TAG_STATE) {
                final Tag tag = filterTag.getTag();
                final Set<TagToCategory> tagToCategories = tag.getTagToCategories();

                if (tagToCategories.size() != 0) {
                    for (final TagToCategory category : tagToCategories) {
                        if (!categories.contains(category.getCategory())) {
                            categories.add(category.getCategory());
                        }
                    }
                } else {
                    if (!categories.contains(null)) categories.add(null);
                }
            }
        }

        return categories;
    }

    @Transient
    public ArrayList<Project> getFilterTagProjects() {
        final ArrayList<Project> projects = new ArrayList<Project>();

        for (final FilterTag filterTag : filterTags) {
            final Tag tag = filterTag.getTag();
            final Set<TagToProject> tagToProjects = tag.getTagToProjects();

            if (tagToProjects.size() != 0) {
                for (final TagToProject tagToProject : tagToProjects) {
                    final Project project = tagToProject.getProject();

                    if (!projects.contains(project)) projects.add(project);
                }
            } else {
                if (!projects.contains(null)) projects.add(null);
            }
        }

        return projects;
    }

    @Override
    @Transient
    public Integer getId() {
        return filterId;
    }

    @Transient
    public void addFilterTag(FilterTag filterTag) {
        getFilterTags().add(filterTag);
        filterTag.setFilter(this);
    }

    @Transient
    public void removeFilterTag(FilterTag filterTag) {
        getFilterTags().remove(filterTag);
        filterTag.setFilter(null);
    }

    @Transient
    public void addFilterLocale(FilterLocale filterLocale) {
        getFilterLocales().add(filterLocale);
        filterLocale.setFilter(this);
    }

    @Transient
    public void removeFilterLocale(FilterLocale filterLocale) {
        getFilterLocales().remove(filterLocale);
        filterLocale.setFilter(null);
    }

    @Transient
    public void addFilterCategory(FilterCategory filterCategory) {
        getFilterCategories().add(filterCategory);
        filterCategory.setFilter(this);
    }

    @Transient
    public void removeFilterCategory(FilterCategory filterCategory) {
        getFilterCategories().remove(filterCategory);
        filterCategory.setFilter(null);
    }

    @Transient
    public void addFilterField(FilterField filterField) {
        getFilterFields().add(filterField);
        filterField.setFilter(this);
    }

    @Transient
    public void removeFilterField(FilterField filterField) {
        getFilterFields().remove(filterField);
        filterField.setFilter(null);
    }
}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-visualization-model
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.visualization.model.diagram.statement;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import se.jguru.nazgul.tools.visualization.model.diagram.AbstractGraph;
import se.jguru.nazgul.tools.visualization.model.diagram.Comment;
import se.jguru.nazgul.tools.visualization.model.diagram.Graph;
import se.jguru.nazgul.tools.visualization.model.diagram.NodeID;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
 *
Utility class which orders statements and comments in a particular order.
 * The default order (which should yield no problematic/circular references) is:

 *

 *
AttributeStatements, which implies zero or all of the types {@link CommonGraphAttributes},
 * {@link CommonEdgeAttributes} and {@link CommonNodeAttributes}

 *
{@link Node} statements

 *
{@link Identifier} statements

 *
{@link Edge} statements

 *
{@link Subgraph} statements

 *

 *
 * @author Lennart Jörelid, jGuru Europe AB
 */
@XmlType(namespace = AbstractGraph.NAMESPACE, propOrder = {"commonAttributesComment", "commonGraphAttributes",
        "commonNodeAttributes", "commonEdgeAttributes", "nodesComment", "nodes", "identifiersComment", "identifiers",
        "edgesComment", "edges", "subgraphComment", "subgraphs"})
@XmlAccessorType(XmlAccessType.FIELD)
public class Statements implements Serializable {

    // Our Log
    @XmlTransient
    public static final Logger log = LoggerFactory.getLogger(Statement.class);

    /**
     * A comment to be rendered before the common/shared attribute statements.
     */
    @XmlElement
    private Comment commonAttributesComment;

    /**
     * An optional statement containing common/shared Graph attributes.
     * The attributes defined within the CommonGraphAttributes statement will be set as default on all Graph/Subgraphs.
     *
     * @see #subgraphs
     */
    @XmlElement
    private CommonGraphAttributes commonGraphAttributes;

    /**
     * An optional statement containing common/shared Node attributes.
     * The attributes defined within the CommonNodeAttributes statement will be set as default on all Nodes.
     *
     * @see #nodes
     */
    @XmlElement
    private CommonNodeAttributes commonNodeAttributes;

    /**
     * An optional statement containing common/shared Edge attributes.
     * The attributes defined within the CommonEdgeAttributes statement will be set as default on all Edges.
     *
     * @see #nodes
     */
    @XmlElement
    private CommonEdgeAttributes commonEdgeAttributes;

    /**
     * A comment to be rendered before the Node statements.
     */
    @XmlElement
    private Comment nodesComment;

    /**
     * A sequence of all Nodes added/known to this Statements wrapper.
     */
    @XmlElementWrapper
    @XmlElement(name = "node")
    private List<Node> nodes;

    /**
     * A comment to be rendered before the Identifier statements.
     */
    @XmlElement
    private Comment identifiersComment;

    /**
     * A sequence of all Identifiers added/known to this statements wrapper.
     */
    @XmlElementWrapper
    @XmlElement(name = "identifier")
    private List<Identifier> identifiers;

    /**
     * A comment to be rendered before the Edge statements.
     */
    @XmlElement
    private Comment edgesComment;

    /**
     * A sequence of all Edges added/known to this Statements wrapper.
     */
    @XmlElementWrapper
    @XmlElement(name = "edge")
    private List<Edge> edges;

    /**
     * A comment to be rendered before the Subgraph statements.
     */
    @XmlElement
    private Comment subgraphComment;

    /**
     * A sequence of all Subgraphs added/known to this Statements wrapper.
     */
    @XmlElement
    private List<Subgraph> subgraphs;

    /**
     * Default constructor, assigning default lines to all Comments.
     * Reserved for framework use only.
     */
    @SuppressWarnings("all")
    public Statements() {

        super();

        // Assign internal state
        this.nodes = new ArrayList<>();
        this.identifiers = new ArrayList<>();
        this.edges = new ArrayList<>();
        this.subgraphs = new ArrayList<>();

        setCommonAttributesComment(null);
        setNodesComment(null);
        setIdentifiersComment(null);
        setEdgesComment(null);
        setSubgraphComment(null);
    }

    /**
     * Assigns the comment to be rendered before the Common/Shared attribute statements.
     *
     * @param lines The line(s) within the Comment. A standard Comment will be used if lines is {@code null}.
     */
    public final void setCommonAttributesComment(final String... lines) {
        this.commonAttributesComment = lines == null
                ? new Comment("Common Attribute Statements")
                : new Comment(lines);
    }

    /**
     * Assigns the comment to be rendered before the Node statements.
     *
     * @param lines The line(s) within the Comment. A standard Comment will be used if lines is {@code null}.
     */
    public final void setNodesComment(final String... lines) {
        this.nodesComment = lines == null
                ? new Comment("Node Statements")
                : new Comment(lines);
    }

    /**
     * Assigns the comment to be rendered before the Identifier statements.
     *
     * @param lines The line(s) within the Comment. A standard Comment will be used if lines is {@code null}.
     */
    public final void setIdentifiersComment(final String... lines) {
        this.identifiersComment = lines == null
                ? new Comment("Identifier Statements")
                : new Comment(lines);
    }

    /**
     * Assigns the comment to be rendered before the Edge statements.
     *
     * @param lines The line(s) within the Comment. A standard Comment will be used if lines is {@code null}.
     */
    public final void setEdgesComment(final String... lines) {
        this.edgesComment = lines == null
                ? new Comment("Edge Statements")
                : new Comment(lines);
    }

    /**
     * Assigns the comment to be rendered before the Subgraph statements.
     *
     * @param lines The line(s) within the Comment. A standard Comment will be used if lines is {@code null}.
     */
    public final void setSubgraphComment(final String... lines) {
        this.subgraphComment = (lines == null
                ? new Comment("Subgraph Statements")
                : new Comment(lines));
    }

    /**
     * Adds one or more {@link Statement} objects to this {@link Statements} holder.
     *
     * @param toAdd One or more {@link Statement} instances.
     */
    @SuppressWarnings("PMD")
    public void add(final Statement... toAdd) {

        if (toAdd != null) {

            Arrays.stream(toAdd)
                    .filter(Objects::nonNull)
                    .forEach(c -> {

                        // Resolve the type of the current Statement, and assign it to
                        // the appropriate internal state.
                        if (c instanceof CommonEdgeAttributes) {
                            this.commonEdgeAttributes = (CommonEdgeAttributes) c;
                        } else if (c instanceof CommonGraphAttributes) {
                            this.commonGraphAttributes = (CommonGraphAttributes) c;
                        } else if (c instanceof CommonNodeAttributes) {
                            this.commonNodeAttributes = (CommonNodeAttributes) c;
                        } else if (c instanceof Node) {
                            this.nodes.add((Node) c);
                        } else if (c instanceof Identifier) {
                            this.identifiers.add((Identifier) c);
                        } else if (c instanceof Edge) {
                            this.edges.add((Edge) c);
                        } else if (c instanceof Subgraph) {
                            this.subgraphs.add((Subgraph) c);
                        }
                    });
        }
    }

    /**
     * Retrieves all Nodes within this Statement instance. No recursion into any Subgraph is done.
     *
     * @return all Nodes being immediate children to this {@link Statements} instance.
     */
    public List<Node> getNodes() {
        return nodes;
    }

    /**
     * Retrieves all Identifiers within this Statement instance. No recursion into any Subgraph is done.
     *
     * @return all Identifiers being immediate children to this {@link Statements} instance.
     */
    public List<Identifier> getIdentifiers() {
        return identifiers;
    }

    /**
     * Retrieves all Edges within this Statement instance. No recursion into any Subgraph is done.
     *
     * @return all Edges being immediate children to this {@link Statements} instance.
     */
    public List<Edge> getEdges() {
        return edges;
    }

    /**
     * Retrieves all Subgraphs within this Statement instance. No recursion into any child Subgraph is done.
     *
     * @return all Subgraphs being immediate children to this {@link Statements} instance.
     */
    public List<Subgraph> getSubgraphs() {
        return subgraphs;
    }

    /**
     * Retrieves an optional statement containing common/shared Graph attributes.
     * The attributes defined within the GraphAttribute statement will be set as default on all Graph/Subgraphs.
     *
     * @return an optional (i.e. nullable) statement containing common/shared Graph attributes.
     */
    public CommonGraphAttributes getCommonGraphAttributes() {
        return commonGraphAttributes;
    }

    /**
     * Retrieves an optional statement containing common/shared Node attributes.
     * The attributes defined within the NodeAttribute statement will be set as default on all Nodes.
     *
     * @return an optional (i.e. nullable) statement containing common/shared Node attributes.
     */
    public CommonNodeAttributes getCommonNodeAttributes() {
        return commonNodeAttributes;
    }

    /**
     * Retrieves an optional statement containing common/shared Edge attributes.
     * The attributes defined within the EdgeAttribute statement will be set as default on all Edges.
     *
     * @return an optional (i.e. nullable) statement containing common/shared Edge attributes.
     */
    public CommonEdgeAttributes getCommonEdgeAttributes() {
        return commonEdgeAttributes;
    }

    /**
     * @return A comment to be rendered before the common/shared attribute statements.
     */
    public Comment getCommonAttributesComment() {
        return commonAttributesComment;
    }

    /**
     * @return A comment to be rendered before the Node statements.
     */
    public Comment getNodesComment() {
        return nodesComment;
    }

    /**
     * @return A comment to be rendered before the Identifier statements.
     */
    public Comment getIdentifiersComment() {
        return identifiersComment;
    }

    /**
     * @return A comment to be rendered before the Edge statements.
     */
    public Comment getEdgesComment() {
        return edgesComment;
    }

    /**
     * @return A comment to be rendered before the Subgraph statements.
     */
    public Comment getSubgraphComment() {
        return subgraphComment;
    }

    /**
     * Finds the Node with the supplied identifier within this Statements (and within any contained Subgraphs, if the
     * {@code recursive} flag is true).
     *
     * @param identifier The identifier to match against the {@link Node#getId()} of a matching Node.
     * @param recursive  If {@code true}, the search should recurse into the Statements of Subgraphs.
     * @return The Node with the supplied identifier - or null if none was found.
     */
    public Node findNode(final String identifier, final boolean recursive) {

        Optional<Node> toReturn = getNodes().stream().filter(c -> c.getId().equals(identifier)).findFirst();
        if (!toReturn.isPresent() && recursive && getSubgraphs() != null && !getSubgraphs().isEmpty()) {

            // Does the Node with the given identifier exist in any Subgraph?
            toReturn = getSubgraphs().stream()
                    .map(c -> c.getStatements().findNode(identifier, true))
                    .filter(c -> c != null)
                    .findFirst();
        }

        // All Done.
        return toReturn.orElse(null);
    }

    /**
     * Finds the Edge with the supplied identifier within this Statements (and within any contained Subgraphs, if the
     * {@code recursive} flag is true).
     *
     * @param fromId    The Node or Subgraph identifier of the starting point of the Edge to find.
     * @param toId      The Node or Subgraph identifier of the finishing point of the Edge to find.
     * @param recursive If {@code true}, the search should recurse into the Statements of Subgraphs.
     * @return The Edge with the supplied identifier - or null if none was found.
     */
    public Edge findEdge(final String fromId, final String toId, final boolean recursive) {

        Optional<Edge> toReturn = getEdges().stream()
                .filter(c -> c.getId().equals(fromId) && c.getRightSideEdge().getId().equals(toId))
                .findFirst();

        if (!toReturn.isPresent() && recursive) {

            // Does the Node with the given identifier exist in any Subgraph?
            toReturn = subgraphs.stream().map(c -> c.getStatements().findEdge(fromId, toId, true)).findFirst();
        }

        // All Done.
        return toReturn.orElse(null);
    }

    /**
     * Finds the Subgraph with the supplied identifier within this Statements (and within any contained Subgraphs, if
     * the {@code recursive} flag is true).
     *
     * @param identifier The identifier to match against the {@link Subgraph#getId()} of a matching Node.
     * @param recursive  If {@code true}, the search should recurse into the Statements of Subgraphs.
     * @return The Subgraph with the supplied identifier - or null if none was found.
     */
    public Subgraph findSubgraph(final String identifier, final boolean recursive) {

        Optional<Subgraph> toReturn = getSubgraphs().stream().filter(c -> c.getId().equals(identifier)).findFirst();
        if (!toReturn.isPresent() && recursive && getSubgraphs() != null && !getSubgraphs().isEmpty()) {

            // Does the Node with the given identifier exist in any Subgraph?
            toReturn = getSubgraphs().stream()
                    .map(c -> c.getStatements().findSubgraph(identifier, true))
                    .filter(c -> c != null)
                    .findFirst();
        }

        // All Done.
        return toReturn.orElse(null);
    }

    /**
     * Convenience method to add and return an Edge between a Node/Subgraph and another Node/Subgraph.
     *
     * @param fromId      The non-empty ID of the Node/Subgraph from which the Edge should originate.
     * @param toId        The non-empty ID of the Node/Subgraph to which the Edge should be directed.
     * @param parentGraph The non-null parent {@link Graph} where the Edge is about to be created.
     * @return The newly created Edge - or {@code null} if the Edge could not be created.
     */
    public Edge addEdge(final String fromId, final String toId, final Graph parentGraph) {

        // Check sanity
        if (fromId == null || fromId.isEmpty()) {
            throw new IllegalArgumentException("Cannot handle null or empty 'fromId' argument.");
        }
        if (toId == null || toId.isEmpty()) {
            throw new IllegalArgumentException("Cannot handle null or empty 'toId' argument.");
        }
        if (parentGraph == null) {
            throw new IllegalArgumentException("Cannot handle null 'parentGraph' argument.");
        }

        Edge toReturn = null;

        // Find the nodes between which the Edge should be created.
        final Node fromNode = findNode(fromId, true);
        final Subgraph fromSubgraph = findSubgraph(fromId, true);

        if (fromNode != null || fromSubgraph != null) {

            // Find the RHS edge data.
            final Node toNode = findNode(toId, true);
            final Subgraph toSubgraph = findSubgraph(toId, true);
            final NodeID toNodeId = toNode == null ? null : toNode.getNodeID();

            // Can we actually create an Edge?
            if (toNodeId != null || toSubgraph != null) {

                // Start by creating the RightSideEdge.
                final RightSideEdge rightSideEdge = toNodeId == null
                        ? new RightSideEdge(toSubgraph)
                        : new RightSideEdge(toNodeId);

                // Finally, create the Edge to return.
                toReturn = fromNode == null
                        ? new Edge(fromSubgraph, rightSideEdge)
                        : new Edge(fromNode.getNodeID(), rightSideEdge);
            }
        }

        // Add the newly created Edge, if applicable.
        if (toReturn != null) {
            add(toReturn);
        }

        // All Done.
        return toReturn;
    }
}


/*-
 * #%L
 * Nazgul Project: nazgul-tools-checkstyle-maven-plugin
 * %%
 * Copyright (C) 2010 - 2018 jGuru Europe AB
 * %%
 * Licensed under the jGuru Europe AB license (the "License"), based
 * on Apache License, Version 2.0; you may not use this file except
 * in compliance with the License.
 *
 * You may obtain a copy of the License at
 *
 *       http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt
 *
 * 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.
 * #L%
 */

package se.jguru.nazgul.tools.plugin.checkstyle;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import com.puppycrawl.tools.checkstyle.DefaultLogger;
import com.puppycrawl.tools.checkstyle.XMLLogger;
import com.puppycrawl.tools.checkstyle.api.AuditListener;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Dependency;
import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginManagement;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecution;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.configuration.PlexusConfiguration;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.PathTool;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.xml.pull.MXParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParser;
import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleExecutor;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleExecutorException;
import se.jguru.nazgul.tools.plugin.checkstyle.exec.CheckstyleExecutorRequest;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * Performs Checkstyle analysis and outputs violations or a count of violations
 * to the console, potentially failing the build.
 * It can also be configured to re-use an earlier analysis.
 *
 * @author Joakim Erdfelt
 * @version $Id$
 */
// @Mojo(name = "check", defaultPhase = LifecyclePhase.VERIFY, requiresDependencyResolution = ResolutionScope.TEST,
//         threadSafe = true)
// @Execute(goal = "checkstyle")
public class CheckstyleViolationCheckMojo
        extends AbstractMojo {

    private static final String JAVA_FILES = "**\\/*.java";

    private static final String CHECKSTYLE_FILE_HEADER = "<?xml version=\"1.0\"?>\n"
            + "<!DOCTYPE module PUBLIC \"-//Puppy Crawl//DTD Check Configuration 1.3//EN\"\n"
            + "        \"http://www.puppycrawl.com/dtds/configuration_1_3.dtd\">\n";

    /**
     * Specifies the path and filename to save the Checkstyle output. The format
     * of the output file is determined by the outputFileFormat
     * parameter.
     */
    @Parameter(property = "checkstyle.output.file", defaultValue = "${project.build.directory}/checkstyle-result.xml")
    private File outputFile;

    /**
     * Specifies the format of the output to be used when writing to the output
     * file. Valid values are "plain" and "xml".
     */
    @Parameter(property = "checkstyle.output.format", defaultValue = "xml")
    private String outputFileFormat;

    /**
     * Fail the build on a violation. The goal checks for the violations
     * after logging them (if {@link #logViolationsToConsole} is {@code true}).
     * Compare this to {@link #failsOnError} which fails the build immediately
     * before examining the output log.
     */
    @Parameter(property = "checkstyle.failOnViolation", defaultValue = "true")
    private boolean failOnViolation;

    /**
     * The maximum number of allowed violations. The execution fails only if the
     * number of violations is above this limit.
     *
     * @since 2.3
     */
    @Parameter(property = "checkstyle.maxAllowedViolations", defaultValue = "0")
    private int maxAllowedViolations;

    /**
     * The lowest severity level that is considered a violation.
     * Valid values are "error", "warning" and "info".
     *
     * @since 2.2
     */
    @Parameter(property = "checkstyle.violationSeverity", defaultValue = "error")
    private String violationSeverity = "error";

    /**
     * Violations to ignore. This is a comma-separated list, each value being either
     * a rule name, a rule category or a java package name of rule class.
     *
     * @since 2.13
     */
    @Parameter(property = "checkstyle.violation.ignore")
    private String violationIgnore;

    /**
     * Skip entire check.
     *
     * @since 2.2
     */
    @Parameter(property = "checkstyle.skip", defaultValue = "false")
    private boolean skip;

    /**
     * Skip Checkstyle execution will only scan the outputFile.
     *
     * @since 2.5
     */
    @Parameter(property = "checkstyle.skipExec", defaultValue = "false")
    private boolean skipExec;

    /**
     * Output the detected violations to the console.
     *
     * @since 2.3
     */
    @Parameter(property = "checkstyle.console", defaultValue = "true")
    private boolean logViolationsToConsole;

    /**
     * Specifies the location of the resources to be used for Checkstyle.
     *
     * @since 2.11
     */
    @Parameter(defaultValue = "${project.resources}", readonly = true)
    protected List<Resource> resources;

    /**
     * Specifies the location of the test resources to be used for Checkstyle.
     *
     * @since 2.16
     */
    @Parameter(defaultValue = "${project.testResources}", readonly = true)
    protected List<Resource> testResources;

    /**
     *

     * Specifies the location of the XML configuration to use.
     *

     *

     *

     * Potential values are a filesystem path, a URL, or a classpath resource.
     * This parameter expects that the contents of the location conform to the
     * xml format (Checkstyle Checker
     * module) configuration of rulesets.
     *

     *

     *

     * This parameter is resolved as resource, URL, then file. If successfully
     * resolved, the contents of the configuration is copied into the
     * ${project.build.directory}/checkstyle-configuration.xml
     * file before being passed to Checkstyle as a configuration.
     *

     *

     *

     * There are 2 predefined rulesets.
     *

     *

     *
sun_checks.xml: Sun Checks.

     *
google_checks.xml: Google Checks.

     *

     *
     * @since 2.5
     */
    @Parameter(property = "checkstyle.config.location", defaultValue = "sun_checks.xml")
    private String configLocation;

    /**
     *

     * Specifies the location of the properties file.
     *

     *

     *

     * This parameter is resolved as URL, File then resource. If successfully
     * resolved, the contents of the properties location is copied into the
     * ${project.build.directory}/checkstyle-checker.properties
     * file before being passed to Checkstyle for loading.
     *

     *

     *

     * The contents of the propertiesLocation will be made
     * available to Checkstyle for specifying values for parameters within the
     * xml configuration (specified in the configLocation
     * parameter).
     *

     *
     * @since 2.5
     */
    @Parameter(property = "checkstyle.properties.location")
    private String propertiesLocation;

    /**
     * Allows for specifying raw property expansion information.
     */
    @Parameter
    private String propertyExpansion;

    /**
     *

     * Specifies the location of the License file (a.k.a. the header file) that
     * can be used by Checkstyle to verify that source code has the correct
     * license header.
     *

     *

     * You need to use ${checkstyle.header.file} in your Checkstyle xml
     * configuration to reference the name of this header file.
     *

     *

     * For instance:
     *

     *

     *
     * <module name="RegexpHeader">
     * <property name="headerFile" value="${checkstyle.header.file}"/>
     * </module>
     *
     *

     *
     * @since 2.0-beta-2
     */
    @Parameter(property = "checkstyle.header.file", defaultValue = "LICENSE.txt")
    private String headerLocation;

    /**
     * Specifies the cache file used to speed up Checkstyle on successive runs.
     */
    @Parameter(defaultValue = "${project.build.directory}/checkstyle-cachefile")
    private String cacheFile;

    /**
     * The key to be used in the properties for the suppressions file.
     *
     * @since 2.1
     */
    @Parameter(property = "checkstyle.suppression.expression", defaultValue = "checkstyle.suppressions.file")
    private String suppressionsFileExpression;

    /**
     *

     * Specifies the location of the suppressions XML file to use.
     *

     *

     *

     * This parameter is resolved as resource, URL, then file. If successfully
     * resolved, the contents of the suppressions XML is copied into the
     * ${project.build.directory}/checkstyle-suppressions.xml file
     * before being passed to Checkstyle for loading.
     *

     *

     *

     * See suppressionsFileExpression for the property that will
     * be made available to your Checkstyle configuration.
     *

     *
     * @since 2.0-beta-2
     */
    @Parameter(property = "checkstyle.suppressions.location")
    private String suppressionsLocation;

    /**
     * The file encoding to use when reading the source files. If the property project.build.sourceEncoding
     * is not set, the platform default encoding is used. Note: This parameter always overrides the
     * property charset from Checkstyle's TreeWalker module.
     *
     * @since 2.2
     */
    @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
    private String encoding;

    /**
     * @since 2.5
     */
    @Component(role = CheckstyleExecutor.class, hint = "default")
    protected CheckstyleExecutor checkstyleExecutor;

    /**
     * Output errors to console.
     */
    @Parameter(property = "checkstyle.consoleOutput", defaultValue = "false")
    private boolean consoleOutput;

    /**
     * The Maven Project Object.
     */
    @Parameter(defaultValue = "${project}", readonly = true, required = true)
    protected MavenProject project;

    /**
     * The Plugin Descriptor
     */
    @Parameter(defaultValue = "${plugin}", readonly = true, required = true)
    private PluginDescriptor plugin;

    // remove when requiring Maven 3.x, just use #plugin
    @Parameter(defaultValue = "${mojoExecution}", readonly = true, required = true)
    private MojoExecution mojoExecution;

    /**
     * If null, the Checkstyle plugin will display violations on stdout.
     * Otherwise, a text file will be created with the violations.
     */
    @Parameter
    private File useFile;

    /**
     * Specifies the names filter of the source files to be excluded for
     * Checkstyle.
     */
    @Parameter(property = "checkstyle.excludes")
    private String excludes;

    /**
     * Specifies the names filter of the source files to be used for Checkstyle.
     */
    @Parameter(property = "checkstyle.includes", defaultValue = JAVA_FILES, required = true)
    private String includes;

    /**
     * Specifies the names filter of the files to be excluded for
     * Checkstyle when checking resources.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.resourceExcludes")
    private String resourceExcludes;

    /**
     * Specifies the names filter of the files to be used for Checkstyle when checking resources.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.resourceIncludes", defaultValue = "**/*.properties", required = true)
    private String resourceIncludes;

    /**
     * If this is true, and Checkstyle reported any violations or errors,
     * the build fails immediately after running Checkstyle, before checking the log
     * for {@link #logViolationsToConsole}. If you want to use {@link #logViolationsToConsole},
     * use {@link #failOnViolation} instead of this.
     */
    @Parameter(defaultValue = "false")
    private boolean failsOnError;

    /**
     * Specifies the location of the test source directory to be used for
     * Checkstyle.
     *
     * @since 2.2
     * @deprecated instead use {@link #testSourceDirectories}
     */
    @Deprecated
    @Parameter
    private File testSourceDirectory;

    /**
     * Specifies the location of the test source directories to be used for
     * Checkstyle.
     *
     * @since 2.13
     */
    @Parameter(defaultValue = "${project.testCompileSourceRoots}")
    private List<String> testSourceDirectories;

    /**
     * Include or not the test source directory to be used for Checkstyle.
     *
     * @since 2.2
     */
    @Parameter(defaultValue = "false")
    private boolean includeTestSourceDirectory;

    /**
     * Specifies the location of the source directory to be used for Checkstyle.
     *
     * @deprecated instead use {@link #sourceDirectories}
     */
    @Deprecated
    @Parameter
    private File sourceDirectory;

    /**
     * Specifies the location of the source directories to be used for Checkstyle.
     *
     * @since 2.13
     */
    @Parameter(defaultValue = "${project.compileSourceRoots}")
    private List<String> sourceDirectories;

    /**
     * Whether to apply Checkstyle to resource directories.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.includeResources", defaultValue = "true", required = true)
    private boolean includeResources = true;

    /**
     * Whether to apply Checkstyle to test resource directories.
     *
     * @since 2.11
     */
    @Parameter(property = "checkstyle.includeTestResources", defaultValue = "true", required = true)
    private boolean includeTestResources = true;

    /**
     * By using this property, you can specify the whole Checkstyle rules
     * inline directly inside this pom. This feature requires Maven 3+.
     *

     *

     * <plugin>
     *   ...
     *   <configuration>
     *     <checkstyleRules>
     *       <module name="Checker">
     *         <module name="FileTabCharacter">
     *           <property name="eachLine" value="true" />
     *         </module>
     *         <module name="TreeWalker">
     *           <module name="EmptyBlock"/>
     *         </module>
     *       </module>
     *     </checkstyleRules>
     *   </configuration>
     *   ...
     *

     *
     * @since 2.12
     */
    @Parameter
    private PlexusConfiguration checkstyleRules;

    /**
     * dump file for inlined Checkstyle rules
     */
    @Parameter(property = "checkstyle.output.rules.file",
            defaultValue = "${project.build.directory}/checkstyle-rules.xml")
    private File rulesFiles;

    private ByteArrayOutputStream stringOutputStream;

    /**
     * {@inheritDoc}
     */
    public void execute()
            throws MojoExecutionException, MojoFailureException {
        if (skip) {
            return;
        }

        if (!skipExec) {
            if (checkstyleRules != null) {
                if (!"sun_checks.xml".equals(configLocation)) {
                    throw new MojoExecutionException("If you use inline configuration for rules, don't specify "
                            + "a configLocation");
                }
                if (checkstyleRules.getChildCount() > 1) {
                    throw new MojoExecutionException("Currently only one root module is supported");
                }

                PlexusConfiguration checkerModule = checkstyleRules.getChild(0);

                try {
                    FileUtils.forceMkdir(rulesFiles.getParentFile());
                    FileUtils.fileWrite(rulesFiles, CHECKSTYLE_FILE_HEADER + checkerModule.toString());
                } catch (final IOException e) {
                    throw new MojoExecutionException(e.getMessage(), e);
                }
                configLocation = rulesFiles.getAbsolutePath();
            }

            ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();

            try {
                CheckstyleExecutorRequest request = new CheckstyleExecutorRequest();
                request.setConsoleListener(getConsoleListener()).setConsoleOutput(consoleOutput)
                        .setExcludes(excludes).setFailsOnError(failsOnError).setIncludes(includes)
                        .setResourceIncludes(resourceIncludes)
                        .setResourceExcludes(resourceExcludes)
                        .setIncludeResources(includeResources)
                        .setIncludeTestResources(includeTestResources)
                        .setIncludeTestSourceDirectory(includeTestSourceDirectory).setListener(getListener())
                        .setProject(project).setSourceDirectories(getSourceDirectories())
                        .setResources(resources).setTestResources(testResources)
                        .setStringOutputStream(stringOutputStream).setSuppressionsLocation(suppressionsLocation)
                        .setTestSourceDirectories(getTestSourceDirectories()).setConfigLocation(configLocation)
                        .setConfigurationArtifacts(collectArtifacts("config"))
                        .setPropertyExpansion(propertyExpansion)
                        .setHeaderLocation(headerLocation).setLicenseArtifacts(collectArtifacts("license"))
                        .setCacheFile(cacheFile).setSuppressionsFileExpression(suppressionsFileExpression)
                        .setEncoding(encoding).setPropertiesLocation(propertiesLocation);
                checkstyleExecutor.executeCheckstyle(request);

            } catch (CheckstyleException e) {
                throw new MojoExecutionException("Failed during checkstyle configuration", e);
            } catch (CheckstyleExecutorException e) {
                throw new MojoExecutionException("Failed during checkstyle execution", e);
            } finally {
                //be sure to restore original context classloader
                Thread.currentThread().setContextClassLoader(currentClassLoader);
            }
        }

        if (!"xml".equals(outputFileFormat)) {
            throw new MojoExecutionException("Output format is '" + outputFileFormat
                    + "', checkstyle:check requires format to be 'xml'.");
        }

        if (!outputFile.exists()) {
            getLog().info("Unable to perform checkstyle:check, unable to find checkstyle:checkstyle outputFile.");
            return;
        }

        Reader reader = null;
        try {
            reader = new BufferedReader(ReaderFactory.newXmlReader(outputFile));

            XmlPullParser xpp = new MXParser();
            xpp.setInput(reader);

            int violations = countViolations(xpp);

            if (violations > maxAllowedViolations) {
                if (failOnViolation) {
                    String msg =
                            "You have " + violations + " Checkstyle violation" + ((violations > 1) ? "s" : "") + ".";
                    if (maxAllowedViolations > 0) {
                        msg += " The maximum number of allowed violations is " + maxAllowedViolations + ".";
                    }
                    throw new MojoFailureException(msg);
                }

                getLog().warn("checkstyle:check violations detected but failOnViolation set to false");
            }

            reader.close();
            reader = null;
        } catch (IOException | XmlPullParserException e) {
            throw new MojoExecutionException("Unable to read Checkstyle results xml: " + outputFile.getAbsolutePath(),
                    e);
        } finally {
            IOUtil.close(reader);
        }
    }

    private int countViolations(XmlPullParser xpp)
            throws XmlPullParserException, IOException {
        int count = 0;
        int ignoreCount = 0;
        RuleUtil.Matcher[] ignores =
                (violationIgnore == null) ? null : RuleUtil.parseMatchers(violationIgnore.split(","));

        String basedir = project.getBasedir().getAbsolutePath();
        String file = "";
        for (int eventType = xpp.getEventType(); eventType != XmlPullParser.END_DOCUMENT; eventType = xpp.next()) {
            if (eventType != XmlPullParser.START_TAG) {
                continue;
            } else if ("file".equals(xpp.getName())) {
                file = PathTool.getRelativeFilePath(basedir, xpp.getAttributeValue("", "name"));
                //file = file.substring( file.lastIndexOf( File.separatorChar ) + 1 );
            } else if ("error".equals(xpp.getName())) {
                String severity = xpp.getAttributeValue("", "severity");

                if (!isViolation(severity)) {
                    continue;
                }

                String source = xpp.getAttributeValue("", "source");

                if (ignore(ignores, source)) {
                    ignoreCount++;
                } else {
                    count++;

                    if (logViolationsToConsole) {
                        String line = xpp.getAttributeValue("", "line");
                        String column = xpp.getAttributeValue("", "column");
                        String message = xpp.getAttributeValue("", "message");
                        String rule = RuleUtil.getName(source);
                        String category = RuleUtil.getCategory(source);

                        log(severity, file + ":[" + line + ((column == null) ? "" : (',' + column)) + "] ("
                                + category + ") " + rule + ": " + message);
                    }
                }
            }
        }

        if (ignoreCount > 0) {
            getLog().info("Ignored " + ignoreCount + " error" + ((ignoreCount > 1) ? "s" : "") + ", " + count
                    + " violation" + ((count > 1) ? "s" : "") + " remaining.");
        }

        return count;
    }

    private void log(String severity, String message) {
        if ("info".equals(severity)) {
            getLog().info(message);
        } else if ("warning".equals(severity)) {
            getLog().warn(message);
        } else {
            getLog().error(message);
        }
    }

    /**
     * Checks if the given severity is considered a violation.
     *
     * @param severity The severity to check
     * @return true if the given severity is a violation, otherwise false
     */
    private boolean isViolation(String severity) {
        if ("error".equals(severity)) {
            return "error".equals(violationSeverity) || "warning".equals(violationSeverity)
                    || "info".equals(violationSeverity);
        } else if ("warning".equals(severity)) {
            return "warning".equals(violationSeverity) || "info".equals(violationSeverity);
        } else if ("info".equals(severity)) {
            return "info".equals(violationSeverity);
        } else {
            return false;
        }
    }

    private boolean ignore(RuleUtil.Matcher[] ignores, String source) {
        if (ignores != null) {
            for (RuleUtil.Matcher ignore : ignores) {
                if (ignore.match(source)) {
                    return true;
                }
            }
        }

        return false;
    }

    private DefaultLogger getConsoleListener()
            throws MojoExecutionException {
        DefaultLogger consoleListener;

        if (useFile == null) {
            stringOutputStream = new ByteArrayOutputStream();
            consoleListener = new DefaultLogger(stringOutputStream, false);
        } else {
            OutputStream out = getOutputStream(useFile);

            consoleListener = new DefaultLogger(out, true);
        }

        return consoleListener;
    }

    private OutputStream getOutputStream(File file)
            throws MojoExecutionException {
        File parentFile = file.getAbsoluteFile().getParentFile();

        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }

        FileOutputStream fileOutputStream;
        try {
            fileOutputStream = new FileOutputStream(file);
        } catch (FileNotFoundException e) {
            throw new MojoExecutionException("Unable to create output stream: " + file, e);
        }
        return fileOutputStream;
    }

    private AuditListener getListener()
            throws MojoFailureException, MojoExecutionException {
        AuditListener listener = null;

        if (StringUtils.isNotEmpty(outputFileFormat)) {
            File resultFile = outputFile;

            OutputStream out = getOutputStream(resultFile);

            if ("xml".equals(outputFileFormat)) {
                listener = new XMLLogger(out, true);
            } else if ("plain".equals(outputFileFormat)) {
                listener = new DefaultLogger(out, true);
            } else {
                throw new MojoFailureException("Invalid output file format: (" + outputFileFormat
                        + "). Must be 'plain' or 'xml'.");
            }
        }

        return listener;
    }

    @SuppressWarnings("unchecked")
    private List<Artifact> collectArtifacts(String hint) {
        if (plugin == null || plugin.getGroupId() == null) {
            // Maven 2.x workaround
            plugin = mojoExecution.getMojoDescriptor().getPluginDescriptor();
        }

        List<Artifact> artifacts = new ArrayList<>();

        PluginManagement pluginManagement = project.getBuild().getPluginManagement();
        if (pluginManagement != null) {
            artifacts.addAll(getCheckstylePluginDependenciesAsArtifacts(pluginManagement.getPluginsAsMap(), hint));
        }

        artifacts.addAll(getCheckstylePluginDependenciesAsArtifacts(project.getBuild().getPluginsAsMap(), hint));

        return artifacts;
    }

    private List<Artifact> getCheckstylePluginDependenciesAsArtifacts(Map<String, Plugin> plugins, String hint) {
        List<Artifact> artifacts = new ArrayList<>();

        Plugin checkstylePlugin = plugins.get(plugin.getGroupId() + ":" + plugin.getArtifactId());
        if (checkstylePlugin != null) {
            for (Dependency dep : checkstylePlugin.getDependencies()) {
                // @todo if we can filter on hints, it should be done here...
                String depKey = dep.getGroupId() + ":" + dep.getArtifactId();
                artifacts.add((Artifact) plugin.getArtifactMap().get(depKey));
            }
        }
        return artifacts;
    }

    private List<File> getSourceDirectories() {
        List<File> sourceDirs = null;
        // if sourceDirectory is explicitly set, use it
        if (sourceDirectory != null) {
            sourceDirs = Collections.singletonList(sourceDirectory);
        } else {
            sourceDirs = new ArrayList<>(sourceDirectories.size());
            for (String sourceDir : sourceDirectories) {
                sourceDirs.add(FileUtils.resolveFile(project.getBasedir(), sourceDir));
            }
        }

        return sourceDirs;
    }

    private List<File> getTestSourceDirectories() {
        List<File> testSourceDirs = null;
        // if testSourceDirectory is explicitly set, use it
        if (testSourceDirectory != null) {
            testSourceDirs = Collections.singletonList(testSourceDirectory);
        }
        // probably null-check only required due to MavenProjectStubs
        else if (testSourceDirectories != null) {
            testSourceDirs = new ArrayList<>(testSourceDirectories.size());
            for (String testSourceDir : testSourceDirectories) {
                testSourceDirs.add(FileUtils.resolveFile(project.getBasedir(), testSourceDir));
            }
        }

        return testSourceDirs;
    }

}