Skip to content

Commit b142587

Browse files
authored
Merge pull request #463 from metafacture/extendHttpOpener
Extend `HttpOpener`. (#460)
2 parents d057f20 + b672731 commit b142587

File tree

3 files changed

+541
-31
lines changed

3 files changed

+541
-31
lines changed

metafacture-io/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ dependencies {
2323
implementation 'commons-io:commons-io:2.5'
2424
implementation 'org.apache.commons:commons-compress:1.21'
2525
runtimeOnly 'org.tukaani:xz:1.6'
26+
testImplementation 'com.github.tomakehurst:wiremock-jre8:2.33.2'
2627
testImplementation 'junit:junit:4.12'
2728
testImplementation 'org.mockito:mockito-core:2.5.5'
2829
testImplementation 'org.assertj:assertj-core:3.11.1'
30+
testRuntimeOnly 'org.slf4j:slf4j-simple:1.7.21'
2931
}
Lines changed: 210 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013, 2014 Deutsche Nationalbibliothek
2+
* Copyright 2013, 2022 Deutsche Nationalbibliothek et al
33
*
44
* Licensed under the Apache License, Version 2.0 the "License";
55
* you may not use this file except in compliance with the License.
@@ -24,63 +24,150 @@
2424
import org.metafacture.framework.annotations.Out;
2525
import org.metafacture.framework.helpers.DefaultObjectPipe;
2626

27+
import java.io.ByteArrayInputStream;
2728
import java.io.IOException;
29+
import java.io.InputStream;
2830
import java.io.InputStreamReader;
2931
import java.io.Reader;
32+
import java.io.SequenceInputStream;
33+
import java.net.HttpURLConnection;
3034
import java.net.URL;
31-
import java.net.URLConnection;
3235
import java.util.Arrays;
3336
import java.util.HashMap;
3437
import java.util.Map;
3538
import java.util.regex.Pattern;
3639

3740
/**
38-
* Opens a {@link URLConnection} and passes a reader to the receiver.
41+
* Opens an {@link HttpURLConnection} and passes a reader to the receiver.
3942
*
4043
* @author Christoph Böhme
4144
* @author Jan Schnasse
45+
* @author Jens Wille
4246
*/
43-
@Description("Opens an HTTP resource. Supports the setting of `Accept` and `Accept-Charset` as HTTP header fields, as well as generic headers (separated by `\\n`).")
47+
@Description("Opens an HTTP resource. Supports setting HTTP header fields `Accept`, `Accept-Charset` and `Content-Type`, as well as generic headers (separated by `\\n`). Defaults: request `method` = `GET`, request `url` = `@-` (input data), request `body` = `@-` (input data) if request method supports body and input data not already used, `Accept` header = `*/*`, `Accept-Charset` header (`encoding`) = `UTF-8`, `errorPrefix` = `ERROR: `.")
4448
@In(String.class)
4549
@Out(Reader.class)
4650
@FluxCommand("open-http")
4751
public final class HttpOpener extends DefaultObjectPipe<String, ObjectReceiver<Reader>> {
4852

49-
private static final Pattern HEADER_FIELD_SEPARATOR = Pattern.compile("\n");
50-
private static final Pattern HEADER_VALUE_SEPARATOR = Pattern.compile(":");
53+
public static final String ACCEPT_DEFAULT = "*/*";
54+
public static final String ACCEPT_HEADER = "accept";
55+
public static final String CONTENT_TYPE_HEADER = "content-type";
56+
public static final String DEFAULT_PREFIX = "ERROR: ";
57+
public static final String ENCODING_DEFAULT = "UTF-8";
58+
public static final String ENCODING_HEADER = "accept-charset";
59+
public static final String INPUT_DESIGNATOR = "@-";
5160

52-
private static final String ACCEPT_HEADER = "accept";
53-
private static final String ENCODING_HEADER = "accept-charset";
61+
public static final String DEFAULT_METHOD_NAME = "GET";
62+
public static final Method DEFAULT_METHOD = Method.valueOf(DEFAULT_METHOD_NAME);
5463

55-
private static final String ACCEPT_DEFAULT = "*/*";
56-
private static final String ENCODING_DEFAULT = "UTF-8";
64+
public static final String HEADER_FIELD_SEPARATOR = "\n";
65+
public static final String HEADER_VALUE_SEPARATOR = ":";
66+
67+
private static final Pattern HEADER_FIELD_SEPARATOR_PATTERN = Pattern.compile(HEADER_FIELD_SEPARATOR);
68+
private static final Pattern HEADER_VALUE_SEPARATOR_PATTERN = Pattern.compile(HEADER_VALUE_SEPARATOR);
5769

5870
private final Map<String, String> headers = new HashMap<>();
5971

72+
private Method method;
73+
private String body;
74+
private String errorPrefix;
75+
private String url;
76+
private boolean inputUsed;
77+
78+
public enum Method {
79+
80+
DELETE(false, true),
81+
GET(false, true),
82+
HEAD(false, false),
83+
OPTIONS(false, true),
84+
POST(true, true),
85+
PUT(true, true),
86+
TRACE(false, true);
87+
88+
private final boolean requestHasBody;
89+
private final boolean responseHasBody;
90+
91+
Method(final boolean requestHasBody, final boolean responseHasBody) {
92+
this.requestHasBody = requestHasBody;
93+
this.responseHasBody = responseHasBody;
94+
}
95+
96+
/**
97+
* Checks whether the request method accepts a request body.
98+
*
99+
* @return true if the request method accepts a request body
100+
*/
101+
public boolean getRequestHasBody() {
102+
return requestHasBody;
103+
}
104+
105+
/**
106+
* Checks whether the request method returns a response body.
107+
*
108+
* @return true if the request method returns a response body
109+
*/
110+
public boolean getResponseHasBody() {
111+
return responseHasBody;
112+
}
113+
114+
}
115+
60116
/**
61117
* Creates an instance of {@link HttpOpener}.
62118
*/
63119
public HttpOpener() {
64120
setAccept(ACCEPT_DEFAULT);
65121
setEncoding(ENCODING_DEFAULT);
122+
setErrorPrefix(DEFAULT_PREFIX);
123+
setMethod(DEFAULT_METHOD);
124+
setUrl(INPUT_DESIGNATOR);
66125
}
67126

68127
/**
69-
* Sets the HTTP accept header value. This is a mime-type such as text/plain
70-
* or text/html. The default value of the accept is *&#47;* which means
71-
* any mime-type.
128+
* Sets the HTTP {@value ACCEPT_HEADER} header value. This is a MIME type
129+
* such as {@code text/plain} or {@code application/json}. The default
130+
* value for the accept header is {@value ACCEPT_DEFAULT} which means
131+
* any MIME type.
72132
*
73-
* @param accept mime-type to use for the HTTP accept header
133+
* @param accept MIME type to use for the HTTP accept header
74134
*/
75135
public void setAccept(final String accept) {
76136
setHeader(ACCEPT_HEADER, accept);
77137
}
78138

79139
/**
80-
* Sets the preferred encoding of the HTTP response. This value is in the
81-
* accept-charset header. Additonally, the encoding is used for reading the
82-
* HTTP resonse if it does not specify an encoding. The default value for
83-
* the encoding is UTF-8.
140+
* Sets the HTTP request body. The default value for the request body is
141+
* {@value INPUT_DESIGNATOR} <i>if the {@link #setMethod(Method) request
142+
* method} accepts a request body</i>, which means it will use the {@link
143+
* #process(String) input data} data as request body <i>if the input has
144+
* not already been used</i>; otherwise, no request body will be set by
145+
* default.
146+
*
147+
* <p>If a request body has been set, but the request method does not
148+
* accept a body, the method <i>may</i> be changed to {@code POST}.
149+
*
150+
* @param body the request body
151+
*/
152+
public void setBody(final String body) {
153+
this.body = body;
154+
}
155+
156+
/**
157+
* Sets the HTTP {@value CONTENT_TYPE_HEADER} header value. This is a
158+
* MIME type such as {@code text/plain} or {@code application/json}.
159+
*
160+
* @param contentType MIME type to use for the HTTP content-type header
161+
*/
162+
public void setContentType(final String contentType) {
163+
setHeader(CONTENT_TYPE_HEADER, contentType);
164+
}
165+
166+
/**
167+
* Sets the HTTP {@value ENCODING_HEADER} header value. This is the
168+
* preferred encoding for the HTTP response. Additionally, the encoding
169+
* is used for reading the HTTP response if it does not specify a content
170+
* encoding. The default for the encoding is {@value ENCODING_DEFAULT}.
84171
*
85172
* @param encoding name of the encoding used for the accept-charset HTTP
86173
* header
@@ -90,14 +177,28 @@ public void setEncoding(final String encoding) {
90177
}
91178

92179
/**
93-
* Sets a request property, or multiple request properties separated by
94-
* {@code \n}.
180+
* Sets the error prefix. The default error prefix is
181+
* {@value DEFAULT_PREFIX}.
182+
*
183+
* @param errorPrefix the error prefix
184+
*/
185+
public void setErrorPrefix(final String errorPrefix) {
186+
this.errorPrefix = errorPrefix;
187+
}
188+
189+
/**
190+
* Sets a request property (header), or multiple request properties
191+
* separated by {@value HEADER_FIELD_SEPARATOR}. Header name and value
192+
* are separated by {@value HEADER_VALUE_SEPARATOR}. The header name is
193+
* case-insensitive.
95194
*
96195
* @param header request property line
196+
*
197+
* @see #setHeader(String, String)
97198
*/
98199
public void setHeader(final String header) {
99-
Arrays.stream(HEADER_FIELD_SEPARATOR.split(header)).forEach(h -> {
100-
final String[] parts = HEADER_VALUE_SEPARATOR.split(h, 2);
200+
Arrays.stream(HEADER_FIELD_SEPARATOR_PATTERN.split(header)).forEach(h -> {
201+
final String[] parts = HEADER_VALUE_SEPARATOR_PATTERN.split(h, 2);
101202
if (parts.length == 2) {
102203
setHeader(parts[0], parts[1].trim());
103204
}
@@ -108,7 +209,7 @@ public void setHeader(final String header) {
108209
}
109210

110211
/**
111-
* Sets a request property.
212+
* Sets a request property (header). The header name is case-insensitive.
112213
*
113214
* @param key request property key
114215
* @param value request property value
@@ -117,21 +218,99 @@ public void setHeader(final String key, final String value) {
117218
headers.put(key.toLowerCase(), value);
118219
}
119220

221+
/**
222+
* Sets the HTTP request method. The default request method is
223+
* {@value DEFAULT_METHOD_NAME}.
224+
*
225+
* @param method the request method
226+
*/
227+
public void setMethod(final Method method) {
228+
this.method = method;
229+
}
230+
231+
/**
232+
* Sets the HTTP request URL. The default value for the request URL is
233+
* {@value INPUT_DESIGNATOR}, which means it will use the {@link
234+
* #process(String) input data} as request URL.
235+
*
236+
* @param url the request URL
237+
*/
238+
public void setUrl(final String url) {
239+
this.url = url;
240+
}
241+
120242
@Override
121-
public void process(final String urlStr) {
243+
public void process(final String input) {
122244
try {
123-
final URL url = new URL(urlStr);
124-
final URLConnection con = url.openConnection();
125-
headers.forEach(con::addRequestProperty);
126-
String enc = con.getContentEncoding();
127-
if (enc == null) {
128-
enc = headers.get(ENCODING_HEADER);
245+
final String requestUrl = getInput(input, url);
246+
final String requestBody = getInput(input,
247+
body == null && method.getRequestHasBody() ? INPUT_DESIGNATOR : body);
248+
249+
final HttpURLConnection connection =
250+
(HttpURLConnection) new URL(requestUrl).openConnection();
251+
252+
connection.setRequestMethod(method.name());
253+
headers.forEach(connection::addRequestProperty);
254+
255+
if (requestBody != null) {
256+
connection.setDoOutput(true);
257+
connection.getOutputStream().write(requestBody.getBytes());
129258
}
130-
getReceiver().process(new InputStreamReader(con.getInputStream(), enc));
259+
260+
final InputStream inputStream = getInputStream(connection);
261+
final String contentEncoding = getEncoding(connection.getContentEncoding());
262+
263+
getReceiver().process(new InputStreamReader(inputStream, contentEncoding));
131264
}
132265
catch (final IOException e) {
133266
throw new MetafactureException(e);
134267
}
135268
}
136269

270+
private String getInput(final String input, final String value) {
271+
final String result;
272+
273+
if (!INPUT_DESIGNATOR.equals(value)) {
274+
result = value;
275+
}
276+
else if (inputUsed) {
277+
result = null;
278+
}
279+
else {
280+
inputUsed = true;
281+
result = input;
282+
}
283+
284+
return result;
285+
}
286+
287+
private InputStream getInputStream(final HttpURLConnection connection) throws IOException {
288+
try {
289+
return connection.getInputStream();
290+
}
291+
catch (final IOException e) {
292+
final InputStream errorStream = connection.getErrorStream();
293+
if (errorStream != null) {
294+
return getErrorStream(errorStream);
295+
}
296+
else {
297+
throw e;
298+
}
299+
}
300+
}
301+
302+
private InputStream getErrorStream(final InputStream errorStream) {
303+
if (errorPrefix != null) {
304+
final InputStream errorPrefixStream = new ByteArrayInputStream(errorPrefix.getBytes());
305+
return new SequenceInputStream(errorPrefixStream, errorStream);
306+
}
307+
else {
308+
return errorStream;
309+
}
310+
}
311+
312+
private String getEncoding(final String contentEncoding) {
313+
return contentEncoding != null ? contentEncoding : headers.get(ENCODING_HEADER);
314+
}
315+
137316
}

0 commit comments

Comments
 (0)