content: /*
   * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
   *
   * 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 org.activiti.engine.impl.calendar;
  
  import java.util.Date;
  import java.util.Map;
  import java.util.TimeZone;
  import java.util.concurrent.ConcurrentHashMap;
  import org.activiti.engine.ActivitiIllegalArgumentException;
  import org.activiti.engine.api.internal.Internal;
  import org.activiti.engine.runtime.ClockReader;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  
  /**
   * An Activiti BusinessCalendar for cycle based schedules that takes into account a different daylight savings time zone than the one that the server is configured for.
   * <p>
   * For CRON strings DSTZONE is used as the time zone that the CRON schedule refers to. Leave it out to use the server time zone.
   * <p>
   * For ISO strings the time zone offset for the date/time specified is part of the string itself. DSTZONE is used to determine what the offset should be NOW, which may be different than when the
   * workflow was scheduled if it is scheduled to run across a DST event.
   *
   * <pre>
   *   For example:
   *      R/2013-10-01T20:30:00/P1D DSTZONE:US/Eastern
   *      R/2013-10-01T20:30:00/P1D DSTZONE:UTC
   *      R/2013-10-01T20:30:00/P1D DSTZONE:US/Arizona
   *      0 30 20 ? * MON,TUE,WED,THU,FRI * DSTZONE:US/Eastern
   *      0 30 20 ? * MON,TUE,WED,THU,FRI * DSTZONE:UTC
   *      0 30 20 ? * MON,TUE,WED,THU,FRI * DSTZONE:US/Arizona
   * </pre>
   *
   * Removing the DSTZONE key will cause Activiti to use the server's time zone. This is the original behavior.
   * <p>
   * Schedule strings are versioned. Version 1 strings will use the original Activiti CycleBusinessCalendar. All new properties are ignored. Version 2 strings will use the new daylight saving time
   * logic.
   *
   * <pre>
   *   For example:
   *      R/2013-10-01T20:30:00/P1D VER:2 DSTZONE:US/Eastern
   *      0 30 20 ? * MON,TUE,WED,THU,FRI * VER:1 DSTZONE:US/Arizona
   * </pre>
   *
   * By default (if no VER key is included in the string), it assumes version 2. This can be changed by modifying the defaultScheduleVersion property.
   * <p>
   */
  @Internal
  public class AdvancedCycleBusinessCalendar extends CycleBusinessCalendar {
  
      private Integer defaultScheduleVersion;
  
      private static final Integer DEFAULT_VERSION = 2;
  
      private static final Logger logger = LoggerFactory.getLogger(AdvancedCycleBusinessCalendar.class);
  
      private static final Map<Integer, AdvancedSchedulerResolver> resolvers;
  
      static {
          resolvers = new ConcurrentHashMap<Integer, AdvancedSchedulerResolver>();
          resolvers.put(1, new AdvancedSchedulerResolverWithoutTimeZone());
          resolvers.put(2, new AdvancedSchedulerResolverWithTimeZone());
      }
  
      public AdvancedCycleBusinessCalendar(ClockReader clockReader) {
          super(clockReader);
      }
  
      public AdvancedCycleBusinessCalendar(ClockReader clockReader, Integer defaultScheduleVersion) {
          this(clockReader);
          this.defaultScheduleVersion = defaultScheduleVersion;
      }
  
      public Integer getDefaultScheduleVersion() {
          return defaultScheduleVersion == null ? DEFAULT_VERSION : defaultScheduleVersion;
      }
  
      public void setDefaultScheduleVersion(Integer defaultScheduleVersion) {
          this.defaultScheduleVersion = defaultScheduleVersion;
      }
  
      @Override
      public Date resolveDuedate(String duedateDescription, int maxIterations) {
          logger.info("Resolving Due Date: " + duedateDescription);
  
          String timeZone = getValueFrom("DSTZONE", duedateDescription);
          String version = getValueFrom("VER", duedateDescription);
  
          // START is a legacy value that is no longer used, but may still exist
          // in
          // deployed job schedules
          // Could be used in the future as a start date for a CRON job
          // String startDate = getValueFrom("START", duedateDescription);
  
          duedateDescription = removeValueFrom(
              "VER",
              removeValueFrom("START", removeValueFrom("DSTZONE", duedateDescription))
          ).trim();
  
          try {
              logger.info("Base Due Date: " + duedateDescription);
  
              Date date = resolvers
                  .get(version == null ? getDefaultScheduleVersion() : Integer.valueOf(version))
                  .resolve(
                      duedateDescription,
                      clockReader,
                      timeZone == null ? clockReader.getCurrentTimeZone() : TimeZone.getTimeZone(timeZone)
                  );
  
              logger.info("Calculated Date: " + (date == null ? "Will Not Run Again" : date));
  
              return date;
          } catch (Exception e) {
              throw new ActivitiIllegalArgumentException("Cannot parse duration", e);
          }
      }
  
      private String getValueFrom(String field, String duedateDescription) {
          int fieldIndex = duedateDescription.indexOf(field + ":");
  
          if (fieldIndex > -1) {
              int nextWhiteSpace = duedateDescription.indexOf(" ", fieldIndex);
  
              fieldIndex += field.length() + 1;
  
              if (nextWhiteSpace > -1) {
                  return duedateDescription.substring(fieldIndex, nextWhiteSpace);
              } else {
                  return duedateDescription.substring(fieldIndex);
              }
          }
  
          return null;
      }
  
      private String removeValueFrom(String field, String duedateDescription) {
          int fieldIndex = duedateDescription.indexOf(field + ":");
  
          if (fieldIndex > -1) {
              int nextWhiteSpace = duedateDescription.indexOf(" ", fieldIndex);
  
              if (nextWhiteSpace > -1) {
                  return duedateDescription.replace(duedateDescription.substring(fieldIndex, nextWhiteSpace), "");
              } else {
                  return duedateDescription.substring(0, fieldIndex);
              }
          }
  
          return duedateDescription;
      }
  }
  