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}