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.
2424import org .metafacture .framework .annotations .Out ;
2525import org .metafacture .framework .helpers .DefaultObjectPipe ;
2626
27+ import java .io .ByteArrayInputStream ;
2728import java .io .IOException ;
29+ import java .io .InputStream ;
2830import java .io .InputStreamReader ;
2931import java .io .Reader ;
32+ import java .io .SequenceInputStream ;
33+ import java .net .HttpURLConnection ;
3034import java .net .URL ;
31- import java .net .URLConnection ;
3235import java .util .Arrays ;
3336import java .util .HashMap ;
3437import java .util .Map ;
3538import 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" )
4751public 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 */* 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