001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools; 003 004import java.io.BufferedOutputStream; 005import java.io.ByteArrayInputStream; 006import java.io.IOException; 007import java.io.InputStream; 008import java.io.OutputStream; 009import java.net.HttpURLConnection; 010import java.net.URL; 011import java.util.Collections; 012import java.util.List; 013import java.util.Map; 014import java.util.Map.Entry; 015import java.util.Optional; 016import java.util.TreeMap; 017 018import org.openstreetmap.josm.data.Version; 019import org.openstreetmap.josm.gui.progress.ProgressMonitor; 020import org.openstreetmap.josm.io.ProgressOutputStream; 021 022/** 023 * Provides a uniform access for a HTTP/HTTPS 1.0/1.1 server. 024 * @since 15229 025 */ 026public final class Http1Client extends HttpClient { 027 028 private HttpURLConnection connection; // to allow disconnecting before `response` is set 029 030 /** 031 * Constructs a new {@code Http1Client}. 032 * @param url URL to access 033 * @param requestMethod HTTP request method (GET, POST, PUT, DELETE...) 034 */ 035 public Http1Client(URL url, String requestMethod) { 036 super(url, requestMethod); 037 } 038 039 @Override 040 protected void setupConnection(ProgressMonitor progressMonitor) throws IOException { 041 connection = (HttpURLConnection) getURL().openConnection(); 042 connection.setRequestMethod(getRequestMethod()); 043 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString()); 044 connection.setConnectTimeout(getConnectTimeout()); 045 connection.setReadTimeout(getReadTimeout()); 046 connection.setInstanceFollowRedirects(false); // we do that ourselves 047 if (getIfModifiedSince() > 0) { 048 connection.setIfModifiedSince(getIfModifiedSince()); 049 } 050 connection.setUseCaches(isUseCache()); 051 if (!isUseCache()) { 052 connection.setRequestProperty("Cache-Control", "no-cache"); 053 } 054 for (Map.Entry<String, String> header : getHeaders().entrySet()) { 055 if (header.getValue() != null) { 056 connection.setRequestProperty(header.getKey(), header.getValue()); 057 } 058 } 059 060 notifyConnect(progressMonitor); 061 062 if (requiresBody()) { 063 logRequestBody(); 064 byte[] body = getRequestBody(); 065 connection.setFixedLengthStreamingMode(body.length); 066 connection.setDoOutput(true); 067 try (OutputStream out = new BufferedOutputStream( 068 new ProgressOutputStream(connection.getOutputStream(), body.length, 069 progressMonitor, getOutputMessage(), isFinishOnCloseOutput()))) { 070 out.write(body); 071 } 072 } 073 } 074 075 @Override 076 protected ConnectionResponse performConnection() throws IOException { 077 try { 078 connection.connect(); 079 } catch (RuntimeException e) { 080 throw new IOException(e); 081 } 082 return new ConnectionResponse() { 083 @Override 084 public String getResponseVersion() { 085 String headerField = connection.getHeaderField(0); 086 if (headerField != null && headerField.startsWith("HTTP")) { 087 return headerField.replaceFirst(" .*", ""); 088 } 089 return "HTTP/1"; 090 } 091 092 @Override 093 public int getResponseCode() throws IOException { 094 return connection.getResponseCode(); 095 } 096 097 @Override 098 public String getHeaderField(String name) { 099 return connection.getHeaderField(name); 100 } 101 102 @Override 103 public long getContentLengthLong() { 104 return connection.getContentLengthLong(); 105 } 106 107 @Override 108 public Map<String, List<String>> getHeaderFields() { 109 return connection.getHeaderFields(); 110 } 111 }; 112 } 113 114 @Override 115 protected void performDisconnection() throws IOException { 116 connection.disconnect(); 117 } 118 119 @Override 120 protected Response buildResponse(ProgressMonitor progressMonitor) throws IOException { 121 return new Http1Response(connection, progressMonitor); 122 } 123 124 /** 125 * A wrapper for the HTTP 1.x response. 126 */ 127 public static final class Http1Response extends Response { 128 private final HttpURLConnection connection; 129 130 private Http1Response(HttpURLConnection connection, ProgressMonitor progressMonitor) throws IOException { 131 super(progressMonitor, connection.getResponseCode(), connection.getResponseMessage()); 132 this.connection = connection; 133 debugRedirect(); 134 } 135 136 @Override 137 public URL getURL() { 138 return connection.getURL(); 139 } 140 141 @Override 142 public String getRequestMethod() { 143 return connection.getRequestMethod(); 144 } 145 146 @Override 147 public InputStream getInputStream() throws IOException { 148 InputStream in; 149 try { 150 in = connection.getInputStream(); 151 } catch (IOException ioe) { 152 Logging.debug(ioe); 153 in = Optional.ofNullable(connection.getErrorStream()).orElseGet(() -> new ByteArrayInputStream(new byte[]{})); 154 } 155 return in; 156 } 157 158 @Override 159 public String getContentEncoding() { 160 return connection.getContentEncoding(); 161 } 162 163 @Override 164 public String getContentType() { 165 return connection.getHeaderField("Content-Type"); 166 } 167 168 @Override 169 public long getExpiration() { 170 return connection.getExpiration(); 171 } 172 173 @Override 174 public long getLastModified() { 175 return connection.getLastModified(); 176 } 177 178 @Override 179 public long getContentLength() { 180 return connection.getContentLengthLong(); 181 } 182 183 @Override 184 public String getHeaderField(String name) { 185 return connection.getHeaderField(name); 186 } 187 188 @Override 189 public Map<String, List<String>> getHeaderFields() { 190 // returned map from HttpUrlConnection is case sensitive, use case insensitive TreeMap to conform to RFC 2616 191 Map<String, List<String>> ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 192 for (Entry<String, List<String>> e: connection.getHeaderFields().entrySet()) { 193 if (e.getKey() != null) { 194 ret.put(e.getKey(), e.getValue()); 195 } 196 } 197 return Collections.unmodifiableMap(ret); 198 } 199 200 @Override 201 public void disconnect() { 202 Http1Client.disconnect(connection); 203 } 204 } 205 206 /** 207 * @see HttpURLConnection#disconnect() 208 */ 209 @Override 210 public void disconnect() { 211 Http1Client.disconnect(connection); 212 } 213 214 private static void disconnect(final HttpURLConnection connection) { 215 if (connection != null) { 216 // Fix upload aborts - see #263 217 connection.setConnectTimeout(100); 218 connection.setReadTimeout(100); 219 try { 220 Thread.sleep(100); 221 } catch (InterruptedException ex) { 222 Logging.warn("InterruptedException in " + Http1Client.class + " during cancel"); 223 Thread.currentThread().interrupt(); 224 } 225 connection.disconnect(); 226 } 227 } 228}