// Copyright 2006 Google Inc. // // 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.google.enterprise.connector.scheduler; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.common.base.Strings; import com.google.enterprise.connector.spi.TraversalSchedule; import java.util.Calendar; import java.util.TreeSet; /** * A traversal schedule. */ public class Schedule implements TraversalSchedule { private static int defaultRetryDelayMillis = (5 * 60 * 1000); private String connectorName; private boolean disabled; private int load; private int retryDelayMillis; // maximum of ~24 days private String timeIntervals; private ScheduleTimeInterval[] scheduleIntervals; /* * TODO: Either formalize the versions of serialized {@code Schedule} strings, * or convert the serialized format to XML (or both). * The current Schedule versions are: * <ul> * <li>0 - Unknown</li> * <li>1 - <code>connectorName:hostLoad:timeIntervals...</code></li> * <li>2 - <code>connectorName:hostLoad:retryDelayMillis:timeIntervals...</code> * adds retryDelayMillis.</li> * <li>3 - <code>#connectorName:hostLoad:retryDelayMillis:timeIntervals...</code> * where leading '#' indicates disabled schedule, and a * retryDelayMillis value of -1 indicates traverse to until * no new content, then automatically disable.</li> * </ul> */ public static final String CURRENT_VERSION = "3"; /** * Signal to the Traverser that it should traverse the ECM repository * until there is not new content, then stop. */ public static final int POLLING_DISABLED = -1; /** * Construct a disabled Schedule, with otherwise default values. */ public Schedule() { // Note that GSA's have difficulties with schedules that contain no time // intervals. So even though this is disabled, it has the default 0-0 time // interval. this(null, true, HostLoadManager.DEFAULT_HOST_LOAD, defaultRetryDelayMillis, "0-0"); } /** * Construct a Schedule for a given Connector. * * @param connectorName * @param disabled true if this schedule is currently disabled * @param load The hostload (in docs per minute) as an integer * @param retryDelayMillis Time to wait before next traversal (milliseconds) * @param timeIntervals Time intervals string in the format of "1-2:3-8" */ public Schedule(String connectorName, boolean disabled, int load, int retryDelayMillis, String timeIntervals) { this.connectorName = connectorName; this.load = load; this.disabled = disabled; this.retryDelayMillis = retryDelayMillis; setTimeIntervals(timeIntervals); } /** * Create a schedule object. * * @param schedule String readable by readString() method */ public Schedule(String schedule) { Preconditions.checkArgument(!Strings.isNullOrEmpty(schedule), "Schedule string may not be null or empty."); readString(schedule); } /** * Set the default RetryDelayMillisecs. * * @param defaultValue default value for retryDelay in seconds. */ public static void setDefaultRetryDelaySecs(int defaultValue) { defaultRetryDelayMillis = defaultValue * 1000; } /** * Return the default retryDelayMillis value. * This can be defined in the Context by specifying * TraversalDelaySecondsDefault value. */ public static int defaultRetryDelayMillis() { return defaultRetryDelayMillis; } /** * Factory method for optionally creating a Schedule from a string. * * @param schedule A stringified Schedule, may be null or empty. * @return a Schedule parsed from the supplied schedule string, or * {@code null} if that string was null or empty. */ public static Schedule of(String schedule) { return (Strings.isNullOrEmpty(schedule)) ? null : new Schedule(schedule); } /** * Returns a string representation of the supplied Schedule. * If the supplied Schedule is {@code null}, then return a string * representation of a default, disabled schedule. This is for * the benefit of GSA's that cannot handle a {@code null} Schedule. * * @param schedule a Schedule, may be null. * @return string representation of schedule */ public static String toString(Schedule schedule) { return ((schedule == null) ? new Schedule() : schedule).toString(); } /** * Return a legacy representation of the supplied schedule. * Legacy schedules do not have a delay field or disabled flag. * Only sent to a GSA that does not understand the delay field. * * @param scheduleStr a schedule string. * @return a schedule string without the delay field or disabled flag. */ public static String toLegacyString(String scheduleStr) { Schedule schedule = Strings.isNullOrEmpty(scheduleStr) ? new Schedule() : new Schedule(scheduleStr); return (schedule.connectorName + ":" + schedule.load + ":" + schedule.getTimeIntervals()); } /** * Populate a schedule. * * @param schedule String of the form: * <connectorName>:<load>:<retryDelayMillis>:<timeIntervals> * OR * <connectorName>:<load>:<timeIntervals> * e.g. "connector1:60:86400000:1-2:3-5", "connector1:60:1-2:3-5" */ public void readString(String schedule) { try { String[] strs = schedule.trim().split(":", 4); if (strs[0].charAt(0) == '#') { disabled = true; connectorName = strs[0].substring(1); } else { connectorName = strs[0]; } load = Integer.parseInt(strs[1]); String intervals; if ((strs.length > 3) && (strs[2].indexOf('-') <= 0)) { retryDelayMillis = Integer.parseInt(strs[2]); intervals = strs[3]; } else { // This is a legacy string without the retryDelay. Resplit. retryDelayMillis = defaultRetryDelayMillis; strs = schedule.trim().split(":", 3); intervals = strs[2]; } setTimeIntervals(intervals); } catch(Exception e) { throw new IllegalArgumentException("Invalid schedule string format: \"" + schedule + "\""); } } /** * Parse a string of time intervals. The returned structure is designed for * fast processing by {@link nextScheduledInterval()}. * * @param intervals String of the form e.g. "1-2:3-5:14-18" etc. * @return a non-null array of ScheduleTimeInterval objects ordered by start * time */ private static ScheduleTimeInterval[] parseTimeIntervals(String intervals) { if (intervals.length() == 0) { return new ScheduleTimeInterval[0]; } TreeSet<ScheduleTimeInterval> timeIntervals = new TreeSet<ScheduleTimeInterval> (); for (String interval : intervals.trim().split(":")) { String[] startEndTime = interval.split("-"); int startTime = Integer.parseInt(startEndTime[0]); int endTime = Integer.parseInt(startEndTime[1]); if (endTime == 0) { endTime = 24; } else if (startTime == endTime) { // Legacy disabled schedule, e.g. "1-1". continue; } if (endTime < startTime) { // Interval wraps midnight, split it in two. timeIntervals.add(new ScheduleTimeInterval(0, endTime)); timeIntervals.add(new ScheduleTimeInterval(startTime, 24)); } else { timeIntervals.add(new ScheduleTimeInterval(startTime, endTime)); } } if (!timeIntervals.isEmpty()) { // Add the first interval for tomorrow to the end of the list // for the benefit of nextScheduledInterval. ScheduleTimeInterval first = timeIntervals.first(); timeIntervals.add(new ScheduleTimeInterval(first.startTime + 24, first.endTime + 24)); } return timeIntervals.toArray(new ScheduleTimeInterval[0]); } /** * @return String of the form: e.g. "connector1:500:30000:1-2:3-5" */ @Override public String toString() { StringBuilder buf = new StringBuilder(); if (disabled) { buf.append('#'); } buf.append(Strings.nullToEmpty(connectorName)); buf.append(":" + load); buf.append(":" + retryDelayMillis); buf.append(":" + getTimeIntervals()); return buf.toString(); } public String getConnectorName() { return connectorName; } public void setConnectorName(String connectorName) { this.connectorName = connectorName; } @Override public int getTraversalRate() { return getLoad(); } public int getLoad() { return load; } public void setLoad(int load) { this.load = load; } public int getRetryDelayMillis() { return retryDelayMillis; } @Override public int getRetryDelay() { return retryDelayMillis / 1000; } public void setRetryDelayMillis(int retryDelayMillis) { this.retryDelayMillis = retryDelayMillis; } @Override public boolean isDisabled() { return disabled; } public void setDisabled(boolean disabled) { this.disabled = disabled; } public void setTimeIntervals(String timeIntervals) { this.timeIntervals = (timeIntervals == null) ? "" : timeIntervals.trim(); this.scheduleIntervals = parseTimeIntervals(this.timeIntervals); } /** * @return String of the form: e.g. "1-2:3-5", * or empty string if Schedule has no timeIntervals. */ public String getTimeIntervals() { return timeIntervals; } /** * Return {@code true} if the current time is within a scheduled traversal * interval; {@code false} otherwise. */ @Override public boolean inScheduledInterval() { return nextScheduledInterval(Calendar.getInstance()) == 0; } /** * Returns the number of seconds until the next scheduled traversal interval. * A return value of 0 (zero) indicates the current time is within a scheduled * traversal interval. A returned value of -1 indicates there is no next * traversal interval. */ @Override public int nextScheduledInterval() { return nextScheduledInterval(Calendar.getInstance()); } @VisibleForTesting int nextScheduledInterval(Calendar now) { int hour = now.get(Calendar.HOUR_OF_DAY); for (ScheduleTimeInterval interval : scheduleIntervals) { if ((hour >= interval.startTime) && (hour < interval.endTime)) { return 0; } else if (hour < interval.startTime) { return (interval.startTime - hour) * 3600 - now.get(Calendar.MINUTE) * 60; } } return -1; } /** * Return {@code true} if this Schedule would allow traversals to run * at this time; {@code false} otherwise. */ @Override public boolean shouldRun() { return !isDisabled() && inScheduledInterval(); } /** * Returns a hash code value for the object. * * @return a hash code value for this object */ @Override public int hashCode() { return toString().hashCode(); } /** * Indicates whether some other object is "equal to" this one. * * @return {@code true} if this object is the same as the {@code obj} * argument; {@code false} otherwise */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } Schedule other = (Schedule) obj; return toString().equals(other.toString()); } /** * An interval of time used for schedules. */ private static class ScheduleTimeInterval implements Comparable<ScheduleTimeInterval> { public final int startTime; public final int endTime; public ScheduleTimeInterval(int startTime, int endTime) { this.startTime = startTime; this.endTime = endTime; } @Override public int compareTo(ScheduleTimeInterval o) { if (o == null) { return 1; } int compare = startTime - o.startTime; if (compare == 0) { compare = endTime - o.endTime; } return compare; } } }