001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2024 Richard "Shred" Körber 005 * http://acme4j.shredzone.org 006 * 007 * Licensed under the Apache License, Version 2.0 (the "License"); 008 * you may not use this file except in compliance with the License. 009 * 010 * This program is distributed in the hope that it will be useful, 011 * but WITHOUT ANY WARRANTY; without even the implied warranty of 012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 013 */ 014package org.shredzone.acme4j; 015 016import static java.time.Instant.now; 017 018import java.time.Duration; 019import java.time.Instant; 020import java.time.temporal.ChronoUnit; 021import java.util.Objects; 022import java.util.Optional; 023import java.util.Set; 024 025import org.shredzone.acme4j.exception.AcmeException; 026 027/** 028 * Marks an ACME Resource with a pollable status. 029 * <p> 030 * The resource provides a status, and a method for updating the internal cache to read 031 * the current status from the server. 032 * 033 * @since 3.4.0 034 */ 035public interface PollableResource { 036 037 /** 038 * Default delay between status polls if there is no Retry-After header. 039 */ 040 Duration DEFAULT_RETRY_AFTER = Duration.ofSeconds(3L); 041 042 /** 043 * Returns the current status of the resource. 044 */ 045 Status getStatus(); 046 047 /** 048 * Fetches the current status from the server. 049 * 050 * @return Retry-After time, if given by the CA, otherwise empty. 051 */ 052 Optional<Instant> fetch() throws AcmeException; 053 054 /** 055 * Waits until a terminal status has been reached, by polling until one of the given 056 * status or the given timeout has been reached. This call honors the Retry-After 057 * header if set by the CA. 058 * <p> 059 * This method is synchronous and blocks the current thread. 060 * <p> 061 * If the resource is already in a terminal status, the method returns immediately. 062 * 063 * @param statusSet 064 * Set of {@link Status} that are accepted as terminal 065 * @param timeout 066 * Timeout until a terminal status must have been reached 067 * @return Status that was reached 068 */ 069 default Status waitForStatus(Set<Status> statusSet, Duration timeout) 070 throws AcmeException, InterruptedException { 071 Objects.requireNonNull(timeout, "timeout"); 072 Objects.requireNonNull(statusSet, "statusSet"); 073 if (statusSet.isEmpty()) { 074 throw new IllegalArgumentException("At least one Status is required"); 075 } 076 077 var currentStatus = getStatus(); 078 if (statusSet.contains(currentStatus)) { 079 return currentStatus; 080 } 081 082 var timebox = now().plus(timeout); 083 Instant now; 084 085 while ((now = now()).isBefore(timebox)) { 086 // Poll status and get the time of the next poll 087 var retryAfter = fetch() 088 .orElse(now.plus(DEFAULT_RETRY_AFTER)); 089 090 currentStatus = getStatus(); 091 if (statusSet.contains(currentStatus)) { 092 return currentStatus; 093 } 094 095 // Preemptively end the loop if the next iteration would be after timebox 096 if (retryAfter.isAfter(timebox)) { 097 break; 098 } 099 100 // Wait until retryAfter is reached 101 Thread.sleep(now.until(retryAfter, ChronoUnit.MILLIS)); 102 } 103 104 throw new AcmeException("Timeout has been reached"); 105 } 106 107}