/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.libs;

import java.io.Serializable;
import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/** Time utilities. */
public class Time {

  static Pattern days = Pattern.compile("^([0-9]+)d$");
  static Pattern hours = Pattern.compile("^([0-9]+)h$");
  static Pattern minutes = Pattern.compile("^([0-9]+)mi?n$");
  static Pattern seconds = Pattern.compile("^([0-9]+)s$");

  /**
   * Parses a duration.
   *
   * @param duration a quantity of time, such as 3h, 2mn, 7s
   * @return the length of the duration in seconds
   */
  public static int parseDuration(String duration) {
    if (duration == null) {
      return 60 * 60 * 24 * 30;
    }
    int toAdd = -1;

    /*
     * The `matcher.matches()` statements are required since matcher is stateful.
     * More information: https://docs.oracle.com/javase/8/docs/api/java/util/regex/Matcher.html#matches--
     */
    if (days.matcher(duration).matches()) {
      Matcher matcher = days.matcher(duration);
      matcher.matches();
      toAdd = Integer.parseInt(matcher.group(1)) * (60 * 60) * 24;
    } else if (hours.matcher(duration).matches()) {
      Matcher matcher = hours.matcher(duration);
      matcher.matches();
      toAdd = Integer.parseInt(matcher.group(1)) * (60 * 60);
    } else if (minutes.matcher(duration).matches()) {
      Matcher matcher = minutes.matcher(duration);
      matcher.matches();
      toAdd = Integer.parseInt(matcher.group(1)) * (60);
    } else if (seconds.matcher(duration).matches()) {
      Matcher matcher = seconds.matcher(duration);
      matcher.matches();
      toAdd = Integer.parseInt(matcher.group(1));
    }
    if (toAdd == -1) {
      throw new IllegalArgumentException("Invalid duration pattern : " + duration);
    }
    return toAdd;
  }

  /**
   * Parses a CRON expression.
   *
   * @param cron the CRON String
   * @return the next Date that satisfies the expression
   */
  public static Date parseCRONExpression(String cron) {
    try {
      return new CronExpression(cron).getNextValidTimeAfter(new Date());
    } catch (Exception e) {
      throw new IllegalArgumentException("Invalid CRON pattern : " + cron, e);
    }
  }

  /**
   * Computes the number of milliseconds between the next valid date and the one after.
   *
   * @param cron the CRON String
   * @return the number of milliseconds between the next valid date and the one after, with an
   *     invalid interval between
   */
  public static long cronInterval(String cron) {
    return cronInterval(cron, new Date());
  }

  /**
   * Compute the number of milliseconds between the next valid date and the one after.
   *
   * @param cron the CRON String
   * @param date the date to start search
   * @return the number of milliseconds between the next valid date and the one after, with an
   *     invalid interval between
   */
  public static long cronInterval(String cron, Date date) {
    try {
      return new CronExpression(cron).getNextInterval(date);
    } catch (Exception e) {
      throw new IllegalArgumentException("Invalid CRON pattern : " + cron, e);
    }
  }

  /**
   * Thanks to Quartz project, https://quartz.dev.java.net
   *
   *
Provides a parser and evaluator for unix-like cron expressions. Cron expressions provide the
   * ability to specify complex time combinations such as "At 8:00am every Monday through
   * Friday" or "At 1:30am every last Friday of the month".
   *
   *
Cron expressions are comprised of 6 required fields and one optional field separated by
   * white space. The fields respectively are described as follows:
   *
   *

   *

   * Field Name
   *
   * Allowed Values
   *
   * Allowed Special Characters
   *

   *

   * Seconds
   *
   * 0-59
   *
   * , - * /
   *

   *

   * Minutes
   *
   * 0-59
   *
   * , - * /
   *

   *

   * Hours
   *
   * 0-23
   *
   * , - * /
   *

   *

   * Day-of-month
   *
   * 1-31
   *
   * , - * ? / L W
   *

   *

   * Month
   *
   * 1-12 or JAN-DEC
   *
   * , - * /
   *

   *

   * Day-of-Week
   *
   * 1-7 or SUN-SAT
   *
   * , - * ? / L #
   *

   *

   * Year (Optional)
   *
   * empty, 1970-2099
   *
   * , - * /
   *

   *

   *
   * The '*' character is used to specify all values. For example, "*" in the minute field
   * means "every minute".
   *
   *
The '?' character is allowed for the day-of-month and day-of-week fields. It is used to
   * specify 'no specific value'. This is useful when you need to specify something in one of the
   * two fields, but not the other.
   *
   *
The '-' character is used to specify ranges For example "10-12" in the hour field
   * means "the hours 10, 11 and 12".
   *
   *
The ',' character is used to specify additional values. For example "MON,WED,FRI"
   * in the day-of-week field means "the days Monday, Wednesday, and Friday".
   *
   *
The '/' character is used to specify increments. For example "0/15" in the seconds
   * field means "the seconds 0, 15, 30, and 45". And "5/15" in the seconds
   * field means "the seconds 5, 20, 35, and 50". Specifying '*' before the '/' is
   * equivalent to specifying 0 is the value to start with. Essentially, for each field in the
   * expression, there is a set of numbers that can be turned on or off. For seconds and minutes,
   * the numbers range from 0 to 59. For hours 0 to 23, for days of the month 0 to 31, and for
   * months 1 to 12. The "/" character simply helps you turn on every "nth"
   * value in the given set. Thus "7/6" in the month field only turns on month
   * "7", it does NOT mean every 6th month, please note that subtlety.
   *
   *
The 'L' character is allowed for the day-of-month and day-of-week fields. This character is
   * short-hand for "last", but it has different meaning in each of the two fields. For
   * example, the value "L" in the day-of-month field means "the last day of the
   * month" - day 31 for January, day 28 for February on non-leap years. If used in the
   * day-of-week field by itself, it simply means "7" or "SAT". But if used in
   * the day-of-week field after another value, it means "the last xxx day of the month" -
   * for example "6L" means "the last friday of the month". When using the 'L'
   * option, it is important not to specify lists, or ranges of values, as you'll get confusing
   * results.
   *
   *
The 'W' character is allowed for the day-of-month field. This character is used to specify
   * the weekday (Monday-Friday) nearest the given day. As an example, if you were to specify
   * "15W" as the value for the day-of-month field, the meaning is: "the nearest
   * weekday to the 15th of the month". So if the 15th is a Saturday, the trigger will fire on
   * Friday the 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If the 15th
   * is a Tuesday, then it will fire on Tuesday the 15th. However if you specify "1W" as
   * the value for day-of-month, and the 1st is a Saturday, the trigger will fire on Monday the 3rd,
   * as it will not 'jump' over the boundary of a month's days. The 'W' character can only be
   * specified when the day-of-month is a single day, not a range or list of days.
   *
   *
The 'L' and 'W' characters can also be combined for the day-of-month expression to yield
   * 'LW', which translates to "last weekday of the month".
   *
   *
The '#' character is allowed for the day-of-week field. This character is used to specify
   * "the nth" xxx day of the month. For example, the value of "6#3" in the
   * day-of-week field means the third Friday of the month (day 6 = Friday and "#3" = the
   * 3rd one in the month). Other examples: "2#1" = the first Monday of the month and
   * "4#5" = the fifth Wednesday of the month. Note that if you specify "#5" and
   * there is not 5 of the given day-of-week in the month, then no firing will occur that month.
   *
   * The legal characters and the names of months and days of the week are not case sensitive.
   *
   *
NOTES:
   *
   *

   *
Support for specifying both a day-of-week and a day-of-month value is not complete
   *       (you'll need to use the '?' character in on of these fields).
   *

   *
   * @author Sharada Jambula, James House
   * @author Contributions from Mads Henderson
   * @author Refactoring from CronTrigger to CronExpression by Aaron Craven
   */
  public static class CronExpression implements Serializable, Cloneable {

    private static final long serialVersionUID = 12423409423L;
    protected static final int SECOND = 0;
    protected static final int MINUTE = 1;
    protected static final int HOUR = 2;
    protected static final int DAY_OF_MONTH = 3;
    protected static final int MONTH = 4;
    protected static final int DAY_OF_WEEK = 5;
    protected static final int YEAR = 6;
    protected static final int ALL_SPEC_INT = 99; // '*'
    protected static final int NO_SPEC_INT = 98; // '?'
    protected static final Integer ALL_SPEC = Integer.valueOf(ALL_SPEC_INT);
    protected static final Integer NO_SPEC = Integer.valueOf(NO_SPEC_INT);
    protected static Map<String, Integer> monthMap = new HashMap<String, Integer>(20);
    protected static Map<String, Integer> dayMap = new HashMap<String, Integer>(60);

    static {
      monthMap.put("JAN", Integer.valueOf(0));
      monthMap.put("FEB", Integer.valueOf(1));
      monthMap.put("MAR", Integer.valueOf(2));
      monthMap.put("APR", Integer.valueOf(3));
      monthMap.put("MAY", Integer.valueOf(4));
      monthMap.put("JUN", Integer.valueOf(5));
      monthMap.put("JUL", Integer.valueOf(6));
      monthMap.put("AUG", Integer.valueOf(7));
      monthMap.put("SEP", Integer.valueOf(8));
      monthMap.put("OCT", Integer.valueOf(9));
      monthMap.put("NOV", Integer.valueOf(10));
      monthMap.put("DEC", Integer.valueOf(11));

      dayMap.put("SUN", Integer.valueOf(1));
      dayMap.put("MON", Integer.valueOf(2));
      dayMap.put("TUE", Integer.valueOf(3));
      dayMap.put("WED", Integer.valueOf(4));
      dayMap.put("THU", Integer.valueOf(5));
      dayMap.put("FRI", Integer.valueOf(6));
      dayMap.put("SAT", Integer.valueOf(7));
    }

    private String cronExpression = null;
    private TimeZone timeZone = null;
    protected transient TreeSet<Integer> seconds;
    protected transient TreeSet<Integer> minutes;
    protected transient TreeSet<Integer> hours;
    protected transient TreeSet<Integer> daysOfMonth;
    protected transient TreeSet<Integer> months;
    protected transient TreeSet<Integer> daysOfWeek;
    protected transient TreeSet<Integer> years;
    protected transient boolean lastdayOfWeek = false;
    protected transient int nthdayOfWeek = 0;
    protected transient boolean lastdayOfMonth = false;
    protected transient boolean nearestWeekday = false;
    protected transient boolean expressionParsed = false;

    /**
     * Constructs a new CronExpression based on the specified parameter.
     *
     * @param cronExpression String representation of the cron expression the new object should
     *     represent
     * @throws java.text.ParseException if the string expression cannot be parsed into a valid
     *     CronExpression
     */
    public CronExpression(String cronExpression) throws ParseException {
      if (cronExpression == null) {
        throw new IllegalArgumentException("cronExpression cannot be null");
      }

      this.cronExpression = cronExpression;

      buildExpression(cronExpression.toUpperCase(Locale.US));
    }

    /**
     * Indicates whether the given date satisfies the cron expression. Note that milliseconds are
     * ignored, so two Dates falling on different milliseconds of the same second will always have
     * the same result here.
     *
     * @param date the date to evaluate
     * @return a boolean indicating whether the given date satisfies the cron expression
     */
    public boolean isSatisfiedBy(Date date) {
      Calendar testDateCal = Calendar.getInstance();
      testDateCal.setTime(date);
      testDateCal.set(Calendar.MILLISECOND, 0);
      Date originalDate = testDateCal.getTime();

      testDateCal.add(Calendar.SECOND, -1);

      Date timeAfter = getTimeAfter(testDateCal.getTime());

      return ((timeAfter != null) && (timeAfter.equals(originalDate)));
    }

    /**
     * Returns the next date/time after the given date/time which satisfies the cron
     * expression.
     *
     * @param date the date/time at which to begin the search for the next valid date/time
     * @return the next valid date/time
     */
    public Date getNextValidTimeAfter(Date date) {
      return getTimeAfter(date);
    }

    /**
     * Returns the next date/time after the given date/time which does not satisfy the
     * expression
     *
     * @param date the date/time at which to begin the search for the next invalid date/time
     * @return the next valid date/time
     */
    public Date getNextInvalidTimeAfter(Date date) {
      long difference = 1000;

      // move back to the nearest second so differences will be accurate
      Calendar adjustCal = Calendar.getInstance();
      adjustCal.setTime(date);
      adjustCal.set(Calendar.MILLISECOND, 0);
      Date lastDate = adjustCal.getTime();

      Date newDate = null;

      // keep getting the next included time until it's farther than one second
      // apart. At that point, lastDate is the last valid fire time. We return
      // the second immediately following it.
      while (difference == 1000) {
        newDate = getTimeAfter(lastDate);

        difference = newDate.getTime() - lastDate.getTime();

        if (difference == 1000) {
          lastDate = newDate;
        }
      }

      return new Date(lastDate.getTime() + 1000);
    }

    /**
     * Return the interval between the next valid date and the one after
     *
     * @param date the date/time at which to begin the search
     * @return the number of milliseconds between the next valid and the one after
     */
    public long getNextInterval(Date date) {
      Date nextValid = getNextValidTimeAfter(date);
      Date nextInvalid = getNextInvalidTimeAfter(nextValid);
      Date nextNextValid = getNextValidTimeAfter(nextInvalid);
      return nextNextValid.getTime() - nextValid.getTime();
    }

    /**
     * Returns the time zone for which this CronExpression will be resolved.
     *
     * @return timezone
     */
    public TimeZone getTimeZone() {
      if (timeZone == null) {
        timeZone = TimeZone.getDefault();
      }

      return timeZone;
    }

    /**
     * Sets the time zone for which this CronExpression will be resolved.
     *
     * @param timeZone the time zone.
     */
    public void setTimeZone(TimeZone timeZone) {
      this.timeZone = timeZone;
    }

    /**
     * Returns the string representation of the CronExpression
     *
     * @return a string representation of the CronExpression
     */
    @Override
    public String toString() {
      return cronExpression;
    }

    /**
     * Indicates whether the specified cron expression can be parsed into a valid cron expression
     *
     * @param cronExpression the expression to evaluate
     * @return a boolean indicating whether the given expression is a valid cron expression
     */
    public static boolean isValidExpression(String cronExpression) {

      try {
        new CronExpression(cronExpression);
      } catch (ParseException pe) {
        return false;
      }

      return true;
    }
    ////////////////////////////////////////////////////////////////////////////
    //
    // Expression Parsing Functions
    //
    ////////////////////////////////////////////////////////////////////////////
    protected void buildExpression(String expression) throws ParseException {
      expressionParsed = true;

      try {

        if (seconds == null) {
          seconds = new TreeSet<Integer>();
        }
        if (minutes == null) {
          minutes = new TreeSet<Integer>();
        }
        if (hours == null) {
          hours = new TreeSet<Integer>();
        }
        if (daysOfMonth == null) {
          daysOfMonth = new TreeSet<Integer>();
        }
        if (months == null) {
          months = new TreeSet<Integer>();
        }
        if (daysOfWeek == null) {
          daysOfWeek = new TreeSet<Integer>();
        }
        if (years == null) {
          years = new TreeSet<Integer>();
        }

        int exprOn = SECOND;

        StringTokenizer exprsTok = new StringTokenizer(expression, " \t", false);

        while (exprsTok.hasMoreTokens() && exprOn <= YEAR) {
          String expr = exprsTok.nextToken().trim();
          StringTokenizer vTok = new StringTokenizer(expr, ",");
          while (vTok.hasMoreTokens()) {
            String v = vTok.nextToken();
            storeExpressionVals(0, v, exprOn);
          }

          exprOn++;
        }

        if (exprOn <= DAY_OF_WEEK) {
          throw new ParseException("Unexpected end of expression.", expression.length());
        }

        if (exprOn <= YEAR) {
          storeExpressionVals(0, "*", YEAR);
        }

      } catch (ParseException pe) {
        throw pe;
      } catch (Exception e) {
        throw new ParseException("Illegal cron expression format (" + e.toString() + ")", 0);
      }
    }

    protected int storeExpressionVals(int pos, String s, int type) throws ParseException {

      int incr = 0;
      int i = skipWhiteSpace(pos, s);
      if (i >= s.length()) {
        return i;
      }
      char c = s.charAt(i);
      if ((c >= 'A') && (c <= 'Z') && (!s.equals("L")) && (!s.equals("LW"))) {
        String sub = s.substring(i, i + 3);
        int sval = -1;
        int eval = -1;
        if (type == MONTH) {
          sval = getMonthNumber(sub) + 1;
          if (sval < 0) {
            throw new ParseException("Invalid Month value: '" + sub + "'", i);
          }
          if (s.length() > i + 3) {
            c = s.charAt(i + 3);
            if (c == '-') {
              i += 4;
              sub = s.substring(i, i + 3);
              eval = getMonthNumber(sub) + 1;
              if (eval < 0) {
                throw new ParseException("Invalid Month value: '" + sub + "'", i);
              }
            }
          }
        } else if (type == DAY_OF_WEEK) {
          sval = getDayOfWeekNumber(sub);
          if (sval < 0) {
            throw new ParseException("Invalid Day-of-Week value: '" + sub + "'", i);
          }
          if (s.length() > i + 3) {
            c = s.charAt(i + 3);
            if (c == '-') {
              i += 4;
              sub = s.substring(i, i + 3);
              eval = getDayOfWeekNumber(sub);
              if (eval < 0) {
                throw new ParseException("Invalid Day-of-Week value: '" + sub + "'", i);
              }
              if (sval > eval) {
                throw new ParseException("Invalid Day-of-Week sequence: " + sval + " > " + eval, i);
              }
            } else if (c == '#') {
              try {
                i += 4;
                nthdayOfWeek = Integer.parseInt(s.substring(i));
                if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
                  throw new Exception();
                }
              } catch (Exception e) {
                throw new ParseException(
                    "A numeric value between 1 and 5 must follow the '#' option", i);
              }
            } else if (c == 'L') {
              lastdayOfWeek = true;
              i++;
            }
          }

        } else {
          throw new ParseException("Illegal characters for this position: '" + sub + "'", i);
        }
        if (eval != -1) {
          incr = 1;
        }
        addToSet(sval, eval, incr, type);
        return (i + 3);
      }

      if (c == '?') {
        i++;
        if ((i + 1) < s.length() && (s.charAt(i) != ' ' && s.charAt(i + 1) != '\t')) {
          throw new ParseException("Illegal character after '?': " + s.charAt(i), i);
        }
        if (type != DAY_OF_WEEK && type != DAY_OF_MONTH) {
          throw new ParseException("'?' can only be specified for Day-of-Month or Day-of-Week.", i);
        }
        if (type == DAY_OF_WEEK && !lastdayOfMonth) {
          int val = daysOfMonth.last().intValue();
          if (val == NO_SPEC_INT) {
            throw new ParseException(
                "'?' can only be specified for Day-of-Month -OR- Day-of-Week.", i);
          }
        }

        addToSet(NO_SPEC_INT, -1, 0, type);
        return i;
      }

      if (c == '*' || c == '/') {
        if (c == '*' && (i + 1) >= s.length()) {
          addToSet(ALL_SPEC_INT, -1, incr, type);
          return i + 1;
        } else if (c == '/'
            && ((i + 1) >= s.length() || s.charAt(i + 1) == ' ' || s.charAt(i + 1) == '\t')) {
          throw new ParseException("'/' must be followed by an integer.", i);
        } else if (c == '*') {
          i++;
        }
        c = s.charAt(i);
        if (c == '/') { // is an increment specified?
          i++;
          if (i >= s.length()) {
            throw new ParseException("Unexpected end of string.", i);
          }

          incr = getNumericValue(s, i);

          i++;
          if (incr > 10) {
            i++;
          }
          if (incr > 59 && (type == SECOND || type == MINUTE)) {
            throw new ParseException("Increment > 60 : " + incr, i);
          } else if (incr > 23 && (type == HOUR)) {
            throw new ParseException("Increment > 24 : " + incr, i);
          } else if (incr > 31 && (type == DAY_OF_MONTH)) {
            throw new ParseException("Increment > 31 : " + incr, i);
          } else if (incr > 7 && (type == DAY_OF_WEEK)) {
            throw new ParseException("Increment > 7 : " + incr, i);
          } else if (incr > 12 && (type == MONTH)) {
            throw new ParseException("Increment > 12 : " + incr, i);
          }
        } else {
          incr = 1;
        }

        addToSet(ALL_SPEC_INT, -1, incr, type);
        return i;
      } else if (c == 'L') {
        i++;
        if (type == DAY_OF_MONTH) {
          lastdayOfMonth = true;
        }
        if (type == DAY_OF_WEEK) {
          addToSet(7, 7, 0, type);
        }
        if (type == DAY_OF_MONTH && s.length() > i) {
          c = s.charAt(i);
          if (c == 'W') {
            nearestWeekday = true;
            i++;
          }
        }
        return i;
      } else if (c >= '0' && c <= '9') {
        int val = Integer.parseInt(String.valueOf(c));
        i++;
        if (i >= s.length()) {
          addToSet(val, -1, -1, type);
        } else {
          c = s.charAt(i);
          if (c >= '0' && c <= '9') {
            ValueSet vs = getValue(val, s, i);
            val = vs.value;
            i = vs.pos;
          }
          i = checkNext(i, s, val, type);
          return i;
        }
      } else {
        throw new ParseException("Unexpected character: " + c, i);
      }

      return i;
    }

    protected int checkNext(int pos, String s, int val, int type) throws ParseException {

      int end = -1;
      int i = pos;

      if (i >= s.length()) {
        addToSet(val, end, -1, type);
        return i;
      }

      char c = s.charAt(pos);

      if (c == 'L') {
        if (type == DAY_OF_WEEK) {
          lastdayOfWeek = true;
        } else {
          throw new ParseException("'L' option is not valid here. (pos=" + i + ")", i);
        }
        TreeSet<Integer> set = getSet(type);
        set.add(Integer.valueOf(val));
        i++;
        return i;
      }

      if (c == 'W') {
        if (type == DAY_OF_MONTH) {
          nearestWeekday = true;
        } else {
          throw new ParseException("'W' option is not valid here. (pos=" + i + ")", i);
        }
        TreeSet<Integer> set = getSet(type);
        set.add(Integer.valueOf(val));
        i++;
        return i;
      }

      if (c == '#') {
        if (type != DAY_OF_WEEK) {
          throw new ParseException("'#' option is not valid here. (pos=" + i + ")", i);
        }
        i++;
        try {
          nthdayOfWeek = Integer.parseInt(s.substring(i));
          if (nthdayOfWeek < 1 || nthdayOfWeek > 5) {
            throw new Exception();
          }
        } catch (Exception e) {
          throw new ParseException("A numeric value between 1 and 5 must follow the '#' option", i);
        }

        TreeSet<Integer> set = getSet(type);
        set.add(Integer.valueOf(val));
        i++;
        return i;
      }

      if (c == '-') {
        i++;
        c = s.charAt(i);
        int v = Integer.parseInt(String.valueOf(c));
        end = v;
        i++;
        if (i >= s.length()) {
          addToSet(val, end, 1, type);
          return i;
        }
        c = s.charAt(i);
        if (c >= '0' && c <= '9') {
          ValueSet vs = getValue(v, s, i);
          end = vs.value;
          i = vs.pos;
        }
        if (i < s.length() && ((c = s.charAt(i)) == '/')) {
          i++;
          int v2 = Integer.parseInt(String.valueOf(c));
          i++;
          if (i >= s.length()) {
            addToSet(val, end, v2, type);
            return i;
          }
          c = s.charAt(i);
          if (c >= '0' && c <= '9') {
            ValueSet vs = getValue(v2, s, i);
            int v3 = vs.value;
            addToSet(val, end, v3, type);
            i = vs.pos;
            return i;
          } else {
            addToSet(val, end, v2, type);
            return i;
          }
        } else {
          addToSet(val, end, 1, type);
          return i;
        }
      }

      if (c == '/') {
        i++;
        c = s.charAt(i);
        int v2 = Integer.parseInt(String.valueOf(c));
        i++;
        if (i >= s.length()) {
          addToSet(val, end, v2, type);
          return i;
        }
        c = s.charAt(i);
        if (c >= '0' && c <= '9') {
          ValueSet vs = getValue(v2, s, i);
          int v3 = vs.value;
          addToSet(val, end, v3, type);
          i = vs.pos;
          return i;
        } else {
          throw new ParseException("Unexpected character '" + c + "' after '/'", i);
        }
      }

      addToSet(val, end, 0, type);
      i++;
      return i;
    }

    public String getCronExpression() {
      return cronExpression;
    }

    public String getExpressionSummary() {
      StringBuilder buf = new StringBuilder();

      buf.append("seconds: ");
      buf.append(getExpressionSetSummary(seconds));
      buf.append("\n");
      buf.append("minutes: ");
      buf.append(getExpressionSetSummary(minutes));
      buf.append("\n");
      buf.append("hours: ");
      buf.append(getExpressionSetSummary(hours));
      buf.append("\n");
      buf.append("daysOfMonth: ");
      buf.append(getExpressionSetSummary(daysOfMonth));
      buf.append("\n");
      buf.append("months: ");
      buf.append(getExpressionSetSummary(months));
      buf.append("\n");
      buf.append("daysOfWeek: ");
      buf.append(getExpressionSetSummary(daysOfWeek));
      buf.append("\n");
      buf.append("lastdayOfWeek: ");
      buf.append(lastdayOfWeek);
      buf.append("\n");
      buf.append("nearestWeekday: ");
      buf.append(nearestWeekday);
      buf.append("\n");
      buf.append("NthDayOfWeek: ");
      buf.append(nthdayOfWeek);
      buf.append("\n");
      buf.append("lastdayOfMonth: ");
      buf.append(lastdayOfMonth);
      buf.append("\n");
      buf.append("years: ");
      buf.append(getExpressionSetSummary(years));
      buf.append("\n");

      return buf.toString();
    }

    protected String getExpressionSetSummary(java.util.Set<Integer> set) {

      if (set.contains(NO_SPEC)) {
        return "?";
      }
      if (set.contains(ALL_SPEC)) {
        return "*";
      }

      StringBuilder buf = new StringBuilder();

      Iterator<Integer> itr = set.iterator();
      boolean first = true;
      while (itr.hasNext()) {
        Integer iVal = itr.next();
        String val = iVal.toString();
        if (!first) {
          buf.append(",");
        }
        buf.append(val);
        first = false;
      }

      return buf.toString();
    }

    protected String getExpressionSetSummary(java.util.ArrayList<Integer> list) {

      if (list.contains(NO_SPEC)) {
        return "?";
      }
      if (list.contains(ALL_SPEC)) {
        return "*";
      }

      StringBuilder buf = new StringBuilder();

      Iterator<Integer> itr = list.iterator();
      boolean first = true;
      while (itr.hasNext()) {
        Integer iVal = itr.next();
        String val = iVal.toString();
        if (!first) {
          buf.append(",");
        }
        buf.append(val);
        first = false;
      }

      return buf.toString();
    }

    protected int skipWhiteSpace(int i, String s) {
      while (i < s.length() && (s.charAt(i) == ' ' || s.charAt(i) == '\t')) {
        i++;
      }

      return i;
    }

    protected int findNextWhiteSpace(int i, String s) {
      while (i < s.length() && (s.charAt(i) != ' ' || s.charAt(i) != '\t')) {
        i++;
      }

      return i;
    }

    protected void addToSet(int val, int end, int incr, int type) throws ParseException {

      TreeSet<Integer> set = getSet(type);

      if (type == SECOND || type == MINUTE) {
        if ((val < 0 || val > 59 || end > 59) && (val != ALL_SPEC_INT)) {
          throw new ParseException("Minute and Second values must be between 0 and 59", -1);
        }
      } else if (type == HOUR) {
        if ((val < 0 || val > 23 || end > 23) && (val != ALL_SPEC_INT)) {
          throw new ParseException("Hour values must be between 0 and 23", -1);
        }
      } else if (type == DAY_OF_MONTH) {
        if ((val < 1 || val > 31 || end > 31) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) {
          throw new ParseException("Day of month values must be between 1 and 31", -1);
        }
      } else if (type == MONTH) {
        if ((val < 1 || val > 12 || end > 12) && (val != ALL_SPEC_INT)) {
          throw new ParseException("Month values must be between 1 and 12", -1);
        }
      } else if (type == DAY_OF_WEEK) {
        if ((val == 0 || val > 7 || end > 7) && (val != ALL_SPEC_INT) && (val != NO_SPEC_INT)) {
          throw new ParseException("Day-of-Week values must be between 1 and 7", -1);
        }
      }

      if ((incr == 0 || incr == -1) && val != ALL_SPEC_INT) {
        if (val != -1) {
          set.add(Integer.valueOf(val));
        } else {
          set.add(NO_SPEC);
        }

        return;
      }

      int startAt = val;
      int stopAt = end;

      if (val == ALL_SPEC_INT && incr <= 0) {
        incr = 1;
        set.add(ALL_SPEC); // put in a marker, but also fill values
      }

      if (type == SECOND || type == MINUTE) {
        if (stopAt == -1) {
          stopAt = 59;
        }
        if (startAt == -1 || startAt == ALL_SPEC_INT) {
          startAt = 0;
        }
      } else if (type == HOUR) {
        if (stopAt == -1) {
          stopAt = 23;
        }
        if (startAt == -1 || startAt == ALL_SPEC_INT) {
          startAt = 0;
        }
      } else if (type == DAY_OF_MONTH) {
        if (stopAt == -1) {
          stopAt = 31;
        }
        if (startAt == -1 || startAt == ALL_SPEC_INT) {
          startAt = 1;
        }
      } else if (type == MONTH) {
        if (stopAt == -1) {
          stopAt = 12;
        }
        if (startAt == -1 || startAt == ALL_SPEC_INT) {
          startAt = 1;
        }
      } else if (type == DAY_OF_WEEK) {
        if (stopAt == -1) {
          stopAt = 7;
        }
        if (startAt == -1 || startAt == ALL_SPEC_INT) {
          startAt = 1;
        }
      } else if (type == YEAR) {
        if (stopAt == -1) {
          stopAt = 2099;
        }
        if (startAt == -1 || startAt == ALL_SPEC_INT) {
          startAt = 1970;
        }
      }

      for (int i = startAt; i <= stopAt; i += incr) {
        set.add(Integer.valueOf(i));
      }
    }

    protected TreeSet<Integer> getSet(int type) {
      switch (type) {
        case SECOND:
          return seconds;
        case MINUTE:
          return minutes;
        case HOUR:
          return hours;
        case DAY_OF_MONTH:
          return daysOfMonth;
        case MONTH:
          return months;
        case DAY_OF_WEEK:
          return daysOfWeek;
        case YEAR:
          return years;
        default:
          return null;
      }
    }

    protected ValueSet getValue(int v, String s, int i) {
      char c = s.charAt(i);
      StringBuilder s1 = new StringBuilder(String.valueOf(v));
      while (c >= '0' && c <= '9') {
        s1.append(c);
        i++;
        if (i >= s.length()) {
          break;
        }
        c = s.charAt(i);
      }
      ValueSet val = new ValueSet();

      val.pos = (i < s.length()) ? i : i + 1;
      val.value = Integer.parseInt(s1.toString());
      return val;
    }

    protected int getNumericValue(String s, int i) {
      int endOfVal = findNextWhiteSpace(i, s);
      String val = s.substring(i, endOfVal);
      return Integer.parseInt(val);
    }

    protected int getMonthNumber(String s) {
      Integer integer = monthMap.get(s);

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

      return integer.intValue();
    }

    protected int getDayOfWeekNumber(String s) {
      Integer integer = dayMap.get(s);

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

      return integer.intValue();
    }

    ////////////////////////////////////////////////////////////////////////////
    //
    // Computation Functions
    //
    ////////////////////////////////////////////////////////////////////////////
    protected Date getTimeAfter(Date afterTime) {

      Calendar cl = Calendar.getInstance(getTimeZone());

      // move ahead one second, since we're computing the time *after* the
      // given time
      afterTime = new Date(afterTime.getTime() + 1000);
      // CronTrigger does not deal with milliseconds
      cl.setTime(afterTime);
      cl.set(Calendar.MILLISECOND, 0);

      boolean gotOne = false;
      // loop until we've computed the next time, or we've past the endTime
      while (!gotOne) {

        // if (endTime != null && cl.getTime().after(endTime)) return null;

        SortedSet<Integer> st = null;
        int t = 0;

        int sec = cl.get(Calendar.SECOND);
        int min = cl.get(Calendar.MINUTE);

        // get second.................................................
        st = seconds.tailSet(Integer.valueOf(sec));
        if (st != null && st.size() != 0) {
          sec = st.first().intValue();
        } else {
          sec = seconds.first().intValue();
          min++;
          cl.set(Calendar.MINUTE, min);
        }
        cl.set(Calendar.SECOND, sec);

        min = cl.get(Calendar.MINUTE);
        int hr = cl.get(Calendar.HOUR_OF_DAY);
        t = -1;

        // get minute.................................................
        st = minutes.tailSet(Integer.valueOf(min));
        if (st != null && st.size() != 0) {
          t = min;
          min = st.first().intValue();
        } else {
          min = minutes.first().intValue();
          hr++;
        }
        if (min != t) {
          cl.set(Calendar.SECOND, 0);
          cl.set(Calendar.MINUTE, min);
          setCalendarHour(cl, hr);
          continue;
        }
        cl.set(Calendar.MINUTE, min);

        hr = cl.get(Calendar.HOUR_OF_DAY);
        int day = cl.get(Calendar.DAY_OF_MONTH);
        t = -1;

        // get hour...................................................
        st = hours.tailSet(Integer.valueOf(hr));
        if (st != null && st.size() != 0) {
          t = hr;
          hr = st.first().intValue();
        } else {
          hr = hours.first().intValue();
          day++;
        }
        if (hr != t) {
          cl.set(Calendar.SECOND, 0);
          cl.set(Calendar.MINUTE, 0);
          cl.set(Calendar.DAY_OF_MONTH, day);
          setCalendarHour(cl, hr);
          continue;
        }
        cl.set(Calendar.HOUR_OF_DAY, hr);

        day = cl.get(Calendar.DAY_OF_MONTH);
        int mon = cl.get(Calendar.MONTH) + 1;
        // '+ 1' because calendar is 0-based for this field, and we are
        // 1-based
        t = -1;
        int tmon = mon;

        // get day...................................................
        boolean dayOfMSpec = !daysOfMonth.contains(NO_SPEC);
        boolean dayOfWSpec = !daysOfWeek.contains(NO_SPEC);
        if (dayOfMSpec && !dayOfWSpec) { // get day by day of month rule
          st = daysOfMonth.tailSet(Integer.valueOf(day));
          if (lastdayOfMonth) {
            if (!nearestWeekday) {
              t = day;
              day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
            } else {
              t = day;
              day = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));

              java.util.Calendar tcal = java.util.Calendar.getInstance();
              tcal.set(Calendar.SECOND, 0);
              tcal.set(Calendar.MINUTE, 0);
              tcal.set(Calendar.HOUR_OF_DAY, 0);
              tcal.set(Calendar.DAY_OF_MONTH, day);
              tcal.set(Calendar.MONTH, mon - 1);
              tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));

              int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
              int dow = tcal.get(Calendar.DAY_OF_WEEK);

              if (dow == Calendar.SATURDAY && day == 1) {
                day += 2;
              } else if (dow == Calendar.SATURDAY) {
                day -= 1;
              } else if (dow == Calendar.SUNDAY && day == ldom) {
                day -= 2;
              } else if (dow == Calendar.SUNDAY) {
                day += 1;
              }

              tcal.set(Calendar.SECOND, sec);
              tcal.set(Calendar.MINUTE, min);
              tcal.set(Calendar.HOUR_OF_DAY, hr);
              tcal.set(Calendar.DAY_OF_MONTH, day);
              tcal.set(Calendar.MONTH, mon - 1);
              Date nTime = tcal.getTime();
              if (nTime.before(afterTime)) {
                day = 1;
                mon++;
              }
            }
          } else if (nearestWeekday) {
            t = day;
            day = daysOfMonth.first().intValue();

            java.util.Calendar tcal = java.util.Calendar.getInstance();
            tcal.set(Calendar.SECOND, 0);
            tcal.set(Calendar.MINUTE, 0);
            tcal.set(Calendar.HOUR_OF_DAY, 0);
            tcal.set(Calendar.DAY_OF_MONTH, day);
            tcal.set(Calendar.MONTH, mon - 1);
            tcal.set(Calendar.YEAR, cl.get(Calendar.YEAR));

            int ldom = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));
            int dow = tcal.get(Calendar.DAY_OF_WEEK);

            if (dow == Calendar.SATURDAY && day == 1) {
              day += 2;
            } else if (dow == Calendar.SATURDAY) {
              day -= 1;
            } else if (dow == Calendar.SUNDAY && day == ldom) {
              day -= 2;
            } else if (dow == Calendar.SUNDAY) {
              day += 1;
            }

            tcal.set(Calendar.SECOND, sec);
            tcal.set(Calendar.MINUTE, min);
            tcal.set(Calendar.HOUR_OF_DAY, hr);
            tcal.set(Calendar.DAY_OF_MONTH, day);
            tcal.set(Calendar.MONTH, mon - 1);
            Date nTime = tcal.getTime();
            if (nTime.before(afterTime)) {
              day = daysOfMonth.first().intValue();
              mon++;
            }
          } else if (st != null && st.size() != 0) {
            t = day;
            day = st.first().intValue();
          } else {
            day = daysOfMonth.first().intValue();
            mon++;
          }

          if (day != t || mon != tmon) {
            cl.set(Calendar.SECOND, 0);
            cl.set(Calendar.MINUTE, 0);
            cl.set(Calendar.HOUR_OF_DAY, 0);
            cl.set(Calendar.DAY_OF_MONTH, day);
            cl.set(Calendar.MONTH, mon - 1);
            // '- 1' because calendar is 0-based for this field, and we
            // are 1-based
            continue;
          }
        } else if (dayOfWSpec && !dayOfMSpec) { // get day by day of week rule
          if (lastdayOfWeek) {
            // are we looking for the last day of the month?
            int dow = daysOfWeek.first().intValue(); // desired
            // d-o-w
            int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
            int daysToAdd = 0;
            if (cDow < dow) {
              daysToAdd = dow - cDow;
            }
            if (cDow > dow) {
              daysToAdd = dow + (7 - cDow);
            }

            int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));

            if (day + daysToAdd > lDay) { // did we already miss the
              // last one?
              cl.set(Calendar.SECOND, 0);
              cl.set(Calendar.MINUTE, 0);
              cl.set(Calendar.HOUR_OF_DAY, 0);
              cl.set(Calendar.DAY_OF_MONTH, 1);
              cl.set(Calendar.MONTH, mon);
              // no '- 1' here because we are promoting the month
              continue;
            }

            // find date of last occurrence of this day in this month...
            while ((day + daysToAdd + 7) <= lDay) {
              daysToAdd += 7;
            }

            day += daysToAdd;

            if (daysToAdd > 0) {
              cl.set(Calendar.SECOND, 0);
              cl.set(Calendar.MINUTE, 0);
              cl.set(Calendar.HOUR_OF_DAY, 0);
              cl.set(Calendar.DAY_OF_MONTH, day);
              cl.set(Calendar.MONTH, mon - 1);
              // '- 1' here because we are not promoting the month
              continue;
            }

          } else if (nthdayOfWeek != 0) {
            // are we looking for the Nth day in the month?
            int dow = daysOfWeek.first().intValue(); // desired
            // d-o-w
            int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
            int daysToAdd = 0;
            if (cDow < dow) {
              daysToAdd = dow - cDow;
            } else if (cDow > dow) {
              daysToAdd = dow + (7 - cDow);
            }

            boolean dayShifted = false;
            if (daysToAdd > 0) {
              dayShifted = true;
            }

            day += daysToAdd;
            int weekOfMonth = day / 7;
            if (day % 7 > 0) {
              weekOfMonth++;
            }

            daysToAdd = (nthdayOfWeek - weekOfMonth) * 7;
            day += daysToAdd;
            if (daysToAdd < 0 || day > getLastDayOfMonth(mon, cl.get(Calendar.YEAR))) {
              cl.set(Calendar.SECOND, 0);
              cl.set(Calendar.MINUTE, 0);
              cl.set(Calendar.HOUR_OF_DAY, 0);
              cl.set(Calendar.DAY_OF_MONTH, 1);
              cl.set(Calendar.MONTH, mon);
              // no '- 1' here because we are promoting the month
              continue;
            } else if (daysToAdd > 0 || dayShifted) {
              cl.set(Calendar.SECOND, 0);
              cl.set(Calendar.MINUTE, 0);
              cl.set(Calendar.HOUR_OF_DAY, 0);
              cl.set(Calendar.DAY_OF_MONTH, day);
              cl.set(Calendar.MONTH, mon - 1);
              // '- 1' here because we are NOT promoting the month
              continue;
            }
          } else {
            int cDow = cl.get(Calendar.DAY_OF_WEEK); // current d-o-w
            int dow = daysOfWeek.first().intValue(); // desired
            // d-o-w
            st = daysOfWeek.tailSet(Integer.valueOf(cDow));
            if (st != null && st.size() > 0) {
              dow = st.first().intValue();
            }

            int daysToAdd = 0;
            if (cDow < dow) {
              daysToAdd = dow - cDow;
            }
            if (cDow > dow) {
              daysToAdd = dow + (7 - cDow);
            }

            int lDay = getLastDayOfMonth(mon, cl.get(Calendar.YEAR));

            if (day + daysToAdd > lDay) { // will we pass the end of
              // the month?
              cl.set(Calendar.SECOND, 0);
              cl.set(Calendar.MINUTE, 0);
              cl.set(Calendar.HOUR_OF_DAY, 0);
              cl.set(Calendar.DAY_OF_MONTH, 1);
              cl.set(Calendar.MONTH, mon);
              // no '- 1' here because we are promoting the month
              continue;
            } else if (daysToAdd > 0) { // are we swithing days?
              cl.set(Calendar.SECOND, 0);
              cl.set(Calendar.MINUTE, 0);
              cl.set(Calendar.HOUR_OF_DAY, 0);
              cl.set(Calendar.DAY_OF_MONTH, day + daysToAdd);
              cl.set(Calendar.MONTH, mon - 1);
              // '- 1' because calendar is 0-based for this field,
              // and we are 1-based
              continue;
            }
          }
        } else { // dayOfWSpec && !dayOfMSpec
          throw new UnsupportedOperationException(
              "Support for specifying both a day-of-week AND a day-of-month parameter is not implemented.");
        }
        cl.set(Calendar.DAY_OF_MONTH, day);

        mon = cl.get(Calendar.MONTH) + 1;
        // '+ 1' because calendar is 0-based for this field, and we are
        // 1-based
        int year = cl.get(Calendar.YEAR);
        t = -1;

        // test for expressions that never generate a valid fire date,
        // but keep looping...
        if (year > 2099) {
          return null;
        }

        // get month...................................................
        st = months.tailSet(Integer.valueOf(mon));
        if (st != null && st.size() != 0) {
          t = mon;
          mon = st.first().intValue();
        } else {
          mon = months.first().intValue();
          year++;
        }
        if (mon != t) {
          cl.set(Calendar.SECOND, 0);
          cl.set(Calendar.MINUTE, 0);
          cl.set(Calendar.HOUR_OF_DAY, 0);
          cl.set(Calendar.DAY_OF_MONTH, 1);
          cl.set(Calendar.MONTH, mon - 1);
          // '- 1' because calendar is 0-based for this field, and we are
          // 1-based
          cl.set(Calendar.YEAR, year);
          continue;
        }
        cl.set(Calendar.MONTH, mon - 1);
        // '- 1' because calendar is 0-based for this field, and we are
        // 1-based

        year = cl.get(Calendar.YEAR);
        t = -1;

        // get year...................................................
        st = years.tailSet(Integer.valueOf(year));
        if (st != null && st.size() != 0) {
          t = year;
          year = st.first().intValue();
        } else {
          return null; // ran out of years...
        }

        if (year != t) {
          cl.set(Calendar.SECOND, 0);
          cl.set(Calendar.MINUTE, 0);
          cl.set(Calendar.HOUR_OF_DAY, 0);
          cl.set(Calendar.DAY_OF_MONTH, 1);
          cl.set(Calendar.MONTH, 0);
          // '- 1' because calendar is 0-based for this field, and we are
          // 1-based
          cl.set(Calendar.YEAR, year);
          continue;
        }
        cl.set(Calendar.YEAR, year);

        gotOne = true;
      } // while( !done )

      return cl.getTime();
    }

    /**
     * Advance the calendar to the particular hour paying particular attention to daylight saving
     * problems.
     *
     * @param cal calendar
     * @param hour hour of day.
     */
    protected void setCalendarHour(Calendar cal, int hour) {
      cal.set(java.util.Calendar.HOUR_OF_DAY, hour);
      if (cal.get(java.util.Calendar.HOUR_OF_DAY) != hour && hour != 24) {
        cal.set(java.util.Calendar.HOUR_OF_DAY, hour + 1);
      }
    }

    /**
     * NOT YET IMPLEMENTED: Returns the time before the given time that the CronExpression
     *  matches.
     *
     * @param endTime end time
     * @return date
     */
    protected Date getTimeBefore(Date endTime) {
      throw new UnsupportedOperationException();
    }

    /**
     * NOT YET IMPLEMENTED: Returns the final time that the CronExpression will match.
     *
     * @return date
     */
    public Date getFinalFireTime() {
      throw new UnsupportedOperationException();
    }

    protected boolean isLeapYear(int year) {
      return ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
    }

    protected int getLastDayOfMonth(int monthNum, int year) {

      switch (monthNum) {
        case 1:
          return 31;
        case 2:
          return (isLeapYear(year)) ? 29 : 28;
        case 3:
          return 31;
        case 4:
          return 30;
        case 5:
          return 31;
        case 6:
          return 30;
        case 7:
          return 31;
        case 8:
          return 31;
        case 9:
          return 30;
        case 10:
          return 31;
        case 11:
          return 30;
        case 12:
          return 31;
        default:
          throw new IllegalArgumentException("Illegal month number: " + monthNum);
      }
    }

    private void readObject(java.io.ObjectInputStream stream)
        throws java.io.IOException, ClassNotFoundException {

      stream.defaultReadObject();
      try {
        buildExpression(cronExpression);
      } catch (Exception ignore) {
      } // never happens
    }

    @Override
    public Object clone() {
      CronExpression copy = null;
      try {
        copy = new CronExpression(getCronExpression());
        copy.setTimeZone(getTimeZone());
      } catch (ParseException ex) { // never happens since the source is valid...
        throw new IncompatibleClassChangeError("Not Cloneable.");
      }
      return copy;
    }
  }

  private static class ValueSet {

    public int value;
    public int pos;
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.libs.ws.ahc;

import akka.stream.Materializer;
import play.api.libs.ws.ahc.AhcWSClientConfig;
import play.api.libs.ws.ahc.cache.AhcHttpCache;
import play.api.libs.ws.ahc.cache.Cache;
import play.api.libs.ws.ahc.cache.CachingAsyncHttpClient;
import play.libs.ws.WSClient;
import play.libs.ws.WSRequest;
import play.shaded.ahc.org.asynchttpclient.AsyncHttpClient;

import javax.inject.Inject;
import java.io.IOException;

/**
 * A WS client backed by AsyncHttpClient implementation.
 *
 *
See https://www.playframework.com/documentation/latest/JavaWS for documentation.
 */
public class AhcWSClient implements WSClient {

  private final StandaloneAhcWSClient client;
  private final Materializer materializer;

  public AhcWSClient(AsyncHttpClient client, Materializer materializer) {
    this.client = new StandaloneAhcWSClient(client, materializer);
    this.materializer = materializer;
  }

  @Inject
  public AhcWSClient(StandaloneAhcWSClient client, Materializer materializer) {
    this.client = client;
    this.materializer = materializer;
  }

  /**
   * Creates WS client manually from configuration, internally creating a new instance of
   * AsyncHttpClient and managing its own thread pool.
   *
   *
This client is not managed as part of Play's lifecycle, and must be closed by calling
   * ws.close(), otherwise you will run into memory leaks.
   *
   * @param config a config object, usually from AhcWSClientConfigFactory
   * @param cache if not null, provides HTTP caching.
   * @param materializer an Akka materializer
   * @return a new instance of AhcWSClient.
   */
  public static AhcWSClient create(
      AhcWSClientConfig config, AhcHttpCache cache, Materializer materializer) {
    final StandaloneAhcWSClient client = StandaloneAhcWSClient.create(config, cache, materializer);
    return new AhcWSClient(client, materializer);
  }

  @Override
  public Object getUnderlying() {
    return client.getUnderlying();
  }

  @Override
  public play.api.libs.ws.WSClient asScala() {
    return new play.api.libs.ws.ahc.AhcWSClient(
        new play.api.libs.ws.ahc.StandaloneAhcWSClient(
            (AsyncHttpClient) client.getUnderlying(), materializer));
  }

  @Override
  public WSRequest url(String url) {
    final StandaloneAhcWSRequest plainWSRequest = client.url(url);
    return new AhcWSRequest(this, plainWSRequest);
  }

  @Override
  public void close() throws IOException {
    client.close();
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package javaguide.advanced.routing;

import org.junit.Before;
import org.junit.Test;

// #imports
import javax.inject.Inject;

import play.api.mvc.AnyContent;
import play.api.mvc.BodyParser;
import play.api.mvc.PlayBodyParsers;
import play.core.j.JavaContextComponents;
import play.mvc.Http;
import play.routing.Router;
import play.routing.RoutingDsl;
import java.util.concurrent.CompletableFuture;

import static play.mvc.Controller.*;
// #imports

import play.mvc.Result;
import play.test.WithApplication;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static play.test.Helpers.*;

public class JavaRoutingDsl extends WithApplication {

  private RoutingDsl routingDsl;

  @Before
  public void initializeRoutingDsl() {
    this.routingDsl = app.injector().instanceOf(RoutingDsl.class);
  }

  @Test
  public void simple() {
    // #simple
    Router router =
        routingDsl.GET("/hello/:to").routingTo((request, to) -> ok("Hello " + to)).build();
    // #simple

    assertThat(makeRequest(router, "GET", "/hello/world"), equalTo("Hello world"));
  }

  @Test
  public void fullPath() {
    // #full-path
    Router router =
        routingDsl.GET("/assets/*file").routingTo((request, file) -> ok("Serving " + file)).build();
    // #full-path

    assertThat(
        makeRequest(router, "GET", "/assets/javascripts/main.js"),
        equalTo("Serving javascripts/main.js"));
  }

  @Test
  public void regexp() {
    // #regexp
    Router router =
        routingDsl
            .GET("/api/items/$id<[0-9]+>")
            .routingTo((request, id) -> ok("Getting item " + id))
            .build();
    // #regexp

    assertThat(makeRequest(router, "GET", "/api/items/23"), equalTo("Getting item 23"));
  }

  @Test
  public void integer() {
    // #integer
    Router router =
        routingDsl
            .GET("/api/items/:id")
            .routingTo((Http.Request request, Integer id) -> ok("Getting item " + id))
            .build();
    // #integer

    assertThat(makeRequest(router, "GET", "/api/items/23"), equalTo("Getting item 23"));
  }

  @Test
  public void async() {
    // #async
    Router router =
        routingDsl
            .GET("/api/items/:id")
            .routingAsync(
                (Http.Request request, Integer id) ->
                    CompletableFuture.completedFuture(ok("Getting item " + id)))
            .build();
    // #async

    assertThat(makeRequest(router, "GET", "/api/items/23"), equalTo("Getting item 23"));
  }

  private String makeRequest(Router router, String method, String path) {
    Result result = routeAndCall(app, router, fakeRequest(method, path));
    if (result == null) {
      return null;
    } else {
      return contentAsString(result);
    }
  }

  // #inject
  public class MyComponent {

    private final RoutingDsl routingDsl;

    @Inject
    public MyComponent(RoutingDsl routing) {
      this.routingDsl = routing;
    }
  }
  // #inject

  @Test
  public void createNewRoutingDsl() {
    play.mvc.BodyParser.Default bodyParser =
        app.injector().instanceOf(play.mvc.BodyParser.Default.class);
    JavaContextComponents javaContextComponents =
        app.injector().instanceOf(JavaContextComponents.class);

    // #new-routing-dsl
    RoutingDsl routingDsl = new RoutingDsl(bodyParser, javaContextComponents);
    // #new-routing-dsl
    Router router =
        routingDsl.GET("/hello/:to").routingTo((request, to) -> ok("Hello " + to)).build();

    assertThat(makeRequest(router, "GET", "/hello/world"), equalTo("Hello world"));
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.inject.guice;

import play.api.inject.guice.GuiceableModule;
import play.libs.Scala;
import play.Application;
import play.ApplicationLoader;

/**
 * An ApplicationLoader that uses Guice to bootstrap the application.
 *
 *
Subclasses can override the builder and overrides methods.
 */
public class GuiceApplicationLoader implements ApplicationLoader {

  /** The initial builder to start construction from. */
  protected final GuiceApplicationBuilder initialBuilder;

  public GuiceApplicationLoader() {
    this(new GuiceApplicationBuilder());
  }

  public GuiceApplicationLoader(GuiceApplicationBuilder initialBuilder) {
    this.initialBuilder = initialBuilder;
  }

  @Override
  public final Application load(ApplicationLoader.Context context) {
    return builder(context).build();
  }

  /**
   * Construct a builder to use for loading the given context.
   *
   * @param context the context the returned builder will load
   * @return the builder
   */
  public GuiceApplicationBuilder builder(ApplicationLoader.Context context) {
    return initialBuilder
        .in(context.environment())
        .loadConfig(context.initialConfig())
        .overrides(overrides(context));
  }

  /**
   * Identify some bindings that should be used as overrides when loading an application using this
   * context. The default implementation of this method provides bindings that most applications
   * should include.
   *
   * @param context the context that should be searched for overrides
   * @return the bindings that should be used to override
   */
  protected GuiceableModule[] overrides(ApplicationLoader.Context context) {
    scala.collection.Seq<GuiceableModule> seq =
        play.api.inject.guice.GuiceApplicationLoader$.MODULE$.defaultOverrides(context.asScala());
    return Scala.asArray(GuiceableModule.class, seq);
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.mvc;

import play.i18n.Lang;
import play.i18n.MessagesApi;

import static play.mvc.Http.*;

/** Superclass for a Java-based controller. */
public abstract class Controller extends Results implements Status, HeaderNames {

  /**
   * Generates a 501 NOT_IMPLEMENTED simple result.
   *
   * @deprecated Deprecated as of 2.7.0. Use {@link #TODO(Request)} instead.
   */
  @Deprecated
  public static Result TODO() {
    return TODO(Http.Context.current().request());
  }

  /** Generates a 501 NOT_IMPLEMENTED simple result. */
  public static Result TODO(Request request) {
    return status(NOT_IMPLEMENTED, views.html.defaultpages.todo.render(request.asScala()));
  }

  /**
   * Returns the current HTTP context.
   *
   * @return the context
   * @deprecated Deprecated as of 2.7.0. See
   *     migration guide..
   */
  @Deprecated
  public static Context ctx() {
    return Http.Context.current();
  }

  /**
   * Returns the current HTTP request.
   *
   * @return the request
   * @deprecated Deprecated as of 2.7.0. See
   *     migration guide..
   */
  @Deprecated
  public static Request request() {
    return Http.Context.current().request();
  }

  /**
   * Returns the current lang.
   *
   * @return the language
   * @deprecated Deprecated as of 2.7.0. See
   *     migration guide..
   */
  @Deprecated
  public static Lang lang() {
    return Http.Context.current().lang();
  }

  /**
   * Change durably the lang for the current user
   *
   * @param code New lang code to use (e.g. "fr", "en-US", etc.)
   * @return true if the requested lang was supported by the application, otherwise false.
   * @deprecated Deprecated as of 2.7.0. Use {@link MessagesApi#setLang(Result, Lang)} or {@link
   *     Result#withLang(Lang, MessagesApi)}.
   */
  @Deprecated
  public static boolean changeLang(String code) {
    return Http.Context.current().changeLang(code);
  }

  /**
   * Change durably the lang for the current user
   *
   * @param lang New Lang object to use
   * @return true if the requested lang was supported by the application, otherwise false.
   * @deprecated Deprecated as of 2.7.0. Use {@link MessagesApi#setLang(Result, Lang)} or {@link
   *     Result#withLang(Lang, MessagesApi)}.
   */
  @Deprecated
  public static boolean changeLang(Lang lang) {
    return Http.Context.current().changeLang(lang);
  }

  /**
   * Clear the lang for the current user.
   *
   * @deprecated Deprecated as of 2.7.0. Use {@link MessagesApi#clearLang(Result)} or {@link
   *     Result#withoutLang(MessagesApi)}.
   */
  @Deprecated
  public static void clearLang() {
    Http.Context.current().clearLang();
  }

  /**
   * Returns the current HTTP response.
   *
   * @return the response
   * @deprecated Deprecated as of 2.7.0. Use {@link Result} instead.
   */
  @Deprecated
  public static Response response() {
    return Http.Context.current().response();
  }

  /**
   * Returns the current HTTP session.
   *
   * @return the session
   * @deprecated Deprecated as of 2.7.0. Use {@link Request#session()} and {@link Result} instead.
   */
  @Deprecated
  public static Session session() {
    return Http.Context.current().session();
  }

  /**
   * Puts a new value into the current session.
   *
   * @param key the key to set into the session
   * @param value the value to set for key
   * @deprecated Deprecated as of 2.7.0. Use {@link Result} instead.
   */
  @Deprecated
  public static void session(String key, String value) {
    session().put(key, value);
  }

  /**
   * Returns a value from the session.
   *
   * @param key the session key
   * @return the value for the provided key, or null if there was no value
   * @deprecated Deprecated as of 2.7.0. Use {@link Result} instead.
   */
  @Deprecated
  public static String session(String key) {
    return session().get(key);
  }

  /**
   * Returns the current HTTP flash scope.
   *
   * @return the flash scope
   * @deprecated Deprecated as of 2.7.0. Use {@link Request#flash()} and {@link Result} instead.
   */
  @Deprecated
  public static Flash flash() {
    return Http.Context.current().flash();
  }

  /**
   * Puts a new value into the flash scope.
   *
   * @param key the key to put into the flash scope
   * @param value the value corresponding to key
   * @deprecated Deprecated as of 2.7.0. Use {@link Result} instead.
   */
  @Deprecated
  public static void flash(String key, String value) {
    flash().put(key, value);
  }

  /**
   * Returns a value from the flash scope.
   *
   * @param key the key to look up in the flash scope
   * @return the value corresponding to key from the flash scope, or null if there was
   *     none
   * @deprecated Deprecated as of 2.7.0. Use {@link Result} instead.
   */
  @Deprecated
  public static String flash(String key) {
    return flash().get(key);
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package javaguide.http;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import play.ApplicationLoader;
import play.BuiltInComponentsFromContext;
import play.cache.AsyncCacheApi;
import play.cache.Cached;
import play.cache.caffeine.CaffeineCacheComponents;
import play.core.j.MappedJavaHandlerComponents;
import play.filters.components.NoHttpFiltersComponents;
import play.libs.Json;
import play.libs.typedmap.TypedKey;
import play.mvc.*;
import play.routing.Router;

import javax.inject.Inject;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.CompletionStage;

public class JavaActionsComposition extends Controller {

  private static final Logger log = LoggerFactory.getLogger(JavaActionsComposition.class);

  // #verbose-action
  public class VerboseAction extends play.mvc.Action.Simple {
    public CompletionStage<Result> call(Http.Request req) {
      log.info("Calling action for {}", req);
      return delegate.call(req);
    }
  }
  // #verbose-action

  // #verbose-index
  @With(VerboseAction.class)
  public Result verboseIndex() {
    return ok("It works!");
  }
  // #verbose-index

  // #authenticated-cached-index
  @Security.Authenticated
  @Cached(key = "index.result")
  public Result authenticatedCachedIndex() {
    return ok("It works!");
  }
  // #authenticated-cached-index

  // #verbose-annotation
  @With(VerboseAnnotationAction.class)
  @Target({ElementType.TYPE, ElementType.METHOD})
  @Retention(RetentionPolicy.RUNTIME)
  public @interface VerboseAnnotation {
    boolean value() default true;
  }
  // #verbose-annotation

  // #verbose-annotation-index
  @VerboseAnnotation(false)
  public Result verboseAnnotationIndex() {
    return ok("It works!");
  }
  // #verbose-annotation-index

  // #verbose-annotation-action
  public class VerboseAnnotationAction extends Action<VerboseAnnotation> {
    public CompletionStage<Result> call(Http.Request req) {
      if (configuration.value()) {
        log.info("Calling action for {}", req);
      }
      return delegate.call(req);
    }
  }
  // #verbose-annotation-action

  static class User {
    public static User findById(Integer id) {
      return new User();
    }
  }

  // #pass-arg-action
  // ###replace: public class Attrs {
  static class Attrs {
    public static final TypedKey<User> USER = TypedKey.<User>create("user");
  }

  public class PassArgAction extends play.mvc.Action.Simple {
    public CompletionStage<Result> call(Http.Request req) {
      return delegate.call(req.addAttr(Attrs.USER, User.findById(1234)));
    }
  }
  // #pass-arg-action

  // #pass-arg-action-index
  @With(PassArgAction.class)
  public static Result passArgIndex(Http.Request request) {
    User user = request.attrs().get(Attrs.USER);
    return ok(Json.toJson(user));
  }
  // #pass-arg-action-index

  // #annotated-controller
  @Security.Authenticated
  public class Admin extends Controller {
    /// ###insert: ...

  }
  // #annotated-controller

  // #action-composition-dependency-injection-annotation
  @With(MyOwnCachedAction.class)
  @Target({ElementType.TYPE, ElementType.METHOD})
  @Retention(RetentionPolicy.RUNTIME)
  public @interface WithCache {
    String key();
  }
  // #action-composition-dependency-injection-annotation

  // #action-composition-dependency-injection
  public class MyOwnCachedAction extends Action<WithCache> {

    private final AsyncCacheApi cacheApi;

    @Inject
    public MyOwnCachedAction(AsyncCacheApi cacheApi) {
      this.cacheApi = cacheApi;
    }

    @Override
    public CompletionStage<Result> call(Http.Request req) {
      return cacheApi.getOrElseUpdate(configuration.key(), () -> delegate.call(req));
    }
  }
  // #action-composition-dependency-injection

  // #action-composition-compile-time-di
  public class MyComponents extends BuiltInComponentsFromContext
      implements NoHttpFiltersComponents, CaffeineCacheComponents {

    public MyComponents(ApplicationLoader.Context context) {
      super(context);
    }

    @Override
    public Router router() {
      return Router.empty();
    }

    @Override
    public MappedJavaHandlerComponents javaHandlerComponents() {
      return super.javaHandlerComponents()
          // Add action that does not depends on any other component
          .addAction(VerboseAction.class, VerboseAction::new)
          // Add action that depends on the cache api
          .addAction(MyOwnCachedAction.class, () -> new MyOwnCachedAction(defaultCacheApi()));
    }
  }
  // #action-composition-compile-time-di
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.inject;

import scala.compat.java8.functionConverterImpls.FromJavaSupplier;
import scala.compat.java8.OptionConverters;

import javax.inject.Provider;
import java.lang.annotation.Annotation;
import java.util.Optional;
import java.util.function.Supplier;

/**
 * A binding key.
 *
 *
A binding key consists of a class and zero or more JSR-330 qualifiers.
 *
 *
See the {@link Module} class for information on how to provide bindings.
 */
public final class BindingKey<T> {
  private final play.api.inject.BindingKey<T> underlying;

  /**
   * A binding key.
   *
   *
A binding key consists of a class and zero or more JSR-330 qualifiers.
   *
   *
See the {@link Module} class for information on how to provide bindings.
   *
   * @param clazz The class to bind.
   * @param qualifier An optional qualifier.
   */
  public BindingKey(final Class<T> clazz, final Optional<QualifierAnnotation> qualifier) {
    this(
        play.api.inject.BindingKey.apply(
            clazz, OptionConverters.toScala(qualifier.map(QualifierAnnotation::asScala))));
  }

  public BindingKey(final play.api.inject.BindingKey<T> underlying) {
    this.underlying = underlying;
  }

  public BindingKey(final Class<T> clazz) {
    this(clazz, Optional.empty());
  }

  public Class<T> getClazz() {
    return underlying.clazz();
  }

  public Optional<QualifierAnnotation> getQualifier() {
    return OptionConverters.toJava(underlying.qualifier())
        .map(play.api.inject.QualifierAnnotation::asJava);
  }

  /**
   * Qualify this binding key with the given instance of an annotation.
   *
   *
This can be used to specify bindings with annotations that have particular values.
   */
  public  BindingKey<T> qualifiedWith(final A instance) {
    return underlying.qualifiedWith(instance).asJava();
  }

  /**
   * Qualify this binding key with the given annotation.
   *
   *
For example, you may have both a cached implementation, and a direct implementation of a
   * service. To differentiate between them, you may define a Cached annotation:
   *
   *
{@code
   * bindClass(Foo.class).qualifiedWith(Cached.class).to(FooCached.class),
   * bindClass(Foo.class).to(FooImpl.class)
   *
   * ...
   *
   * class MyController {
   *   {@literal @}Inject
   *   MyController({@literal @}Cached Foo foo) {
   *     ...
   *   }
   *   ...
   * }
   * }

   *
   * In the above example, the controller will get the cached {@code Foo} service.
   */
  public  BindingKey<T> qualifiedWith(final Class annotation) {
    return underlying.qualifiedWith(annotation).asJava();
  }

  /**
   * Qualify this binding key with the given name.
   *
   *
For example, you may have both a cached implementation, and a direct implementation of a
   * service. To differentiate between them, you may decide to name the cached one:
   *
   *
{@code
   * bindClass(Foo.class).qualifiedWith("cached").to(FooCached.class),
   * bindClass(Foo.class).to(FooImpl.class)
   *
   * ...
   *
   * class MyController {
   *   {@literal @}Inject
   *   MyController({@literal @}Named("cached") Foo foo) {
   *     ...
   *   }
   *   ...
   * }
   * }

   *
   * In the above example, the controller will get the cached `Foo` service.
   */
  public BindingKey<T> qualifiedWith(final String name) {
    return underlying.qualifiedWith(name).asJava();
  }

  /**
   * Bind this binding key to the given implementation class.
   *
   *
This class will be instantiated and injected by the injection framework.
   */
  public Binding<T> to(final Class<? extends T> implementation) {
    return underlying.to(implementation).asJava();
  }

  /**
   * Bind this binding key to the given provider instance.
   *
   *
This provider instance will be invoked to obtain the implementation for the key.
   */
  public Binding<T> to(final Provider<? extends T> provider) {
    return underlying.to(provider).asJava();
  }

  /** Bind this binding key to the given instance. */
  public  Binding<T> to(final Supplier instance) {
    return underlying.to(new FromJavaSupplier<>(instance)).asJava();
  }

  /** Bind this binding key to another binding key. */
  public Binding<T> to(final BindingKey<? extends T> key) {
    return underlying.to(key.asScala()).asJava();
  }

  /**
   * Bind this binding key to the given provider class.
   *
   *
The dependency injection framework will instantiate and inject this provider, and then
   * invoke its `get` method whenever an instance of the class is needed.
   */
  public <P extends Provider<? extends T>> Binding<T> toProvider(final Class
 provider) {
    return underlying.toProvider(provider).asJava();
  }

  /** Bind this binding key to the given instance. */
  public Binding<T> toInstance(final T instance) {
    return underlying.toInstance(instance).asJava();
  }

  /** Bind this binding key to itself. */
  public Binding<T> toSelf() {
    return underlying.toSelf().asJava();
  }

  @Override
  public String toString() {
    return underlying.toString();
  }

  public play.api.inject.BindingKey<T> asScala() {
    return underlying;
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.cache.caffeine;

import com.github.benmanes.caffeine.cache.Caffeine;
import com.typesafe.config.Config;

import java.util.Map;
import java.util.Objects;

/**
 * A configuration parser for the {@link Caffeine} builder.
 *
 *

 *
 *

 *
{@code initial-capacity=[integer]}: sets {@link Caffeine#initialCapacity}.
 *
{@code maximum-size=[long]}: sets {@link Caffeine#maximumSize}.
 *
{@code weak-keys}=[condition]: sets {@link Caffeine#weakKeys}.
 *
{@code weak-values}=[condition]: sets {@link Caffeine#weakValues}.
 *
{@code soft-values}=[condition]: sets {@link Caffeine#softValues}.
 *
{@code record-stats}=[condition]: sets {@link Caffeine#recordStats}.
 *

 *
 * It is illegal to use the following configurations together:
 *
 *

 *
{@code maximumSize} and {@code maximumWeight}
 *
{@code weakValues} and {@code softValues} set to {@code true}
 *

 *
 *
{@code CaffeineParser} does not support configuring {@code Caffeine} methods with non-value
 * parameters. These must be configured in code.
 */
public final class CaffeineParser {
  private final Caffeine<Object, Object> cacheBuilder;
  private final Config config;

  private CaffeineParser(Config config) {
    this.cacheBuilder = Caffeine.newBuilder();
    this.config = Objects.requireNonNull(config);
  }

  /** Returns a configured {@link Caffeine} cache builder. */
  public static Caffeine<Object, Object> from(Config config) {
    CaffeineParser parser = new CaffeineParser(config);
    config.entrySet().stream().map(Map.Entry::getKey).forEach(parser::parse);
    return parser.cacheBuilder;
  }

  private void parse(String key) {
    switch (key) {
      case "initial-capacity":
        if (!config.getIsNull(key)) {
          cacheBuilder.initialCapacity(config.getInt(key));
        }
        break;
      case "maximum-size":
        if (!config.getIsNull(key)) {
          cacheBuilder.maximumSize(config.getLong(key));
        }
        break;
      case "weak-keys":
        conditionally(key, cacheBuilder::weakKeys);
        break;
      case "weak-values":
        conditionally(key, cacheBuilder::weakValues);
        break;
      case "soft-values":
        conditionally(key, cacheBuilder::softValues);
        break;
      case "record-stats":
        conditionally(key, cacheBuilder::recordStats);
        break;
      default:
        break;
    }
  }

  private void conditionally(String key, Runnable action) {
    if (config.getBoolean(key)) {
      action.run();
    }
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.i18n;

import java.util.*;
import java.util.stream.Stream;

import play.Application;
import play.libs.*;
import scala.collection.immutable.Seq;

import static java.util.stream.Collectors.toList;

/** A Lang supported by the application. */
public class Lang extends play.api.i18n.Lang {

  public Lang(play.api.i18n.Lang underlyingLang) {
    super(underlyingLang.locale());
  }

  public Lang(java.util.Locale locale) {
    this(new play.api.i18n.Lang(locale));
  }

  /** A valid ISO Language Code. */
  public String language() {
    return locale().getLanguage();
  }

  /** A valid ISO Country Code. */
  public String country() {
    return locale().getCountry();
  }

  /** The script tag for this Lang */
  public String script() {
    return locale().getScript();
  }

  /** The variant tag for this Lang */
  public String variant() {
    return locale().getVariant();
  }

  /** The language tag (such as fr or en-US). */
  public String code() {
    return locale().toLanguageTag();
  }

  /** Convert to a Java Locale value. */
  public java.util.Locale toLocale() {
    return locale();
  }

  /**
   * Create a Lang value from a code (such as fr or en-US).
   *
   * @param code the language code
   * @return the Lang for the code, or null of no matching lang was found.
   */
  public static Lang forCode(String code) {
    try {
      return new Lang(play.api.i18n.Lang.apply(code));
    } catch (Exception e) {
      return null;
    }
  }

  /**
   * Retrieve Lang availables from the application configuration.
   *
   * @param app the current application.
   * @return the list of available Lang.
   */
  public static List<Lang> availables(Application app) {
    play.api.i18n.Langs langs = app.injector().instanceOf(play.api.i18n.Langs.class);
    List<play.api.i18n.Lang> availableLangs = Scala.asJava(langs.availables());
    return availableLangs.stream().map(Lang::new).collect(toList());
  }

  /**
   * Guess the preferred lang in the langs set passed as argument. The first Lang that matches an
   * available Lang wins, otherwise returns the first Lang available in this application.
   *
   * @param app the currept application
   * @param availableLangs the set of langs from which to guess the preferred
   * @return the preferred lang.
   */
  public static Lang preferred(Application app, List<Lang> availableLangs) {
    play.api.i18n.Langs langs = app.injector().instanceOf(play.api.i18n.Langs.class);
    Stream<Lang> stream = availableLangs.stream();
    List<play.api.i18n.Lang> langSeq =
        stream.map(l -> new play.api.i18n.Lang(l.toLocale())).collect(toList());
    return new Lang(langs.preferred(Scala.toSeq(langSeq)));
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.db.evolutions;

import play.api.db.evolutions.DatabaseEvolutions;
import play.db.Database;

import java.util.*;

/** Utilities for working with evolutions. */
public class Evolutions {

  /**
   * Create an evolutions reader that reads evolution files from this class's own classloader.
   *
   *
Only useful in simple classloading environments, such as when the classloader structure is
   * flat.
   *
   * @return the evolutions reader.
   */
  public static play.api.db.evolutions.EvolutionsReader fromClassLoader() {
    return fromClassLoader(Evolutions.class.getClassLoader());
  }

  /**
   * Create an evolutions reader that reads evolution files from a classloader.
   *
   * @param classLoader The classloader to read from.
   * @return the evolutions reader.
   */
  public static play.api.db.evolutions.EvolutionsReader fromClassLoader(ClassLoader classLoader) {
    return fromClassLoader(classLoader, "");
  }

  /**
   * Create an evolutions reader that reads evolution files from a classloader.
   *
   * @param classLoader The classloader to read from.
   * @param prefix A prefix that gets added to the resource file names, for example, this could be
   *     used to namespace evolutions in different environments to work with different databases.
   * @return the evolutions reader.
   */
  public static play.api.db.evolutions.EvolutionsReader fromClassLoader(
      ClassLoader classLoader, String prefix) {
    return new play.api.db.evolutions.ClassLoaderEvolutionsReader(classLoader, prefix);
  }

  /**
   * Create an evolutions reader based on a simple map of database names to evolutions.
   *
   * @param evolutions The map of database names to evolutions.
   * @return the evolutions reader.
   */
  public static play.api.db.evolutions.EvolutionsReader fromMap(
      Map<String, List<Evolution>> evolutions) {
    return new SimpleEvolutionsReader(evolutions);
  }

  /**
   * Create an evolutions reader for the default database from a list of evolutions.
   *
   * @param evolutions The list of evolutions.
   * @return the evolutions reader.
   */
  public static play.api.db.evolutions.EvolutionsReader forDefault(Evolution... evolutions) {
    Map<String, List<Evolution>> map = new HashMap<String, List<Evolution>>();
    map.put("default", Arrays.asList(evolutions));
    return fromMap(map);
  }

  /**
   * Apply evolutions for the given database.
   *
   * @param database The database to apply the evolutions to.
   * @param reader The reader to read the evolutions.
   * @param autocommit Whether autocommit should be used.
   * @param schema The schema where all the play evolution tables are saved in
   */
  public static void applyEvolutions(
      Database database,
      play.api.db.evolutions.EvolutionsReader reader,
      boolean autocommit,
      String schema) {
    DatabaseEvolutions evolutions = new DatabaseEvolutions(database.asScala(), schema);
    evolutions.evolve(evolutions.scripts(reader), autocommit);
  }

  /**
   * Apply evolutions for the given database.
   *
   * @param database The database to apply the evolutions to.
   * @param reader The reader to read the evolutions.
   * @param schema The schema where all the play evolution tables are saved in
   */
  public static void applyEvolutions(
      Database database, play.api.db.evolutions.EvolutionsReader reader, String schema) {
    applyEvolutions(database, reader, true, schema);
  }

  /**
   * Apply evolutions for the given database.
   *
   * @param database The database to apply the evolutions to.
   * @param reader The reader to read the evolutions.
   * @param autocommit Whether autocommit should be used.
   */
  public static void applyEvolutions(
      Database database, play.api.db.evolutions.EvolutionsReader reader, boolean autocommit) {
    applyEvolutions(database, reader, autocommit, "");
  }

  /**
   * Apply evolutions for the given database.
   *
   * @param database The database to apply the evolutions to.
   * @param reader The reader to read the evolutions.
   */
  public static void applyEvolutions(
      Database database, play.api.db.evolutions.EvolutionsReader reader) {
    applyEvolutions(database, reader, true);
  }

  /**
   * Apply evolutions for the given database.
   *
   * @param database The database to apply the evolutions to.
   * @param schema The schema where all the play evolution tables are saved in
   */
  public static void applyEvolutions(Database database, String schema) {
    applyEvolutions(database, fromClassLoader(), schema);
  }

  /**
   * Apply evolutions for the given database.
   *
   * @param database The database to apply the evolutions to.
   */
  public static void applyEvolutions(Database database) {
    applyEvolutions(database, "");
  }

  /**
   * Cleanup evolutions for the given database.
   *
   *
This will run the down scripts for all the applied evolutions.
   *
   * @param database The database to apply the evolutions to.
   * @param autocommit Whether autocommit should be used.
   * @param schema The schema where all the play evolution tables are saved in
   */
  public static void cleanupEvolutions(Database database, boolean autocommit, String schema) {
    DatabaseEvolutions evolutions = new DatabaseEvolutions(database.asScala(), schema);
    evolutions.evolve(evolutions.resetScripts(), autocommit);
  }

  /**
   * Cleanup evolutions for the given database.
   *
   *
This will run the down scripts for all the applied evolutions.
   *
   * @param database The database to apply the evolutions to.
   * @param autocommit Whether autocommit should be used.
   */
  public static void cleanupEvolutions(Database database, boolean autocommit) {
    cleanupEvolutions(database, autocommit, "");
  }

  /**
   * Cleanup evolutions for the given database.
   *
   *
This will run the down scripts for all the applied evolutions.
   *
   * @param database The database to apply the evolutions to.
   * @param schema The schema where all the play evolution tables are saved in
   */
  public static void cleanupEvolutions(Database database, String schema) {
    cleanupEvolutions(database, true, schema);
  }

  /**
   * Cleanup evolutions for the given database.
   *
   *
This will run the down scripts for all the applied evolutions.
   *
   * @param database The database to apply the evolutions to.
   */
  public static void cleanupEvolutions(Database database) {
    cleanupEvolutions(database, "");
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.inject.guice;

import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;

import com.typesafe.config.Config;
import play.Application;
import play.Environment;
import play.api.inject.guice.GuiceableModule;
import play.libs.Scala;

import static scala.compat.java8.JFunction.func;

public final class GuiceApplicationBuilder
    extends GuiceBuilder<GuiceApplicationBuilder, play.api.inject.guice.GuiceApplicationBuilder> {

  public GuiceApplicationBuilder() {
    this(new play.api.inject.guice.GuiceApplicationBuilder());
  }

  private GuiceApplicationBuilder(play.api.inject.guice.GuiceApplicationBuilder builder) {
    super(builder);
  }

  public static GuiceApplicationBuilder fromScalaBuilder(
      play.api.inject.guice.GuiceApplicationBuilder builder) {
    return new GuiceApplicationBuilder(builder);
  }

  /**
   * Set the initial configuration loader. Overrides the default or any previously configured
   * values.
   *
   * @param load the configuration loader
   * @return the configured application builder
   */
  public GuiceApplicationBuilder withConfigLoader(Function<Environment, Config> load) {
    return newBuilder(
        delegate.loadConfig(
            func(
                (play.api.Environment env) ->
                    new play.api.Configuration(load.apply(new Environment(env))))));
  }

  /**
   * Set the initial configuration. Overrides the default or any previously configured values.
   *
   * @param conf the configuration
   * @return the configured application builder
   */
  public GuiceApplicationBuilder loadConfig(Config conf) {
    return withConfigLoader(env -> conf);
  }

  /**
   * Set the module loader. Overrides the default or any previously configured values.
   *
   * @param loader the configuration
   * @return the configured application builder
   */
  public GuiceApplicationBuilder withModuleLoader(
      BiFunction<Environment, Config, List<GuiceableModule>> loader) {
    return newBuilder(
        delegate.load(
            func(
                (play.api.Environment env, play.api.Configuration conf) ->
                    Scala.toSeq(loader.apply(new Environment(env), conf.underlying())))));
  }

  /**
   * Override the module loader with the given guiceable modules.
   *
   * @param modules the set of overriding modules
   * @return an application builder that incorporates the overrides
   */
  public GuiceApplicationBuilder load(GuiceableModule... modules) {
    return newBuilder(delegate.load(Scala.varargs(modules)));
  }

  /**
   * Override the module loader with the given Guice modules.
   *
   * @param modules the set of overriding modules
   * @return an application builder that incorporates the overrides
   */
  public GuiceApplicationBuilder load(com.google.inject.Module... modules) {
    return load(Guiceable.modules(modules));
  }

  /**
   * Override the module loader with the given Play modules.
   *
   * @param modules the set of overriding modules
   * @return an application builder that incorporates the overrides
   */
  public GuiceApplicationBuilder load(play.api.inject.Module... modules) {
    return load(Guiceable.modules(modules));
  }

  /**
   * Override the module loader with the given Play bindings.
   *
   * @param bindings the set of binding override
   * @return an application builder that incorporates the overrides
   */
  public GuiceApplicationBuilder load(play.api.inject.Binding<?>... bindings) {
    return load(Guiceable.bindings(bindings));
  }

  /**
   * Create a new Play Application using this configured builder.
   *
   * @return the application
   */
  public Application build() {
    return injector().instanceOf(Application.class);
  }

  /**
   * Implementation of Self creation for GuiceBuilder.
   *
   * @return the application builder
   */
  protected GuiceApplicationBuilder newBuilder(
      play.api.inject.guice.GuiceApplicationBuilder builder) {
    return new GuiceApplicationBuilder(builder);
  }
}


/*
 * Copyright 2016 The Netty Project
 *
 * The Netty Project 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.
 */
package play.core.cookie.encoding;

import java.util.Iterator;

import static play.core.cookie.encoding.CookieUtil.*;

/**
 * A RFC6265 compliant cookie encoder to be used
 * client side, so only name=value pairs are sent.
 *
 *
Note that multiple cookies are supposed to be sent at once in a single "Cookie" header.
 *
 * @see ClientCookieDecoder
 */
public final class ClientCookieEncoder extends CookieEncoder {

  /**
   * Strict encoder that validates that name and value chars are in the valid scope defined in
   * RFC6265
   */
  public static final ClientCookieEncoder STRICT = new ClientCookieEncoder(true);

  /** Lax instance that doesn't validate name and value */
  public static final ClientCookieEncoder LAX = new ClientCookieEncoder(false);

  private ClientCookieEncoder(boolean strict) {
    super(strict);
  }

  /**
   * Encodes the specified cookie into a Cookie header value.
   *
   * @param name the cookie name
   * @param value the cookie value
   * @return a Rfc6265 style Cookie header value
   */
  public String encode(String name, String value) {
    return encode(new DefaultCookie(name, value));
  }

  /**
   * Encodes the specified cookie into a Cookie header value.
   *
   * @param cookie specified the cookie
   * @return a Rfc6265 style Cookie header value
   */
  public String encode(Cookie cookie) {
    if (cookie == null) {
      throw new NullPointerException("cookie");
    }
    StringBuilder buf = new StringBuilder();
    encode(buf, cookie);
    return stripTrailingSeparator(buf);
  }

  /**
   * Encodes the specified cookies into a single Cookie header value.
   *
   * @param cookies some cookies
   * @return a Rfc6265 style Cookie header value, null if no cookies are passed.
   */
  public String encode(Cookie... cookies) {
    if (cookies == null) {
      throw new NullPointerException("cookies");
    }
    if (cookies.length == 0) {
      return null;
    }

    StringBuilder buf = new StringBuilder();
    for (Cookie c : cookies) {
      if (c == null) {
        break;
      }

      encode(buf, c);
    }
    return stripTrailingSeparatorOrNull(buf);
  }

  /**
   * Encodes the specified cookies into a single Cookie header value.
   *
   * @param cookies some cookies
   * @return a Rfc6265 style Cookie header value, null if no cookies are passed.
   */
  public String encode(Iterable<? extends Cookie> cookies) {
    if (cookies == null) {
      throw new NullPointerException("cookies");
    }
    Iterator<? extends Cookie> cookiesIt = cookies.iterator();
    if (!cookiesIt.hasNext()) {
      return null;
    }

    StringBuilder buf = new StringBuilder();
    while (cookiesIt.hasNext()) {
      Cookie c = cookiesIt.next();
      if (c == null) {
        break;
      }

      encode(buf, c);
    }
    return stripTrailingSeparatorOrNull(buf);
  }

  private void encode(StringBuilder buf, Cookie c) {
    final String name = c.name();
    final String value = c.value() != null ? c.value() : "";

    validateCookie(name, value);

    if (c.wrap()) {
      addQuoted(buf, name, value);
    } else {
      add(buf, name, value);
    }
  }
}


/*
 * Copyright (C) 2009-2019 Lightbend Inc. <https://www.lightbend.com>
 */

package play.db.jpa;

import play.mvc.Http;

import javax.persistence.EntityManager;
import java.util.ArrayDeque;
import java.util.Deque;

/**
 * This is a deprecated class. An injected JPAApi instance should be used instead.
 *
 *
Please see Using
 * play.db.jpa.JPAApi for more details.
 *
 * @deprecated Use a dependency injected JPAApi instance here, since 2.7.0
 */
@Deprecated
public class JPAEntityManagerContext extends ThreadLocal<Deque<EntityManager>> {

  private static final String CURRENT_ENTITY_MANAGER = "entityManagerContext";

  @Override
  public Deque<EntityManager> initialValue() {
    return new ArrayDeque<>();
  }

  /**
   * Get the default EntityManager for this thread.
   *
   * @throws RuntimeException if no EntityManager is bound to the current Http.Context or the
   *     current Thread.
   * @return the EntityManager
   */
  public EntityManager em() {
    Deque<EntityManager> ems = this.emStack(true);

    if (ems.isEmpty()) {
      Http.Context.safeCurrent()
          .map(
              ctx -> {
                throw new RuntimeException(
                    "No EntityManager found in the context. Try to annotate your action method with @play.db.jpa.Transactional");
              })
          .orElseGet(
              () -> {
                throw new RuntimeException(
                    "No EntityManager bound to this thread. Try wrapping this call in JPAApi.withTransaction, or ensure that the HTTP context is setup on this thread.");
              });
    }

    return ems.peekFirst();
  }

  /**
   * Get the EntityManager stack.
   *
   * @param threadLocalFallback if true, fall back to a ThreadLocal queue of entity managers if no
   *     HTTP.Context object is found.
   * @return the queue of entity managers.
   */
  @SuppressWarnings("unchecked")
  public Deque<EntityManager> emStack(boolean threadLocalFallback) {
    return Http.Context.safeCurrent()
        .map(
            context -> {
              Object emsObject = context.args.get(CURRENT_ENTITY_MANAGER);
              if (emsObject != null) {
                return (Deque<EntityManager>) emsObject;
              } else {
                Deque<EntityManager> ems = new ArrayDeque<>();
                context.args.put(CURRENT_ENTITY_MANAGER, ems);
                return ems;
              }
            })
        .orElseGet(
            () -> {
              // Not a web request
              if (threadLocalFallback) {
                return this.get();
              } else {
                throw new RuntimeException(
                    "No Http.Context is present. If you want to invoke this method outside of a HTTP request, you need to wrap the call with JPA.withTransaction instead.");
              }
            });
  }

  public void push(EntityManager em, boolean threadLocalFallback) {
    Deque<EntityManager> ems = this.emStack(threadLocalFallback);
    if (em != null) {
      ems.push(em);
    }
  }

  public void pop(boolean threadLocalFallback) {
    Deque<EntityManager> ems = this.emStack(threadLocalFallback);
    if (ems.isEmpty()) {
      throw new IllegalStateException("Tried to remove the EntityManager, but none was set.");
    }
    ems.pop();
  }

  /**
   * Pushes or pops the EntityManager stack depending on the value of the em argument. If em is
   * null, then the current EntityManager is popped. If em is non-null, then em is pushed onto the
   * stack and becomes the current EntityManager.
   *
   * @param em the entity manager to push, if null then will pop one off the stack.
   * @param threadLocalFallback if true, fall back to a ThreadLocal queue of entity managers if no
   *     HTTP.Context object is found.
   */
  void pushOrPopEm(EntityManager em, boolean threadLocalFallback) {
    Deque<EntityManager> ems = this.emStack(threadLocalFallback);
    if (em != null) {
      ems.push(em);
    } else {
      if (ems.isEmpty()) {
        throw new IllegalStateException("Tried to remove the EntityManager, but none was set.");
      }
      ems.pop();
    }
  }
}