Java tutorial
/* * Copyright (c) 2014 by the author(s). * * 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.thevortex.lighting.jinks.robot; import java.io.IOException; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.TextStyle; import java.time.temporal.ChronoField; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAccessor; import java.util.Collections; import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; /** * How an event may recur. Assumes the start of a week is the same as the {@link java.time} classes, which is MONDAY. * * @author E. A. Graham Jr. */ @JsonDeserialize(using = Recurrence.RecurrenceDeserializer.class) @JsonSerialize(using = Recurrence.RecurrenceSerializer.class) public class Recurrence { /** * Format for start/end times with useless timezone and date information. */ public static final DateTimeFormatter ICAL_DT = new DateTimeFormatterBuilder().appendLiteral("TZID=") .parseLenient().appendZoneText(TextStyle.FULL).appendLiteral(':').appendValue(ChronoField.YEAR, 4) .appendValue(ChronoField.MONTH_OF_YEAR, 2).appendValue(ChronoField.DAY_OF_MONTH, 2).appendLiteral('T') .appendValue(ChronoField.HOUR_OF_DAY, 2).appendValue(ChronoField.MINUTE_OF_HOUR, 2) .appendValue(ChronoField.SECOND_OF_MINUTE, 2).toFormatter(); public static final String DTSTART = "DTSTART;"; public static final String DTEND = "DTEND;"; public static final String FREQ = "FREQ="; public static final String RRULE = "RRULE:"; public static final String BYDAY = "BYDAY="; protected LocalTime startTime; protected Duration duration; protected Frequency frequency; protected SortedSet<DayOfWeek> days = Collections.emptySortedSet(); /** * When this starts. */ public LocalTime getStartTime() { return startTime; } public void setStartTime(LocalTime startTime) { this.startTime = startTime; } /** * How long it lasts if it's not just a start. */ public Duration getDuration() { return duration; } public void setDuration(Duration duration) { this.duration = duration; } /** * How often. */ public Frequency getFrequency() { return frequency; } public void setFrequency(Frequency frequency) { this.frequency = frequency; } /** * If WEEKLY, which DOW */ public SortedSet<DayOfWeek> getDays() { return days; } public void setDays(SortedSet<DayOfWeek> days) { this.days = days; } public static enum Frequency { DAILY, WEEKLY } /** * Get the next occurrence from a time. * * @param fromWhen when * @return the next occurrence or {@code null} if there is no more */ public LocalDateTime nextOccurrence(TemporalAccessor fromWhen) { LocalDateTime from = LocalDateTime.from(fromWhen); // if it's not today, try the next day if (frequency == Frequency.WEEKLY && !days.contains(from.getDayOfWeek())) { return nextOccurrence(from.plusDays(1).truncatedTo(ChronoUnit.DAYS)); } // if we've already started, it's too late - next day if (from.toLocalTime().isAfter(startTime)) { return nextOccurrence(from.plusDays(1).truncatedTo(ChronoUnit.DAYS)); } // otherwise, we're on the right day, so just adjust the time return from.with(startTime).truncatedTo(ChronoUnit.MINUTES); } /** * Determine if the target is within the boundaries of this event. * * @param target the target time/date * @return {@code true} if the target is after the start time and within the duration; {@code false} if outside * the duration and/or there is no duration */ public boolean within(TemporalAccessor target) { if (duration == null) return false; LocalTime lt = LocalTime.from(target); return duration != null && lt.isAfter(startTime) && (Duration.between(startTime, lt).compareTo(duration) < 0); } /** * @return the ICAL-formatted string */ @Override public String toString() { StringBuilder formatted = new StringBuilder(DTSTART) .append(ZonedDateTime.now().with(startTime).truncatedTo(ChronoUnit.MINUTES).format(ICAL_DT)); // if we have a duration, there's an end if (duration != null) { formatted.append('\n').append(DTEND).append(ZonedDateTime.now().with(startTime.plus(duration)) .truncatedTo(ChronoUnit.MINUTES).format(ICAL_DT)); } // always a frequency formatted.append('\n').append(RRULE).append(FREQ).append(frequency); if (frequency == Frequency.WEEKLY) { // build the buffer of days StringBuilder dayBuilder = new StringBuilder(); boolean notFirst = false; DayOfWeek lastDay = null; for (DayOfWeek day : days) { if (notFirst) { dayBuilder.append(','); } notFirst = true; dayBuilder.append(day.name().substring(0, 2)); lastDay = day; } String formattedDays = dayBuilder.toString(); // if SUNDAY is at the end, move it to the front if (lastDay == DayOfWeek.SUNDAY) { formattedDays = "SU," + formattedDays.substring(0, formattedDays.lastIndexOf(",")); } // spit it out formatted.append(';').append(BYDAY).append(formattedDays); } return formatted.toString(); } @Override public boolean equals(Object o) { if (o == null) return false; if (this == o) return true; if (o instanceof Recurrence) { Recurrence that = (Recurrence) o; return Objects.equals(startTime, that.startTime) && Objects.equals(duration, that.duration) && Objects.equals(frequency, that.frequency) && Objects.equals(days, that.days); } return false; } @Override public int hashCode() { return Objects.hash(startTime, duration, frequency, days); } //================================================================================================================= /** * Parse from a partial ICAL string. * * @param recurrenceString the string * @return a thing */ public static Recurrence parse(String recurrenceString) { if (recurrenceString == null) return null; Recurrence result = new Recurrence(); // each major part will be on a separate line String[] parts = recurrenceString.split("\\n"); // first one MUST be a "DTSTART;" or we're already fucked String working = checkAndStrip(DTSTART, parts[0]); result.startTime = LocalTime.parse(working, ICAL_DT); // if there are three parts, the second must be an end time if (parts.length == 3) { working = checkAndStrip(DTEND, parts[1]); LocalTime endsAt = LocalTime.parse(working, ICAL_DT); result.duration = Duration.between(result.startTime, endsAt); } // we always have a RRULE: first part is FREQ and if it's weekly, followed by BYDAY working = checkAndStrip(RRULE, parts[parts.length - 1]); parts = working.split(";"); // the type is first working = checkAndStrip(FREQ, parts[0]); result.frequency = Frequency.valueOf(working); // if it's weekly, there's more if (result.frequency == Frequency.WEEKLY) { SortedSet<DayOfWeek> list = new TreeSet<>(); working = checkAndStrip(BYDAY, parts[1]); for (String dayValue : working.split(",")) { for (DayOfWeek dow : DayOfWeek.values()) { if (dow.name().startsWith(dayValue)) { list.add(dow); break; } } } result.days = list; } return result; } protected static String checkAndStrip(String token, String victim) { if (!victim.startsWith(token)) { throw new IllegalArgumentException( String.format("Expected token '%s' was not found in [%s]", token, victim)); } return victim.substring(token.length()); } // ================================================================================================================ public static class RecurrenceSerializer extends JsonSerializer<Recurrence> { @Override public void serialize(Recurrence value, JsonGenerator jgen, SerializerProvider provider) throws IOException { String text = value.toString(); // TODO not ready to write yet - need unit tests throw new UnsupportedOperationException(text); // jgen.writeString(text); } } public static class RecurrenceDeserializer extends JsonDeserializer<Recurrence> { @Override public Recurrence deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { return Recurrence.parse(jp.getValueAsString()); } } }