Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2022-2026 the original author or authors.
*
* 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
*
* https://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.springframework.modulith.moments;

import java.time.LocalDateTime;
import java.util.Objects;

import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;

/**
* A {@link DomainEvent} published on each minute.
*
* @author John Cunliffe
*/
public class MinuteHasPassed implements DomainEvent {

/**
* The minute that has just passed.
*/
private final LocalDateTime time;

/**
* Creates a new {@link MinuteHasPassed} for the given {@link LocalDateTime}.
*
* @param time must not be {@literal null}.
*/
private MinuteHasPassed(LocalDateTime time) {

Assert.notNull(time, "LocalDateTime must not be null!");

this.time = time;
}

/**
* Creates a new {@link MinuteHasPassed} for the given {@link LocalDateTime}.
*
* @param time must not be {@literal null}.
*/
public static MinuteHasPassed of(LocalDateTime time) {
return new MinuteHasPassed(time);
}

/**
* The minute that has just passed.
*
* @return will never be {@literal null}.
*/
public LocalDateTime getTime() {
return time;
}

/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {

if (this == obj) {
return true;
}

if (!(obj instanceof MinuteHasPassed that)) {
return false;
}

return Objects.equals(time, that.time);
}

/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(time);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2022-2026 the original author or authors.
*
* 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
*
* https://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.springframework.modulith.moments;

import java.time.LocalDateTime;
import java.util.Objects;

import org.jmolecules.event.types.DomainEvent;
import org.springframework.util.Assert;

/**
* A {@link DomainEvent} published on each second.
*
* @author John Cunliffe
*/
public class SecondHasPassed implements DomainEvent {

/**
* The second that has just passed.
*/
private final LocalDateTime time;

/**
* Creates a new {@link SecondHasPassed} for the given {@link LocalDateTime}.
*
* @param time must not be {@literal null}.
*/
private SecondHasPassed(LocalDateTime time) {

Assert.notNull(time, "LocalDateTime must not be null!");

this.time = time;
}

/**
* Creates a new {@link SecondHasPassed} for the given {@link LocalDateTime}.
*
* @param time must not be {@literal null}.
*/
public static SecondHasPassed of(LocalDateTime time) {
return new SecondHasPassed(time);
}

/**
* The second that has just passed.
*
* @return will never be {@literal null}.
*/
public LocalDateTime getTime() {
return time;
}

/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {

if (this == obj) {
return true;
}

if (!(obj instanceof SecondHasPassed that)) {
return false;
}

return Objects.equals(time, that.time);
}

/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return Objects.hash(time);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.modulith.moments.DayHasPassed;
import org.springframework.modulith.moments.HourHasPassed;
import org.springframework.modulith.moments.MinuteHasPassed;
import org.springframework.modulith.moments.MonthHasPassed;
import org.springframework.modulith.moments.QuarterHasPassed;
import org.springframework.modulith.moments.SecondHasPassed;
import org.springframework.modulith.moments.ShiftedQuarter;
import org.springframework.modulith.moments.WeekHasPassed;
import org.springframework.modulith.moments.YearHasPassed;
Expand All @@ -42,6 +44,7 @@
* Core component to publish passage-of-time events.
*
* @author Oliver Drotbohm
* @author John Cunliffe
*/
public class Moments implements Now {

Expand Down Expand Up @@ -72,6 +75,28 @@ public Moments(Clock clock, ApplicationEventPublisher events, MomentsProperties
this.properties = properties;
}

/**
* Triggers event publication every second.
*/
@Scheduled(cron = "* * * * * *")
void everySecond() {

if (properties.isSecondly()) {
emitSecondEventFor(now().minusSeconds(1).truncatedTo(ChronoUnit.SECONDS));
}
}

/**
* Triggers event publication every minute.
*/
@Scheduled(cron = "0 * * * * *")
void everyMinute() {

if (properties.isMinutely()) {
emitMinuteEventFor(now().minusMinutes(1).truncatedTo(ChronoUnit.MINUTES));
}
}

/**
* Triggers event publication every hour.
*/
Expand Down Expand Up @@ -102,15 +127,36 @@ Moments shiftBy(Duration duration) {
return this;
}

LocalDateTime current = before.truncatedTo(ChronoUnit.HOURS);
boolean secondly = properties.isSecondly();
boolean minutely = properties.isMinutely();
boolean hourly = properties.isHourly();

while (current.isBefore(after.truncatedTo(ChronoUnit.HOURS))) {

LocalDateTime next = hourly ? current.plusHours(1) : current.plusDays(1);

if (hourly) {
emitEventsFor(next);
ChronoUnit step = secondly ? ChronoUnit.SECONDS
: minutely ? ChronoUnit.MINUTES
: ChronoUnit.HOURS;

LocalDateTime current = before.truncatedTo(step);
LocalDateTime stop = after.truncatedTo(step);

while (current.isBefore(stop)) {

LocalDateTime next = current.plus(1, step);

if (secondly) {
emitSecondEventFor(next.minusSeconds(1).truncatedTo(ChronoUnit.SECONDS));
if (current.getMinute() != next.getMinute()) {
emitMinuteEventFor(current.truncatedTo(ChronoUnit.MINUTES));
}
if (current.getHour() != next.getHour()) {
emitEventsFor(current.truncatedTo(ChronoUnit.HOURS));
}
} else if (minutely) {
emitMinuteEventFor(next.minusMinutes(1).truncatedTo(ChronoUnit.MINUTES));
if (current.getHour() != next.getHour()) {
emitEventsFor(current.truncatedTo(ChronoUnit.HOURS));
}
} else if (hourly) {
emitEventsFor(current);
}

if (current.toLocalDate().isBefore(next.toLocalDate())) {
Expand Down Expand Up @@ -161,6 +207,14 @@ private void emitEventsFor(LocalDateTime time) {
events.publishEvent(HourHasPassed.of(time.truncatedTo(ChronoUnit.HOURS)));
}

private void emitMinuteEventFor(LocalDateTime time) {
events.publishEvent(MinuteHasPassed.of(time.truncatedTo(ChronoUnit.MINUTES)));
}

private void emitSecondEventFor(LocalDateTime time) {
events.publishEvent(SecondHasPassed.of(time.truncatedTo(ChronoUnit.SECONDS)));
}

private void emitEventsFor(LocalDate date) {

// Day has passed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
* Configuration properties for {@link Moments}.
*
* @author Oliver Drotbohm
* @author John Cunliffe
*/
@ConfigurationProperties(prefix = "spring.modulith.moments")
public class MomentsProperties {
Expand Down Expand Up @@ -135,7 +136,21 @@ public boolean isEnableTimeMachine() {
* Returns whether to create hourly events.
*/
boolean isHourly() {
return Granularity.HOURS.equals(granularity);
return granularity.isAtLeastAsFineAs(Granularity.HOURS);
}

/**
* Returns whether to create minutely events.
*/
boolean isMinutely() {
return granularity.isAtLeastAsFineAs(Granularity.MINUTES);
}

/**
* Returns whether to create per-second events.
*/
boolean isSecondly() {
return granularity.isAtLeastAsFineAs(Granularity.SECONDS);
}

/**
Expand Down Expand Up @@ -164,12 +179,24 @@ MomentsProperties withLocale(Locale locale) {
}

/**
* The granularity of events to publish.
* The granularity of events to publish. Ordered finest-first: an entry is "at least as fine as" any
* later entry, so {@code SECONDS} fires every supported event, {@code DAYS} fires only the daily and
* coarser ones.
*
* @author Oliver Drotbohm
*/
static enum Granularity {

/**
* Publish per-second events. Will include minute, hourly and daily events.
*/
SECONDS,

/**
* Publish per-minute events. Will include hourly and daily events.
*/
MINUTES,

/**
* Publish hourly events. Will include daily events.
*/
Expand All @@ -179,6 +206,10 @@ static enum Granularity {
* Publish daily events only.
*/
DAYS;

boolean isAtLeastAsFineAs(Granularity other) {
return this.ordinal() <= other.ordinal();
}
}

private static class ShiftedQuarters {
Expand Down
Loading