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 connection.connect(); 078 return new ConnectionResponse() { 079 @Override 080 public String getResponseVersion() { 081 return "HTTP_1"; 082 } 083 084 @Override 085 public int getResponseCode() throws IOException { 086 return connection.getResponseCode(); 087 } 088 089 @Override 090 public String getHeaderField(String name) { 091 return connection.getHeaderField(name); 092 } 093 094 @Override 095 public long getContentLengthLong() { 096 return connection.getContentLengthLong(); 097 } 098 099 @Override 100 public Map<String, List<String>> getHeaderFields() { 101 return connection.getHeaderFields(); 102 } 103 }; 104 } 105 106 @Override 107 protected void performDisconnection() throws IOException { 108 connection.disconnect(); 109 } 110 111 @Override 112 protected Response buildResponse(ProgressMonitor progressMonitor) throws IOException { 113 return new Http1Response(connection, progressMonitor); 114 } 115 116 /** 117 * A wrapper for the HTTP 1.x response. 118 */ 119 public static final class Http1Response extends Response { 120 private final HttpURLConnection connection; 121 122 private Http1Response(HttpURLConnection connection, ProgressMonitor progressMonitor) throws IOException { 123 super(progressMonitor, connection.getResponseCode(), connection.getResponseMessage()); 124 this.connection = connection; 125 debugRedirect(); 126 } 127 128 @Override 129 public URL getURL() { 130 return connection.getURL(); 131 } 132 133 @Override 134 public String getRequestMethod() { 135 return connection.getRequestMethod(); 136 } 137 138 @Override 139 public InputStream getInputStream() throws IOException { 140 InputStream in; 141 try { 142 in = connection.getInputStream(); 143 } catch (IOException ioe) { 144 Logging.debug(ioe); 145 in = Optional.ofNullable(connection.getErrorStream()).orElseGet(() -> new ByteArrayInputStream(new byte[]{})); 146 } 147 return in; 148 } 149 150 @Override 151 public String getContentEncoding() { 152 return connection.getContentEncoding(); 153 } 154 155 @Override 156 public String getContentType() { 157 return connection.getHeaderField("Content-Type"); 158 } 159 160 @Override 161 public long getExpiration() { 162 return connection.getExpiration(); 163 } 164 165 @Override 166 public long getLastModified() { 167 return connection.getLastModified(); 168 } 169 170 @Override 171 public long getContentLength() { 172 return connection.getContentLengthLong(); 173 } 174 175 @Override 176 public String getHeaderField(String name) { 177 return connection.getHeaderField(name); 178 } 179 180 @Override 181 public Map<String, List<String>> getHeaderFields() { 182 // returned map from HttpUrlConnection is case sensitive, use case insensitive TreeMap to conform to RFC 2616 183 Map<String, List<String>> ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 184 for (Entry<String, List<String>> e: connection.getHeaderFields().entrySet()) { 185 if (e.getKey() != null) { 186 ret.put(e.getKey(), e.getValue()); 187 } 188 } 189 return Collections.unmodifiableMap(ret); 190 } 191 192 @Override 193 public void disconnect() { 194 Http1Client.disconnect(connection); 195 } 196 } 197 198 /** 199 * @see HttpURLConnection#disconnect() 200 */ 201 @Override 202 public void disconnect() { 203 Http1Client.disconnect(connection); 204 } 205 206 private static void disconnect(final HttpURLConnection connection) { 207 if (connection != null) { 208 // Fix upload aborts - see #263 209 connection.setConnectTimeout(100); 210 connection.setReadTimeout(100); 211 try { 212 Thread.sleep(100); 213 } catch (InterruptedException ex) { 214 Logging.warn("InterruptedException in " + Http1Client.class + " during cancel"); 215 Thread.currentThread().interrupt(); 216 } 217 connection.disconnect(); 218 } 219 } 220}