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}