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