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}