All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.spotify.styx.util.ParameterUtil Maven / Gradle / Ivy

/*-
 * -\-\-
 * Spotify Styx Scheduler Service
 * --
 * Copyright (C) 2016 Spotify AB
 * --
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * -/-/-
 */

package com.spotify.styx.util;

import static com.spotify.styx.model.Schedule.WellKnown.MONTHLY;
import static com.spotify.styx.model.Schedule.WellKnown.YEARLY;
import static java.lang.Integer.parseInt;
import static java.time.ZoneOffset.UTC;
import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;

import com.google.protobuf.Timestamp;
import com.spotify.styx.model.Schedule;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.format.ResolverStyle;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Helpers for working with {@link com.spotify.styx.model.Workflow} parameter handling.
 */
public final class ParameterUtil {

  private static final Pattern YEAR_PATTERN = Pattern.compile("(\\d{4})(-\\d{2})?");
  private static final Pattern YEAR_MONTH_PATTERN = Pattern.compile("(\\d{4})-(\\d{2})");
  private static final Logger LOG = LoggerFactory.getLogger(ParameterUtil.class);

  private static final int MIN_YEAR_WIDTH = 4;
  private static final int MAX_YEAR_WIDTH = 10;

  private static final DateTimeFormatter DATE_HOUR_FORMAT = new DateTimeFormatterBuilder()
      .append(DateTimeFormatter.ISO_LOCAL_DATE)
      .optionalStart()
      .appendLiteral('T')
      .appendValue(ChronoField.HOUR_OF_DAY, 2)
      .optionalStart()
      .appendLiteral(':')
      .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
      .optionalStart()
      .appendLiteral(':')
      .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
      .optionalStart()
      .appendLiteral('.')
      .appendValue(ChronoField.NANO_OF_SECOND, 1, 9, SignStyle.NORMAL)
      .optionalEnd()
      .optionalEnd()
      .optionalEnd()
      .optionalStart()
      .appendZoneOrOffsetId()
      .optionalEnd()
      .optionalEnd()
      .toFormatter()
      .withResolverStyle(ResolverStyle.STRICT)
      .withZone(UTC);

  private static final DateTimeFormatter ISO_LOCAL_YEAR = new DateTimeFormatterBuilder()
      .appendValue(ChronoField.YEAR, MIN_YEAR_WIDTH, MAX_YEAR_WIDTH, SignStyle.EXCEEDS_PAD)
      .toFormatter();

  private static final DateTimeFormatter ISO_LOCAL_MONTH = new DateTimeFormatterBuilder()
      .appendValue(ChronoField.YEAR, MIN_YEAR_WIDTH, MAX_YEAR_WIDTH, SignStyle.EXCEEDS_PAD)
      .appendLiteral('-')
      .appendValue(MONTH_OF_YEAR, 2)
      .toFormatter();

  private static final DateTimeFormatter ISO_LOCAL_DATE_HOUR = new DateTimeFormatterBuilder()
      .append(DateTimeFormatter.ISO_LOCAL_DATE)
      .appendLiteral('T')
      .appendValue(ChronoField.HOUR_OF_DAY, 2)
      .toFormatter();

  private ParameterUtil() {
    throw new UnsupportedOperationException();
  }

  public static Instant parseDate(String date) {
    return Instant.from(LocalDate.from(
        DateTimeFormatter.ISO_LOCAL_DATE.parse(date))
                            .atStartOfDay(UTC));
  }

  public static Instant parseDateHour(String dateHour) {
    return Instant.from(LocalDateTime.from(
        ISO_LOCAL_DATE_HOUR.parse(dateHour)).atOffset(UTC));
  }

  public static Instant parseDateMonth(String dateMonth) {
    return parseDate(dateMonth + "-01");
  }

  public static Instant parseDateYear(String dateYear) {
    return parseDate(dateYear + "-01-01");
  }

  public static String toParameter(Schedule schedule, Instant instant) {
    switch (schedule.wellKnown()) {
      case DAILY:
      case WEEKLY:
        return formatDate(instant);
      case HOURLY:
        return formatDateHour(instant);
      case MONTHLY:
        return formatMonth(instant);
      case YEARLY:
        return formatYear(instant);

      default:
        return formatDateTime(instant);
    }
  }

 public static Timestamp parseBest(String value) {
    List> functions = List.of(
        ParameterUtil::parseDate,
        ParameterUtil::parseDateHour,
        ParameterUtil::parseDateMonth,
        ParameterUtil::parseDateYear,
        Instant::parse
    );
    for (Function function : functions) {
      try {
        Instant instant = function.apply(value);
        return Timestamp.newBuilder().setSeconds(instant.getEpochSecond()).build();
      } catch (DateTimeParseException e) {}
    }
    var msg = "Could not parse: " + value;
    LOG.error(msg);
    throw new DateTimeException(msg);
  }

  public static Instant parseAlignedInstant(String dateHour, Schedule schedule) {
    final Schedule.WellKnown wellKnown = schedule.wellKnown();

    Matcher matcher;
    final ZonedDateTime parsed;
    if (wellKnown == YEARLY && (matcher = YEAR_PATTERN.matcher(dateHour)).matches()) {
      parsed = LocalDate.of(parseInt(matcher.group(1)), 1, 1).atStartOfDay(UTC);
    } else if (wellKnown == MONTHLY && (matcher = YEAR_MONTH_PATTERN.matcher(dateHour)).matches()) {
      parsed = LocalDate.of(parseInt(matcher.group(1)), parseInt(matcher.group(2)), 1).atStartOfDay(UTC);
    } else {
      parsed = tryParseDateHour(dateHour);
    }

    final Instant instant = parsed.toInstant();

    // test alignment
    if (!TimeUtil.isAligned(instant, schedule)) {
      return TimeUtil.previousInstant(instant, schedule);
    } else {
      return instant;
    }
  }

  private static ZonedDateTime tryParseDateHour(String dateTime) {
    try {
      try {
        return ZonedDateTime.parse(dateTime, DATE_HOUR_FORMAT);
      } catch (DateTimeParseException ignore) {
        return LocalDate.parse(dateTime, DATE_HOUR_FORMAT).atStartOfDay(UTC);
      }
    } catch (DateTimeParseException e) {
      throw new IllegalArgumentException(
          "Cannot parse time parameter " + dateTime + " - " + e.getMessage(),
          e);
    }
  }

  private static String formatDateTime(Instant instant) {
    return ISO_OFFSET_DATE_TIME.format(
        instant.truncatedTo(ChronoUnit.MINUTES)
            .atOffset(UTC));
  }

  private static String formatDate(Instant instant) {
    return DateTimeFormatter.ISO_LOCAL_DATE.format(
        instant.atOffset(UTC));
  }

  private static String formatDateHour(Instant instant) {
    return ISO_LOCAL_DATE_HOUR.format(
        instant.truncatedTo(ChronoUnit.HOURS)
            .atOffset(UTC));
  }

  private static String formatMonth(Instant instant) {
    return ISO_LOCAL_MONTH.format(
        instant.truncatedTo(ChronoUnit.DAYS)
            .atOffset(UTC));
  }

  private static String formatYear(Instant instant) {
    return ISO_LOCAL_YEAR.format(
        instant.truncatedTo(ChronoUnit.DAYS)
            .atOffset(UTC));
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy