TedeeCallbackRegistration.java
package org.sanmibuh.tedee.lock.infrastructure.secondary;
import static org.sanmibuh.tedee.lock.infrastructure.TedeeInfrastructureConfiguration.*;
import com.tedee.bridge.client.api.CallbackApi;
import com.tedee.bridge.client.model.CallbackDetails;
import com.tedee.bridge.client.model.CallbackDetailsNoId;
import com.tedee.bridge.client.model.CallbackHeader;
import java.time.Clock;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import lombok.extern.slf4j.Slf4j;
import org.jspecify.annotations.Nullable;
import org.sanmibuh.tedee.ServerProperties;
import org.sanmibuh.tedee.lock.infrastructure.TedeeProperties;
import org.springframework.context.SmartLifecycle;
import org.springframework.http.HttpMethod;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClientException;
import org.springframework.web.util.UriComponentsBuilder;
@Slf4j
@Component
final class TedeeCallbackRegistration implements SmartLifecycle {
private final CallbackApi callbackApi;
private final String callbackUrl;
private final String callbackSecret;
private final Clock clock;
private final TaskScheduler taskScheduler;
private final Duration registrationRetryInterval;
private boolean running;
private @Nullable Long registeredId;
TedeeCallbackRegistration(
final CallbackApi callbackApi,
final ServerProperties serverProperties,
final TedeeProperties tedeeProperties,
final Clock clock,
final TaskScheduler taskScheduler) {
this.callbackApi = callbackApi;
callbackSecret = tedeeProperties.callbackSecret();
this.clock = clock;
this.taskScheduler = taskScheduler;
registrationRetryInterval = tedeeProperties.callbackRegistrationInterval();
callbackUrl =
UriComponentsBuilder.fromUriString(serverProperties.publicUrl())
.path(EVENTS_PATH)
.toUriString();
}
@Override
public synchronized void start() {
running = true;
registerCallback();
}
private void registerCallback() {
try {
registeredId =
keepSingleExistingCallback()
.map(callback -> Objects.requireNonNull(callback.getId()))
.orElseGet(this::register);
} catch (final RestClientException exception) {
log.warn("Could not register callback {} on the Tedee Bridge", callbackUrl, exception);
scheduleRetry();
}
}
@SuppressWarnings("FutureReturnValueIgnored")
private void scheduleRetry() {
taskScheduler.schedule(this::reregister, clock.instant().plus(registrationRetryInterval));
}
synchronized void reregister() {
if (running) {
registerCallback();
}
}
private Optional<CallbackDetails> keepSingleExistingCallback() {
final var ourCallbacks = ourCallbacks();
ourCallbacks.stream()
.skip(1)
.forEach(duplicate -> delete(Objects.requireNonNull(duplicate.getId())));
return ourCallbacks.stream().findFirst();
}
private List<CallbackDetails> ourCallbacks() {
return callbackApi.getCallback().stream()
.filter(callback -> callbackUrl.equals(callback.getUrl()))
.toList();
}
private long register() {
final var registered =
callbackApi.postSingleCallback(
new CallbackDetailsNoId()
.url(callbackUrl)
.method(HttpMethod.POST.name())
.addHeadersItem(
new CallbackHeader()
.headerName(CALLBACK_SECRET_HEADER + ": " + callbackSecret)));
return Objects.requireNonNull(registered.getId());
}
private void delete(final long callbackId) {
callbackApi.deleteCallback(Math.toIntExact(callbackId));
}
@Override
public synchronized void stop() {
running = false;
if (registeredId != null) {
try {
delete(registeredId);
} catch (final RestClientException exception) {
log.warn("Could not unregister callback {} from the Tedee Bridge", callbackUrl, exception);
} finally {
registeredId = null;
}
}
}
@Override
public synchronized boolean isRunning() {
return running;
}
}