001/* 002 * acme4j - Java ACME client 003 * 004 * Copyright (C) 2016 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.connector; 015 016import static java.util.Objects.requireNonNull; 017 018import java.net.URL; 019import java.util.ArrayDeque; 020import java.util.Deque; 021import java.util.Iterator; 022import java.util.NoSuchElementException; 023import java.util.function.BiFunction; 024 025import edu.umd.cs.findbugs.annotations.Nullable; 026import org.shredzone.acme4j.AcmeResource; 027import org.shredzone.acme4j.Login; 028import org.shredzone.acme4j.exception.AcmeException; 029import org.shredzone.acme4j.exception.AcmeProtocolException; 030import org.shredzone.acme4j.toolbox.JSON; 031 032/** 033 * An {@link Iterator} that fetches a batch of URLs from the ACME server, and generates 034 * {@link AcmeResource} instances. 035 * 036 * @param <T> 037 * {@link AcmeResource} type to iterate over 038 */ 039public class ResourceIterator<T extends AcmeResource> implements Iterator<T> { 040 041 private final Login login; 042 private final String field; 043 private final Deque<URL> urlList = new ArrayDeque<>(); 044 private final BiFunction<Login, URL, T> creator; 045 private boolean eol = false; 046 private @Nullable URL nextUrl; 047 048 /** 049 * Creates a new {@link ResourceIterator}. 050 * 051 * @param login 052 * {@link Login} to bind this iterator to 053 * @param field 054 * Field name to be used in the JSON response 055 * @param start 056 * URL of the first JSON array, may be {@code null} for an empty iterator 057 * @param creator 058 * Creator for an {@link AcmeResource} that is bound to the given 059 * {@link Login} and {@link URL}. 060 */ 061 public ResourceIterator(Login login, String field, @Nullable URL start, BiFunction<Login, URL, T> creator) { 062 this.login = requireNonNull(login, "login"); 063 this.field = requireNonNull(field, "field"); 064 this.nextUrl = start; 065 this.creator = requireNonNull(creator, "creator"); 066 } 067 068 /** 069 * Checks if there is another object in the result. 070 * 071 * @throws AcmeProtocolException 072 * if the next batch of URLs could not be fetched from the server 073 */ 074 @Override 075 public boolean hasNext() { 076 if (eol) { 077 return false; 078 } 079 080 if (urlList.isEmpty()) { 081 fetch(); 082 } 083 084 if (urlList.isEmpty()) { 085 eol = true; 086 } 087 088 return !urlList.isEmpty(); 089 } 090 091 /** 092 * Returns the next object of the result. 093 * 094 * @throws AcmeProtocolException 095 * if the next batch of URLs could not be fetched from the server 096 * @throws NoSuchElementException 097 * if there are no more entries 098 */ 099 @Override 100 public T next() { 101 if (!eol && urlList.isEmpty()) { 102 fetch(); 103 } 104 105 var next = urlList.poll(); 106 if (next == null) { 107 eol = true; 108 throw new NoSuchElementException("no more " + field); 109 } 110 111 return creator.apply(login, next); 112 } 113 114 /** 115 * Unsupported operation, only here to satisfy the {@link Iterator} interface. 116 */ 117 @Override 118 public void remove() { 119 throw new UnsupportedOperationException("cannot remove " + field); 120 } 121 122 /** 123 * Fetches the next batch of URLs. Handles exceptions. Does nothing if there is no 124 * URL of the next batch. 125 */ 126 private void fetch() { 127 if (nextUrl == null) { 128 return; 129 } 130 131 try { 132 readAndQueue(); 133 } catch (AcmeException ex) { 134 throw new AcmeProtocolException("failed to read next set of " + field, ex); 135 } 136 } 137 138 /** 139 * Reads the next batch of URLs from the server, and fills the queue with the URLs. If 140 * there is a "next" header, it is used for the next batch of URLs. 141 */ 142 private void readAndQueue() throws AcmeException { 143 var session = login.getSession(); 144 try (var conn = session.connect()) { 145 conn.sendSignedPostAsGetRequest(requireNonNull(nextUrl), login); 146 fillUrlList(conn.readJsonResponse()); 147 148 nextUrl = conn.getLinks("next").stream().findFirst().orElse(null); 149 } 150 } 151 152 /** 153 * Fills the url list with the URLs found in the desired field. 154 * 155 * @param json 156 * JSON map to read from 157 */ 158 private void fillUrlList(JSON json) { 159 json.get(field).asArray().stream() 160 .map(JSON.Value::asURL) 161 .forEach(urlList::add); 162 } 163 164}