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.HttpURLConnection; 017import java.net.URL; 018import java.util.ArrayDeque; 019import java.util.Deque; 020import java.util.Iterator; 021import java.util.NoSuchElementException; 022import java.util.Objects; 023import java.util.function.BiFunction; 024 025import org.shredzone.acme4j.AcmeResource; 026import org.shredzone.acme4j.Session; 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 Session session; 041 private final String field; 042 private final Deque<URL> urlList = new ArrayDeque<>(); 043 private final BiFunction<Session, URL, T> creator; 044 private boolean eol = false; 045 private URL nextUrl; 046 047 /** 048 * Creates a new {@link ResourceIterator}. 049 * 050 * @param session 051 * {@link Session} 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 Session} and {@link URL}. 059 */ 060 public ResourceIterator(Session session, String field, URL start, BiFunction<Session, URL, T> creator) { 061 this.session = Objects.requireNonNull(session, "session"); 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 URL next = urlList.poll(); 105 if (next == null) { 106 eol = true; 107 throw new NoSuchElementException("no more " + field); 108 } 109 110 return creator.apply(session, 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 try (Connection conn = session.provider().connect()) { 143 conn.sendRequest(nextUrl, session); 144 conn.accept(HttpURLConnection.HTTP_OK); 145 146 JSON json = conn.readJsonResponse(); 147 fillUrlList(json); 148 149 nextUrl = conn.getLink("next"); 150 } 151 } 152 153 /** 154 * Fills the url list with the URLs found in the desired field. 155 * 156 * @param json 157 * JSON map to read from 158 */ 159 private void fillUrlList(JSON json) { 160 JSON.Array array = json.get(field).asArray(); 161 if (array == null) { 162 return; 163 } 164 165 array.stream().map(JSON.Value::asURL).forEach(urlList::add); 166 } 167 168}