1 /*
2 * SPDX-FileCopyrightText: Copyright (c) 2013-2025 Yegor Bugayenko
3 * SPDX-License-Identifier: MIT
4 */
5 package com.jcabi.github.wire;
6
7 import com.jcabi.aspects.Immutable;
8 import com.jcabi.http.Request;
9 import com.jcabi.http.Response;
10 import com.jcabi.http.Wire;
11 import com.jcabi.log.Logger;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.util.Collection;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.concurrent.TimeUnit;
18 import lombok.EqualsAndHashCode;
19 import lombok.ToString;
20
21 /**
22 * Wire that waits if number of remaining request per hour is less than
23 * a given threshold.
24 *
25 * <p>GitHub sets following headers in each response:
26 * {@code X-RateLimit-Limit}, {@code X-RateLimit-Remaining}, and
27 * {@code X-RateLimit-Reset}. If {@code X-RateLimit-Remaining} is
28 * less than a given threshold, {@code CarefulWire} will sleep until a time
29 * specified in the {@code X-RateLimit-Reset} header. For further information
30 * about the GitHub rate limiting see
31 * <a href="https://developer.github.com/v3/#rate-limiting">API
32 * documentation</a>.
33 *
34 * <p>You can use {@code CarefulWire} with a {@link com.jcabi.github.GitHub}
35 * object:
36 * <pre>
37 * {@code
38 * GitHub github = new RtGitHub(
39 * new RtGitHub().entry().through(CarefulWire.class, 50)
40 * );
41 * }
42 * </pre>
43 * @since 0.4
44 */
45 @Immutable
46 @ToString
47 @EqualsAndHashCode(of = { "origin", "threshold" })
48 @SuppressWarnings("PMD.AvoidDuplicateLiterals")
49 public final class CarefulWire implements Wire {
50
51 /**
52 * Original wire.
53 */
54 private final transient Wire origin;
55
56 /**
57 * Threshold of number of remaining requests, below which requests are
58 * blocked until reset.
59 */
60 private final transient int threshold;
61
62 /**
63 * Public ctor.
64 *
65 * @param wire Original wire
66 * @param thrshld Threshold of number of remaining requests, below which
67 * requests are blocked until reset
68 */
69 public CarefulWire(final Wire wire, final int thrshld) {
70 this.origin = wire;
71 this.threshold = thrshld;
72 }
73
74 @Override
75 // @checkstyle ParameterNumber (8 lines)
76 public Response send(
77 final Request req,
78 final String home,
79 final String method,
80 final Collection<Map.Entry<String, String>> headers,
81 final InputStream content,
82 final int connect, final int read
83 ) throws IOException {
84 final Response resp = this.origin
85 .send(req, home, method, headers, content, connect, read);
86 final int remaining = CarefulWire.remainingHeader(resp);
87 if (remaining < this.threshold) {
88 final long reset = CarefulWire.resetHeader(resp);
89 final long now = TimeUnit.MILLISECONDS
90 .toSeconds(System.currentTimeMillis());
91 if (reset > now) {
92 final long length = reset - now;
93 Logger.info(
94 this,
95 // @checkstyle LineLength (1 line)
96 "Remaining number of requests per hour is less than %d. Waiting for %d seconds.",
97 this.threshold, length
98 );
99 try {
100 TimeUnit.SECONDS.sleep(length);
101 } catch (final InterruptedException ex) {
102 Thread.currentThread().interrupt();
103 throw new IllegalStateException(ex);
104 }
105 }
106 }
107 return resp;
108 }
109
110 /**
111 * Get the header with the given name from the response.
112 * If there is no such header, returns null.
113 * @param resp Response to get header from
114 * @param headername Name of header to get
115 * @return The value of the first header with the given name, or null.
116 * @checkstyle NonStaticMethodCheck (5 lines)
117 */
118 @SuppressWarnings("PMD.ProhibitPublicStaticMethods")
119 private static String headerOrNull(
120 final Response resp,
121 final String headername) {
122 final List<String> values = resp.headers().get(headername);
123 String value = null;
124 if (values != null && !values.isEmpty()) {
125 value = values.get(0);
126 }
127 return value;
128 }
129
130 /**
131 * Returns the value of the X-RateLimit-Remaining header.
132 * If there is no such header, returns Integer.MAX_VALUE (no rate limit).
133 * @param resp Response to get header from
134 * @return Number of requests remaining before the rate limit will be hit
135 */
136 private static int remainingHeader(
137 final Response resp) {
138 final String remainingstr = CarefulWire.headerOrNull(
139 resp,
140 "X-RateLimit-Remaining"
141 );
142 int remaining = Integer.MAX_VALUE;
143 if (remainingstr != null) {
144 remaining = Integer.parseInt(remainingstr);
145 }
146 return remaining;
147 }
148
149 /**
150 * Returns the value of the X-RateLimit-Reset header.
151 * If there is no such header, returns 0 (reset immediately).
152 * @param resp Response to get header from
153 * @return Timestamp (in seconds) at which the rate limit will reset
154 */
155 private static long resetHeader(
156 final Response resp) {
157 final String resetstr = CarefulWire.headerOrNull(resp, "X-RateLimit-Reset");
158 long reset = 0;
159 if (resetstr != null) {
160 reset = Long.parseLong(resetstr);
161 }
162 return reset;
163 }
164
165 }